@azlib/logger
v0.2.0
Published
A lightweight, fast, structured logger for Node.js. Log calls are synchronous ring-buffer pushes — zero format or I/O work on the calling thread. Drain and transport I/O happen asynchronously on `process.nextTick`.
Readme
@azlib/logger
A lightweight, fast, structured logger for Node.js. Log calls are synchronous ring-buffer pushes — zero format or I/O work on the calling thread. Drain and transport I/O happen asynchronously on process.nextTick.
Installation
pnpm add @azlib/logger
# Optional: SQLite adapter peer dependency
pnpm add better-sqlite3Quick Start
import { createLogger, createConsoleTransport } from "@azlib/logger";
const logger = createLogger({
level: "info",
transports: [createConsoleTransport()],
});
logger.info("Application started", { port: 3000 });
logger.warn("High memory", { usedMb: 512 });
await logger.close();Entry Points
| Import path | Contents |
|---|---|
| @azlib/logger | Core logger, transports, and formats |
| @azlib/logger/adapters/sqlite | Optional SQLite DatabaseAdapter |
| @azlib/logger/dashboard | In-process HTTP dashboard transport |
API Reference
createLogger(options: LoggerOptions): Logger
const logger = createLogger({
level: "info", // Minimum log level
transports: [...], // One or more transports
format: jsonFormat(), // Optional global format
bindings: { service: "api" }, // Root-level context fields
buffer: {
maxSize: 1024, // Ring buffer capacity (records), default 1024
overflow: "drop-oldest", // "drop" | "drop-oldest" | "throw"
},
});Logger methods: trace, debug, info, warn, error, fatal, child, flush, close.
Child loggers
const reqLogger = logger.child({ requestId: "abc-123" });
reqLogger.info("Handling request"); // includes requestId in every recordBindings are serialised once at child-creation time — no per-call allocation.
Transports
createConsoleTransport(options?)
createConsoleTransport({
splitStreams: true, // warn/error/fatal → stderr; others → stdout (default: true)
format: prettyFormat({ colors: true }), // per-transport format override
})createFileTransport(options)
createFileTransport({
filePath: "./logs/app.log", // parent directory created if missing
bufferSize: 4096, // write buffer bytes (default: 4096)
})createDatabaseTransport(options)
createDatabaseTransport({
adapter, // any DatabaseAdapter
batchSize: 100, // flush when this many records accumulate (default: 100)
flushInterval: 5000, // flush every N ms even if batchSize not reached (default: 5000)
})Formats
import { jsonFormat, prettyFormat, combineFormats } from "@azlib/logger";
jsonFormat() // newline-delimited JSON
prettyFormat({
colors: true, // auto-detected from TTY by default
showPid: false,
})
// Chain multiple transforms; null short-circuits the chain
combineFormats(redactSecrets, jsonFormat())Custom format:
const redactFormat = {
transform: (record) => ({ ...record, meta: { ...record.meta, password: undefined } }),
};SQLite adapter
import { createSqliteAdapter } from "@azlib/logger/adapters/sqlite";
import { createLogger, createDatabaseTransport } from "@azlib/logger";
const adapter = createSqliteAdapter({ filePath: "./logs.db" });
const logger = createLogger({
level: "info",
transports: [createDatabaseTransport({ adapter })],
});
// Query stored logs
const errors = await adapter.query({ minLevel: 50, limit: 100 });Dashboard
import { createDashboardTransport } from "@azlib/logger/dashboard";
const dashboard = createDashboardTransport({
config: { port: 8999 },
});
const logger = createLogger({
level: "debug",
transports: [dashboard],
});
// Visit http://127.0.0.1:8999/logs/ui in your browserThe dashboard binds to 127.0.0.1 only and is intended for local development.
Overflow Policy
When the ring buffer is full, the overflow option controls behaviour:
| Policy | Behaviour |
|---|---|
| drop | Silently discard the incoming record. Increments droppedCount. |
| drop-oldest | Evict the oldest buffered record to make room (default). |
| throw | Throw LogBufferFullError on the calling thread. |
Performance
logger.info()is a synchronous ring-buffer append — no format, no I/O.- Child bindings are JSON-serialised once at construction, never per-call.
- Drain runs via
process.nextTick, keeping the event loop responsive. - Benchmark: 100k
logger.info()calls in < 2s wall time on a standard laptop.
Custom transport
import type { Transport } from "@azlib/logger";
const myTransport: Transport = {
name: "my-transport",
options: {},
write(record) { /* sync or async */ },
flush: async () => { /* drain buffers */ },
close: async () => { /* shutdown */ },
};