Skip to content

Repository files navigation

Zario

Fast structured logging without the ceremony

Bun-first structured logging for TypeScript and Node.js. Zero runtime dependencies.

npm version license downloads bundle size

Japanese

Install

bun add zario

Node.js users can install the same package with their package manager of choice.

npm install zario
# or
pnpm add zario

10-second start

import { zario } from "zario";

const log = zario();

log.info("server started", { port: 3000 });
log.error(new Error("database unavailable"));

const requestLog = log.child({ requestId: "req-123" });
requestLog.info("request completed", { status: 200 });

zario() enables info and higher, writes synchronously to the console, and creates no log files. Production uses JSON; development uses colored text. Options override those defaults.

Why Zario?

  • Zero runtime dependencies — a small dependency surface with nothing else pulled into production.
  • Bun-first, Node-compatible — Bun is the primary development and benchmark runtime; Node.js 20+ is supported.
  • Structured by default — metadata, child context, direct Error objects, circular values, and BigInt are handled without custom serializers for common cases.
  • Predictable shutdownflush() and close() let applications drain queued logs and transport work before exit.
  • Built-in redaction — redact sensitive paths before output.
  • Transport surface included — Console, File, HTTP, retry/circuit-breaker and dead-letter patterns are available without adding a logging plugin stack.
  • Lean import available — use zario/logger when you only need the core logger.

Framework integrations

Official adapters are available for common TypeScript backends:

Elysia on Bun

import { Elysia } from "elysia";
import { elysiaLogger } from "elysia-zario";

new Elysia()
  .use(elysiaLogger())
  .get("/", ({ log, requestId }) => {
    log.info("request handled");
    return { requestId };
  })
  .listen(3000);

The adapter adds typed request loggers, request IDs, completion/error logs, and shutdown hooks. It targets Zario 0.9.0. See the Bun guide for setup and local development.

Zario, Pino, or Winston?

Zario is not presented as universally faster than every logger in every workload. The checked-in comparison reports where it wins, where it loses, the exact environment, and how to reproduce the measurements.

Start with the API comparison and measurement caveats. The benchmark report includes reproducible commands and raw output. Historical numbers do not describe the current serialization and shutdown implementation; no universal throughput claim is made here.

If you are migrating an existing service, see Migrating from Pino.

Everyday logging

import { zario } from "zario";

const log = zario();
log.info("Server ready", { port: 3000 });
log.info({ port: 3000 }, "Server ready"); // Object-first works too
log.info({ event: "heartbeat" });          // Message is optional for objects
log.error(new Error("Connection failed")); // Keeps name, message and stack
log.error("Query failed", new Error("Database unavailable"));

const requestLog = log.child({ requestId: "req-123" });
requestLog.info("Request completed", { status: 200 });

if (log.isLevelEnabled("debug")) {
  log.debug("Diagnostics", { details: {} });
}

Both argument orders work with every level and logWithLevel. Direct errors are stored under err; nested errors also retain their diagnostic fields, including cause. Object-only calls use an empty message. Existing createChild({ context: ... }) calls continue to work; child(context, options?) is shorthand, and per-call metadata overrides child bindings.

Circular metadata is serialized as "[Circular]" in text, JSON, HTTP payloads, and dead-letter records. Shared objects that are not cycles remain intact. BigInt values become decimal strings. Serialization does not mutate your data.

JSON and redaction

const log = zario({
  json: true,
  redact: { paths: ["password", "user.token"] },
});

log.info("Login", { user: { id: 42, token: "secret" } });

Shutdown

await log.flush(); // Wait for queued logs and transport buffers
await log.close(); // Drain, release resources, stop accepting logs

Await shutdown before exiting the process. Closing a child leaves inherited transports open; closing its parent also closes its children. Failed delivery rejects flush()/close(); custom transports should implement lifecycle hooks for any background work they start.

File transport

import { Logger, FileTransport } from "zario";

const logger = new Logger({
  transports: [
    new FileTransport({
      path: "./logs/app.log",
      maxSize: 10 * 1024 * 1024,
      maxFiles: 5,
    }),
  ],
});

Lean import

If you only need the core logger:

import { zario } from "zario/logger";

const log = zario();

Documentation

Section Description
Bun and Elysia Bun-first setup, native usage, and typed Elysia integration
Migrating from Pino Call mapping and compatibility differences
Comparison API examples and benchmark limitations
Configuration Logger options, custom levels, and colors
API Reference Logger class and utilities
Transports Console, File, HTTP, CircuitBreaker, DeadLetterQueue
Advanced Usage Filters, enrichers, aggregators, async mode
Log Formats Text and JSON output spec
Benchmarks Performance comparison with other libraries
Roadmap Future plans

Contributing

Bug reports, feature requests, and pull requests are welcome. See the Contributing Guide.

License

MIT License

If Zario is useful in your project, consider starring the repository.

Back to Top

About

Fast structured logging for TypeScript, Bun and Node.js. Zero runtime dependencies.

Topics

Resources

Code of conduct

Contributing

Stars

7 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages