@trebired/logger
v2.0.0
Published
Structured server and browser logger with JSONL storage, shared event semantics, custom weighted levels, redaction, retention, and local query helpers.
Maintainers
Readme
@trebired/logger
Structured logging for server and browser applications that want one event model, readable console output, and durable local logs without running a separate logging stack.
@trebired/logger writes JSONL logs into group-based folders on the server, supports optional partitioned storage, and now ships a framework-neutral browser runtime with an optional React adapter on top.
Install
Runtime support: Bun 1+ and Node.js 18+.
npm install @trebired/loggerimport { createLog } from "@trebired/logger";
const log = createLog({
dir: "/var/log/my-app",
console: true,
quiet: true,
});
log.info("app.start", "ready", { port: 3000 });
await log.flush();Browser entrypoints:
import { createBrowserLog } from "@trebired/logger/browser";
import { LogProvider, useLog } from "@trebired/logger/browser/react";Browser Runtime
Use @trebired/logger/browser when you want the same levels, metadata conventions, grouping rules, and scoped logger behavior in browser code:
import { createBrowserLog } from "@trebired/logger/browser";
const log = createBrowserLog({
group: "frontend.app",
metadata: {
deploymentId: "deploy-42",
requestId: "req-abc",
},
});
log.info("frontend boot");
await log.flush();The first browser release ships with console delivery built in. It also supports custom browser transports with in-memory batching so you can add fetch, beacon, websocket, or other delivery later without changing the logger API.
There is no built-in SSR bootstrap helper in this release. If you want browser correlation data such as requestId, sessionId, or deploymentId, pass it explicitly through metadata or per-log metadata.
React Adapter
@trebired/logger/browser/react is intentionally thin. It does not create a logger for you. It only helps wire an existing browser logger into React context:
import { createRoot } from "react-dom/client";
import { createBrowserLog } from "@trebired/logger/browser";
import { LogProvider, useLog } from "@trebired/logger/browser/react";
const log = createBrowserLog({
group: "frontend.app",
metadata: { deploymentId: "deploy-42" },
});
function SaveButton() {
const scopedLog = useLog("ui.save_button");
return <button onClick={() => scopedLog.info("clicked")}>Save</button>;
}
createRoot(document.getElementById("root")!).render(
<LogProvider log={log}>
<SaveButton />
</LogProvider>,
);LogErrorBoundary is also available from @trebired/logger/browser/react when you want render errors logged with the shared event shape.
Why This Logger
Most loggers either write to stdout and expect an external collector, or provide a very broad transport system. This package is intentionally opinionated around a simpler operational workflow:
- structured JSONL entries
- one directory tree per log group
- optional partition folders above group trees
- async queued file writes by default
- custom weighted log levels
- local querying by group, level, day, and hour
- built-in redaction for common sensitive fields
- retention and file-size rolling without a database
The storage layout is meant to stay human-browsable:
/var/log/my-app/
app/
start/
2026-05-03-13-0000-info.jsonl
billing/
invoice/
2026-05-03-13-0000-audit.jsonlEach line is a JSON object:
{"recorded_at":"2026-05-03T13:00:00.000Z","level":"info","group":"app.start","message":"ready","origin":{"source":"app","instance":null},"metadata":{"port":3000}}If you want an extra top-level separation layer, set partition and the logger writes one more folder layer. This is useful for deployments, releases, environments, sessions, tenants, workers, import batches, or any other caller-defined bucket:
/var/log/my-app/
blue-2026-05-16/
app/
start/
2026-05-03-13-0000-info.jsonlPartition names can now be built from a stable time prefix plus any caller-defined suffix:
import {
buildPartitionName,
buildTemporaryPartitionName,
createLog,
} from "@trebired/logger";
const staged = buildTemporaryPartitionName({
timeZone: "UTC",
suffix: "deployment-unknown",
});
const final = buildPartitionName({
timeZone: "UTC",
suffix: "deployment-42",
});
const log = createLog({
dir: "/var/log/my-app",
partition: staged,
temporaryPartition: true,
});
log.info("app.boot", "starting before final ownership is known");
await log.flush();
await log.promotePartition(final);If you already have a full custom partition string, pass it directly with partition, or normalize it first with sanitizePartitionName().
Partition Lifecycle
You can manage partitions either from a live logger or with standalone helpers:
import {
copyPartition,
createPartition,
deleteLogs,
listPartitions,
renamePartition,
} from "@trebired/logger";
await createPartition("/var/log/my-app", "2026-05-17-12-0000-staged", {
temporary: true,
});
console.log(await listPartitions("/var/log/my-app"));
await renamePartition("/var/log/my-app", {
from: "2026-05-17-12-0000-staged",
to: "2026-05-17-12-0000-final",
});
await copyPartition({
fromDir: "/var/log/my-app",
from: "2026-05-17-12-0000-final",
toDir: "/var/log/archive",
to: "2026-05-17-12-0000-final-copy",
});
await deleteLogs("/var/log/my-app", {
partition: "2026-05-17-12-0000-final",
groupKey: "jobs.queue",
level: "warn",
olderThanDays: 7,
});Core API
const log = createLog({
dir: "/var/log/my-app",
save: true,
console: true,
timeZone: "America/New_York",
source: "api",
});
log.debug("app.boot", "config loaded");
log.info("app.boot", "ready");
log.success("job.import", "finished", { rows: 1200 });
log.warn("http.request", "slow request", { took_ms: 842 });
log.fail("job.import", "failed validation");
log.error("app.runtime", "uncaught error");save defaults to true when dir is provided. If no dir is provided, the logger can still emit console output and live stream events.
If you log without passing a group, the logger always uses "default".
@trebired/logger runs on both Bun and Node.js. It may print one-time package notices for runtime-specific guidance or important future package messages. For example, when it detects Node.js, it recommends Bun for best startup and file I/O performance. Pass quiet: true to suppress package notices:
const log = createLog({
quiet: true,
});When quiet is not true, the package also prints a one-time startup greeting using the same console logger style as normal entries.
timeZone is a top-level logger option because it controls the actual local moment used for saved file names and console timestamps. It defaults to the host timezone, then falls back to America/New_York.
Console output is configurable. level and message are always shown; timestamp, group, and metadata can be hidden. console.locale only controls display formatting. When locale is omitted or invalid, the logger passes undefined to Intl.DateTimeFormat, so JavaScript uses the runtime or system default locale:
const log = createLog({
timeZone: "America/New_York",
console: {
colors: true,
timestamp: true,
group: false,
metadata: false,
locale: "en-US",
},
});Use timeZone for the local hour and console.locale for the display style.
European dot-date locales such as cs-CZ and de-DE are formatted consistently as 03.05.2026, 15:59:23.
Full API Example
import { createLog } from "@trebired/logger";
const log = createLog({
dir: "/var/log/my-app",
partition: "blue-2026-05-16",
temporaryPartition: false,
save: true,
console: {
enabled: true,
colors: true,
timestamp: true,
group: true,
metadata: true,
locale: "en-US",
},
quiet: true,
timeZone: "America/New_York",
source: "api",
levels: {
debug: { weight: 10, label: "DEBUG", color: "#94a3b8" },
info: { weight: 20, label: "INFO", color: "#38bdf8" },
success: { weight: 25, label: "SUCCESS", color: "#22c55e", bold: true },
warn: { weight: 30, label: "WARN", color: "#f59e0b", stream: "stderr" },
fail: { weight: 40, label: "FAIL", color: "#fb7185", stream: "stderr" },
error: { weight: 50, label: "ERROR", color: "#ef4444", stream: "stderr", showStack: true, bold: true },
audit: { weight: 35, label: "AUDIT", color: "#8b5cf6" },
panic: { weight: 100, label: "PANIC", color: "#dc2626", stream: "stderr", showStack: true, bold: true },
},
minLevel: "debug",
write: {
mode: "async",
maxQueue: 10000,
overflow: "drop-newest",
},
retention: {
enabled: true,
maxFileSize: "20mb",
compressOldFiles: false,
cleanupIntervalMs: 60_000,
// deletion is opt-in:
maxAgeDays: 30,
maxPartitions: 5,
},
redact: {
includeDefaultSensitiveKeys: true,
paths: ["user.password", /^headers\.authorization$/i],
replacement: "[REDACTED]",
},
serializers: {
userId: (value) => `user:${String(value)}`,
error: (value) =>
value instanceof Error
? { name: value.name, message: value.message, stack: value.stack }
: value,
},
sample: (entry) => entry.level !== "debug" || Math.random() < 0.1,
request: {
group: "http.request",
idHeader: "x-request-id",
attach: true,
},
});
log.info("app.start", "ready", { port: 3000 });
log.audit("billing.invoice", "created", { invoiceId: "inv_123" });
log.error("app.start", "failed", { reason: "missing config" });
await log.flush();
await log.close();Custom Levels
Levels are weighted. minLevel filters out entries with lower weight.
const log = createLog({
levels: {
audit: { weight: 35, label: "AUDIT", color: "#8b5cf6" },
panic: { weight: 100, label: "PANIC", stream: "stderr", bold: true },
},
minLevel: "audit",
});
log.audit("billing.invoice", "created", { invoiceId: "inv_123" });
log.panic("runtime", "unrecoverable");Built-in levels are debug, info, success, warn, fail, and error. A custom level with the same name overrides the built-in config.
Custom level methods are available at runtime. In TypeScript, the logger exposes dynamic level methods through an index signature, so custom names work but are not exhaustively autocompleted from the levels object yet.
Async Writes, Flush, and Close
File writes are async queued by default.
const log = createLog({
dir: "/var/log/my-app",
write: {
mode: "async",
maxQueue: 10000,
overflow: "drop-newest",
},
});
log.info("queue.example", "queued");
await log.flush();
await log.close();getStats() exposes write health:
const stats = log.getStats();
// { mode, queued, written, dropped, failed, queueLength, closed }For simple scripts or tests, sync writing is available:
const log = createLog({
dir: "./logs",
write: { mode: "sync" },
});Retention and Rolling
Defaults:
- logs are kept forever unless you set a retention number
maxFileSize: "20mb"compressOldFiles: false
const log = createLog({
dir: "/var/log/my-app",
retention: {
// only delete when you set one or both of these:
maxAgeDays: 30,
maxPartitions: 5,
maxFileSize: "100mb",
compressOldFiles: true,
cleanupIntervalMs: 60 * 60 * 1000,
},
});maxAgeDays deletes old files by age. maxPartitions keeps only the newest partition folders by last write time. A partition can represent deployments, sessions, environments, release versions, or any other caller-defined grouping. If you omit both, the logger stores logs indefinitely.
When a file exceeds the configured size, the logger rolls to the next sequence inside the same group and hour:
2026-05-03-13-0000-info.jsonl
2026-05-03-13-0001-info.jsonl
2026-05-03-13-0002-info.jsonlRedaction and Serializers
Common sensitive keys are redacted by default: password, token, secret, authorization, cookie, api_key, and related variants.
const log = createLog({
redact: {
paths: ["user.ssn", /^payment\./],
replacement: "[hidden]",
},
serializers: {
userId: (value) => `user:${value}`,
},
});
log.info("account.update", "saved", {
userId: 42,
password: "secret",
user: { ssn: "123-45-6789" },
});Scoped Loggers
const jobs = log.group("jobs.queue");
jobs.info("started", { jobId: "job_1" });
const worker = log.withScope("worker", "jobs.queue", 2);
worker.error("failed", { jobId: "job_1" });Express-Style Middleware
requestLogger() is compatible with Express-style middleware and attaches req.log by default.
app.use(log.requestLogger({
group: "http.request",
idHeader: "x-request-id",
}));
app.get("/", (req, res) => {
req.log.info("handled");
res.end("ok");
});Query Saved Logs
Old query names are not supported anymore. Use getLogsForDir(), getAllLogs(), and getAllLogsAcrossPartitions(). Do not use getEntriesForDir() or getAll().
import { getLogsForDir } from "@trebired/logger";
const result = await getLogsForDir("/var/log/my-app", {
level: "error",
groupKey: "app.runtime",
limit: 100,
});
console.log(result.logs);
console.log(result.levels.error.color);
console.log(result.metadata.count);
console.log(result.metadata.total);The main instance query method is getAllLogs():
const recent = await log.getAllLogs({ groupKey: "billing.invoice", limit: 50 });
console.log(recent.logs);
console.log(recent.levels);
console.log(recent.metadata.total);Pass partition: null when you want only unpartitioned logs from a mixed directory tree.
If you use partition folders and want a merged read across every partition:
const merged = await log.getAllLogsAcrossPartitions({
groupKey: "billing.invoice",
limit: 100,
});
console.log(merged.logs);
console.log(merged.metadata.partitions.items);Sampling
const log = createLog({
sample: 0.1,
});
const selective = createLog({
sample: (entry) => entry.level === "error" || entry.group.startsWith("audit."),
});Live Stream
import { logStream } from "@trebired/logger";
logStream.on("log", (entry, context) => {
// context.dir is the active log directory, if one is configured
});Development
bun install
bun run demo
bun test
bun run typecheck
bun run buildbun run demo starts a small dummy system that keeps logging until interrupted. It exercises grouped and scoped loggers, custom levels, redaction, request middleware, live stream events, local querying, and write stats. It writes throwaway logs under the OS temp directory, such as /tmp/@trebired-logger/dummy on Linux and macOS. Microslop Windows is not supported.
The npm package exports compiled files from dist. Publishing runs typecheck, tests, and build through prepublishOnly.
