# LoggerJS Full LLM Context > Expanded LoggerJS documentation context generated from repository Markdown sources. # Repository README Source: https://github.com/jskits/loggerjs/blob/main/README.md
LoggerJS logo # LoggerJS **A faster, more powerful isomorphic logger. Collect, process, deliver — one fast pipeline.** [![CI](https://github.com/jskits/loggerjs/actions/workflows/ci.yml/badge.svg)](https://github.com/jskits/loggerjs/actions/workflows/ci.yml) [![npm](https://img.shields.io/npm/v/@loggerjs/core.svg)](https://www.npmjs.com/package/@loggerjs/core) [![license](https://img.shields.io/npm/l/@loggerjs/core)](LICENSE) [![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](tsconfig.base.json) [![core dependencies](https://img.shields.io/badge/core_deps-0-44CC11)](packages/core/package.json) [![Node runtime](https://img.shields.io/badge/runtime_Node-%E2%89%A520.19-339933?logo=node.js&logoColor=white)](.github/workflows/ci.yml) [![modules](https://img.shields.io/badge/modules-ESM%20%2B%20CJS-F7DF1E)](#packages) [Getting Started](docs/GETTING-STARTED.md) · [Concepts](docs/CONCEPTS.md) · [Transports](docs/TRANSPORTS.md) · [Pretty](docs/PRETTY.md) · [Integrations](docs/INTEGRATIONS.md) · [Benchmarks](docs/BENCHMARKS.md) · [Comparison](docs/COMPARISON.md) · [AI Skill](docs/AI-SKILL.md) · [Architecture](docs/ARCHITECTURE.md) **13 packages** · **35 integrations** (**19 browser / 16 Node.js**) · **25+ transports** (core / browser / Node.js / pretty / vendor) · **27 runtime-neutral processors** · **8 codecs** · **zero-dependency core**
--- LoggerJS is a monorepo of logging packages around a dependency-free, platform-neutral core. The same logger code runs in Node, browsers, workers, and edge runtimes. It is organized around three user-facing concepts plus one boundary rule: - **Integrations** collect logs automatically from platform behavior — browser console calls, script errors, fetch/XHR failures, Web Vitals, route changes, Node process crashes, HTTP servers, serverless handlers, queue and database clients. All opt-in. - **Middleware / processors** synchronously enrich, redact, sample, dedupe, rate-limit, fingerprint, buffer (fingers-crossed), route, and tag logs before delivery. Middleware run on raw records; processors run on projected events. - **Transports** deliver logs anywhere — console, pretty DevTools/terminal output, stdout, files, HTTP, IndexedDB, WebSocket, service workers, worker threads, OTLP, Sentry, Datadog, Elasticsearch, Loki, CloudWatch, SQL databases — with reusable batching, retry, backoff, and circuit-breaker wrappers where the destination needs them. - **Codecs belong to transports.** The pipeline keeps values raw; each destination owns its serialization. Built-in codecs are fast by default and never lose a log to an encoding error.
🌐 **Truly isomorphic**
Zero-dependency core, zero platform APIs, a type surface that compiles without DOM libs. One logger for Node, browsers, workers, and edge.
🎣 **Automatic collection, first-class**
35 opt-in integrations — 19 browser/frontend and 16 Node.js/server — turn platform behavior into structured logs.
⚡ **Performance with receipts**
Disabled levels cost ~3ns (pino parity). On the M1 Max reference machine, lean NDJSON runs at ~1.19× pino throughput and the prepared encoder at ~1.28× — faster than pino for equivalent output — [measured](docs/BENCHMARKS.md) with a drift-canceling A/B harness, checked into the [benchmark matrix](docs/BENCHMARK-MATRIX.md), and CI-gated. Ranking vs pino is CPU/V8-dependent.
🛟 **Logs survive bad days**
Crash-path `flushSync`, beacon on page close, offline replay, batch retry with circuit breakers, codecs that fall back instead of throwing.
🧩 **Composable pipeline**
27 runtime-neutral middleware and processors enrich, redact, sample, dedupe, rate-limit, fingerprint, route, and buffer — on raw records or projected events.
📚 **Library-author friendly**
`getLogger(["my-lib"])` is a silent no-op until the host app calls `configure()` — log from libraries without forcing a dependency on users.
## Table of Contents - [Install](#install) - [AI Skill](#ai-skill) - [Quick Start](#quick-start) - [Node](#node) - [Browser](#browser) - [Library authors](#library-authors) - [How It Works](#how-it-works) - [Typed Events](#typed-events) - [Context Propagation](#context-propagation) - [Performance](#performance) - [Packages](#packages) - [The Ecosystem](#the-ecosystem) - [How It Compares](#how-it-compares) - [Documentation](#documentation) - [Development](#development) - [License](#license) ## Install Pick the package for your platform. Each platform package **re-exports all of `@loggerjs/core`**, so one install per app is enough to start. ```bash # Node services npm install @loggerjs/node @loggerjs/processors # Browser apps npm install @loggerjs/browser @loggerjs/processors ``` Using pnpm or yarn? Swap `npm install` for `pnpm add` / `yarn add`. Add vendor packages (`@loggerjs/otel`, `@loggerjs/sentry`, `@loggerjs/datadog`, …) only when you deliver to that destination. All packages ship **ESM + CJS** with full TypeScript declarations and granular subpath exports (`@loggerjs/node/transport-stdout`, `@loggerjs/browser/integration-console`, …) so bundlers tree-shake to exactly what you import. ## AI Skill LoggerJS includes an installable AI skill for coding agents. Use it when you want an agent to add LoggerJS to a JavaScript or TypeScript app, choose the right runtime packages, configure production logging, or migrate from console, pino, winston, loglevel, debug, or an existing wrapper. ```bash # Install the LoggerJS skill npx skills add jskits/loggerjs --skill loggerjs # Install it for a specific agent npx skills add jskits/loggerjs --skill loggerjs --agent codex npx skills add jskits/loggerjs --skill loggerjs --agent claude-code # Use it once without installing npx skills use jskits/loggerjs --skill loggerjs ``` Then ask your agent: ```text Use $loggerjs to add production-ready structured logging to this Node API. ``` The skill lives in [`skills/loggerjs`](skills/loggerjs), and the full usage guide is in [docs/AI-SKILL.md](docs/AI-SKILL.md). ## Quick Start ### Node ```ts import { captureProcessIntegration, createLogger, stdoutTransport, } from "@loggerjs/node"; import { redactProcessor } from "@loggerjs/processors"; const logger = createLogger({ category: ["api"], level: "info", tags: { service: "checkout", env: process.env.NODE_ENV ?? "dev" }, processors: [redactProcessor({ keys: ["password", /token/i] })], transports: [stdoutTransport()], integrations: [captureProcessIntegration()], }); logger.info("order created", { orderId: "ord_123" }); logger.error(new Error("card declined"), "payment failed", { orderId: "ord_123", }); await logger.flush(); ``` `stdoutTransport()` writes one NDJSON line per log; `captureProcessIntegration()` turns uncaught exceptions, unhandled rejections, and process warnings into structured events automatically. ### Browser ```ts import { browserHttpTransport, captureBrowserErrorsIntegration, captureConsoleIntegration, captureFetchIntegration, createLogger, memoryBrowserHttpOfflineQueue, pageLifecycleIntegration, } from "@loggerjs/browser"; import { redactProcessor } from "@loggerjs/processors"; const logger = createLogger({ category: ["web"], level: "info", processors: [redactProcessor()], transports: [ browserHttpTransport({ url: "/api/logs", offlineQueue: memoryBrowserHttpOfflineQueue({ maxEntries: 500 }), useBeaconOnPageHide: true, }), ], integrations: [ captureConsoleIntegration({ levels: ["warn", "error"] }), captureBrowserErrorsIntegration(), captureFetchIntegration(), pageLifecycleIntegration(), ], }); logger.info("page loaded"); ``` Logs batch over HTTP, queue while offline, replay with backoff when the network returns, and attempt a best-effort `sendBeacon` flush when the tab closes. ### Library authors ```ts // inside your library — silent until the app configures it import { getLogger } from "@loggerjs/core"; const logger = getLogger(["my-lib", "client"]); logger.debug("handshake started"); // no-op until configure() runs // inside the application import { configure } from "@loggerjs/core"; import { stdoutTransport } from "@loggerjs/node"; await configure({ transports: { stdout: stdoutTransport() }, loggers: [{ category: ["my-lib"], level: "warn", transports: ["stdout"] }], }); ``` ## How It Works Every log flows through one pipeline. The hot path is engineered to do as little as possible until a value is actually needed. ``` logger.info("order created", { orderId }) │ ▼ level gate ──── disabled levels stop here (~5ns, no allocation) │ ┌─────────────┐ lazy message · raw error · shared ctx/tags · no id yet │ LogRecord │ └──────┬──────┘ │ middleware ───────── sync · ordered · enrich / redact / drop on raw records │ any processors? ──no──▶ record fast path — straight to transports, no projection │ yes ▼ ┌─────────────┐ id assigned · message resolved · error normalized │ LogEvent │ └──────┬──────┘ │ processors ───────── sample · dedupe · fingerprint · route · fingers-crossed │ ▼ transports ───────── console · stdout · file · http · indexeddb · otlp · sentry · … │ shared batching · retry · backoff · circuit breaker ▼ codec ────────────── each destination owns its serialization (fast, never throws) ``` `LogRecord` is the hot-path shape — it keeps the message function unevaluated, the error raw, and context/tags shared by reference. A logger with **zero processors** sends records straight to transports (the _record fast path_, no event projection). Adding any processor opts that logger into `LogEvent` projection so processors can see the resolved shape. See [CONCEPTS.md](docs/CONCEPTS.md) for the full model. ## Typed Events Define an event once and log it with a checked payload — message and tags derive from the data. ```ts import { defineEvent } from "@loggerjs/core"; const CheckoutCompleted = defineEvent<{ orderId: string; amountCents: number }>( { type: "checkout.completed", message: (event) => `checkout completed ${event.orderId}`, tags: { domain: "checkout" }, }, ); logger.event(CheckoutCompleted, { orderId: "ord_123", amountCents: 4999 }); ``` ## Context Propagation Bind fields once and have them follow async execution across every `await` — no manual threading. ```ts import { withContext } from "@loggerjs/core"; import { installAsyncLocalStorageContext } from "@loggerjs/node"; installAsyncLocalStorageContext(); // once at startup await withContext({ requestId: "req_123" }, async () => { logger.info("request started"); // carries { requestId } across awaits }); ``` ## Performance Reference machine: Apple M1 Max (64 GB), Node v22.21.1, against pino 10.3.1 / winston 3.19.0 / LogTape 2.1.3. The loggerjs-vs-pino rows use the drift-canceling paired A/B harness (`BENCH_AB`, 22 runs); competitor rows are the sequential suite. Full methodology and the regression gate live in [docs/BENCHMARKS.md](docs/BENCHMARKS.md), with checked-in machine evidence in [docs/BENCHMARK-MATRIX.md](docs/BENCHMARK-MATRIX.md). | Logger / path | ns/op | Relative | | -------------------------------------------------- | ------: | ------------------------------- | | **loggerjs** — disabled level (lazy message) | **3** | parity with pino (9) | | **loggerjs** — prepared lean NDJSON | **224** | **1.28× pino** (faster) | | **loggerjs** — lean NDJSON, comparable line | **242** | **1.19× pino** (faster) | | pino — NDJSON noop sink | 287 | 1.00× baseline | | **loggerjs** — full envelope (`+id/seq/levelName`) | **307** | ~0.9× pino, 3 extra fields/line | | **loggerjs** — batch transport enqueue | **172** | — | | Node `console` — noop stream | 769 | loggerjs ~3× faster | | winston — JSON noop sink | 2,726 | loggerjs ~11× faster | | LogTape — JSON lines noop sink | 6,584 | loggerjs ~27× faster | The hot path is deliberate: level gating before any allocation, lazy message resolution, frozen shared tags, memoized ids, a record fast path that skips event projection, and fragment-cached serialization — all guarded by `pnpm bench:gate` in CI. On the M1 Max reference, loggerjs lean and prepared edge out pino in paired A/B runs, but the ranking is **CPU/V8-dependent**; reproduce it on your own machine with `BENCH_AB=1 pnpm bench:node` and add broader evidence through the benchmark matrix. LoggerJS keeps one record per log so middleware, integrations, and multiple transports can observe it, and reaches pino's class **without** giving that pipeline up — see the [architecture note](docs/ARCHITECTURE.md). ## Packages | Package | Contents | | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | [`@loggerjs/core`](packages/core) | Logger, record/event model, registry, context, middleware kernel, integration API, console/memory/test/batch transports, json/safe-json/ndjson codecs. **Zero dependencies.** | | [`@loggerjs/browser`](packages/browser) | HTTP / IndexedDB / WebSocket / service-worker / broadcast-channel transports, offline queues, ZIP export, **19 browser integrations** | | [`@loggerjs/node`](packages/node) | stdout / stderr / file / rotating-file / HTTP / syslog / worker transports, AsyncLocalStorage context, **16 Node integrations** | | [`@loggerjs/pretty`](packages/pretty) | Browser DevTools and Node terminal pretty output: styled console transport, ANSI stdout/stderr transports, and shared formatter | | [`@loggerjs/processors`](packages/processors) | redact, privacy-guard, sample, dynamic-sampler, rate-limit, dedupe, fingerprint, filter, route, level-override, normalize-error, stack-parser, enrich, tags, trace, fingers-crossed, breadcrumbs, schema-dev-check | | [`@loggerjs/codecs`](packages/codecs) | fast-event-json (the performance codec), msgpackr, projector | | [`@loggerjs/otel`](packages/otel) | OTLP JSON mapping, OTLP/HTTP transport, OpenTelemetry log bridge, active-span trace processor | | [`@loggerjs/sentry`](packages/sentry) | Sentry structured logs, breadcrumbs, exception/message capture | | [`@loggerjs/datadog`](packages/datadog) | Datadog Logs intake transport | | [`@loggerjs/elastic`](packages/elastic) | Elasticsearch bulk API transport | | [`@loggerjs/loki`](packages/loki) | Grafana Loki push transport | | [`@loggerjs/cloudwatch`](packages/cloudwatch) | CloudWatch Logs transport with built-in SigV4 signing | | [`@loggerjs/database`](packages/database) | SQLite / Postgres / custom-adapter batch transports | Vendor HTTP transports speak wire protocols directly; SDK/provider adapters use the SDK object or provider your app already owns. **No vendor SDKs are bundled.** ## The Ecosystem
Runtime support at a glance — what runs in browser, Node.js, or both
| Capability family | Browser / frontend | Node.js / server | Runtime-neutral | | ----------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | Integrations | 19 first-party browser collectors | 16 first-party Node.js collectors | Core exposes the integration API; automatic capture lives in platform packages. | | Transports | HTTP, IndexedDB, WebSocket, service worker, BroadcastChannel, offline-first, pretty DevTools console | stdout/stderr, files, rotation, HTTP, syslog, worker threads, database-backed transports, pretty terminal stdout/stderr | console, memory, test, batch/retry/fallback wrappers; vendor HTTP transports can run where their credentials and fetch/runtime requirements are safe. | | Processors / middleware | All 27 supported | All 27 supported | `@loggerjs/processors` has no browser or Node.js platform dependency; only routed transport targets are runtime-specific. | See [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md), [docs/TRANSPORTS.md](docs/TRANSPORTS.md), and [docs/PROCESSORS.md](docs/PROCESSORS.md) for the full support notes.
25+ transports — core/browser/Node.js/pretty/vendor destinations plus reusable reliability wrappers
**Core / runtime-neutral** (`@loggerjs/core`) — `consoleTransport` · `memoryTransport` · `testTransport`, plus reliability wrappers `batchTransport` · `retryTransport` · `fallbackTransport` **Pretty developer UX** (`@loggerjs/pretty`) — `prettyConsoleTransport` for browser DevTools or local consoles · `prettyStdoutTransport` / `prettyStderrTransport` for Node terminals · `formatPrettyEvent` for custom display sinks **Node.js / server** (`@loggerjs/node`) — `stdoutTransport` · `stderrTransport` · `fileTransport` · `rotatingFileTransport` · `nodeHttpTransport` · `nodeSyslogTransport` · `workerTransport` **Browser / frontend** (`@loggerjs/browser`) — `browserHttpTransport` · `indexedDbTransport` · `browserWebSocketTransport` · `browserServiceWorkerTransport` · `browserBroadcastChannelTransport` · `offlineFirstTransport` For frontend support bundles, `indexedDbTransport()` is session-aware by default and can add a bounded `localStorage` spill for the async IndexedDB write tail: ```ts import { createLogger, downloadBlob, exportLogsToZip, indexedDbTransport } from "@loggerjs/browser"; import { privacyGuardProcessor, redactProcessor } from "@loggerjs/processors"; const supportStore = indexedDbTransport({ dbName: "my-app-support-logs", localStorageSpill: { namespace: "my-app-support-logs" }, }); const logger = createLogger({ category: ["web"], processors: [redactProcessor(), privacyGuardProcessor()], transports: [supportStore], }); export async function downloadSupportLogs() { await logger.flush(); downloadBlob( await exportLogsToZip(supportStore, { groupBySession: true, includeRecent: true, query: { order: "asc" }, }), "support-logs.zip", ); } ``` **Observability & vendors** — `otlpHttpTransport` · `openTelemetryLogBridgeTransport` · `sentryTransport` · `datadogLogsTransport` · `elasticTransport` · `lokiTransport` · `cloudWatchLogsTransport`. HTTP wire transports depend on `fetch`/crypto and credential placement; SDK/provider adapters use the SDK object or provider your app already owns. **Databases / local app / backend** — `databaseTransport` · `postgresTransport` · `sqliteTransport`. These require application-provided database drivers and are intended for Node.js, Electron, CLIs, or backend workers. See [docs/TRANSPORTS.md](docs/TRANSPORTS.md) for options and how to write your own.
35 integrations — 19 browser/frontend + 16 Node.js/server automatic collectors
**Browser** (19) — _Console & errors:_ `captureConsoleIntegration` · `captureBrowserErrorsIntegration` · `captureFrameworkErrorsIntegration` · `captureReportingIntegration` _Network:_ `captureFetchIntegration` · `captureXHRIntegration` · `captureWebSocketIntegration` _Performance:_ `captureWebVitalsIntegration` · `capturePerformanceIntegration` _Navigation:_ `captureRouterIntegration` · `nextRouterIntegration` · `reactRouterIntegration` · `vueRouterIntegration` · `nuxtRouterIntegration` _Lifecycle & context:_ `pageLifecycleIntegration` · `captureUserActionsIntegration` · `captureServiceWorkerIntegration` · `captureRuntimeHostIntegration` · `browserContextPropagationIntegration` **Node** (16) — _Process & runtime:_ `captureProcessIntegration` · `captureCliIntegration` · `diagnosticsChannelIntegration` · `serverlessIntegration` _HTTP frameworks:_ `expressIntegration` · `fastifyIntegration` · `koaIntegration` · `hapiIntegration` · `nestMiddlewareIntegration` _Clients:_ `nodeFetchIntegration` · `nodeHttpClientIntegration` · `redisIntegration` · `prismaIntegration` · `databaseIntegration` _Queues:_ `queueIntegration` · `bullMqIntegration` Every integration uses re-entrancy guards and an unpatched-original registry so capture never loops. See [docs/INTEGRATIONS.md](docs/INTEGRATIONS.md) for the integration API and how to write your own.
27 processors & middleware — runtime-neutral, synchronous, error-isolated, composable
_Redaction & privacy:_ `redactProcessor` · `privacyGuardProcessor` _Sampling & volume:_ `sampleProcessor` · `dynamicSamplerProcessor` · `rateLimitProcessor` · `dedupeProcessor` · `coalesceProcessor` _Enrichment & tagging:_ `enrichProcessor` / `enrichMiddleware` · `tagsProcessor` / `tagsMiddleware` · `typeProcessor` / `typeMiddleware` · `contextProcessor` / `contextMiddleware` · `traceContextProcessor` / `traceContextMiddleware` _Errors:_ `normalizeErrorProcessor` · `fingerprintProcessor` · `stackParserProcessor` · `symbolicateStackProcessor` _Routing & control:_ `routeProcessor` · `filterProcessor` · `levelOverrideProcessor` _Buffering:_ `fingersCrossedProcessor` · `breadcrumbBufferProcessor` _Development:_ `schemaDevCheckProcessor` Middleware run on raw records before id/message/error work; processors run on projected events. The processor package is platform-neutral and works in browser, Node.js, workers, and edge runtimes; only route/fingers-crossed targets depend on transports available in that runtime. See [docs/PROCESSORS.md](docs/PROCESSORS.md) for ordering guidance.
8 codecs — serialization owned by the transport, fast by default, never throws
`jsonCodec` · `safeJsonCodec` · `ndjsonCodec` · `metricsCodec` (core) — `fastEventJsonCodec` (the performance codec) · `pinoCompatCodec` · `msgpackrCodec` · `projectorCodec` (`@loggerjs/codecs`). Codecs fall back to a safe representation on circular references instead of throwing, and increment a `codec.fallback` meta counter so silent degradation is observable. See [docs/CODECS.md](docs/CODECS.md).
## How It Compares LoggerJS shines when the logging problem spans **browser and server** collection from one mental model — and when you want logs delivered to **your own** destinations (HTTP, files, your DB, Loki/Elasticsearch, OTLP) rather than a single vendor's SaaS, from a zero-dependency core that runs under strict CSP, on edge/Workers, and offline. A fair, repo-sourced snapshot (full matrix and sources in [docs/COMPARISON.md](docs/COMPARISON.md)): | Capability | LoggerJS | Pino | Winston | LogTape | | -------------------------------------- | :---------------------: | :--: | :-----: | :-----: | | Isomorphic (browser + Node, one API) | ✅ | ⚠️ | ⚠️ | ✅ | | Automatic collection (integrations) | ✅ 19 browser / 16 Node | ❌ | ⚠️ | ⚠️ | | Built-in batching / retry / offline | ✅ | ⚠️ | ⚠️ | ⚠️ | | Transport-owned codecs | ✅ | ⚠️ | ⚠️ | ⚠️ | | Library-safe (silent until configured) | ✅ | ⚠️ | ⚠️ | ✅ | | Direct Node JSON throughput | ✅ 1.19× pino | ✅ | slower | slower | On the direct Node JSON path loggerjs and pino are in the same class — on the M1 Max reference loggerjs lean is ~1.19× pino, while on other CPUs pino can lead (it's CPU/V8-dependent; reproduce with `BENCH_AB`; see the checked-in [benchmark matrix](docs/BENCHMARK-MATRIX.md)). LoggerJS reaches that throughput while adding a record pipeline that works the same in the browser, captures automatically, and delivers reliably. ## Documentation | Doc | Contents | | ------------------------------------------------ | ------------------------------------------------------------------------------- | | [Getting Started](docs/GETTING-STARTED.md) | Install, first loggers, levels, context, typed events, registry | | [Concepts](docs/CONCEPTS.md) | The pipeline model: records, events, middleware, processors, transports, codecs | | [Transports](docs/TRANSPORTS.md) | Every built-in transport, batch reliability options, writing your own | | [Pretty Output](docs/PRETTY.md) | Browser DevTools and Node terminal pretty output UX | | [Integrations](docs/INTEGRATIONS.md) | All integrations, the integration API, writing your own | | [Processors](docs/PROCESSORS.md) | The middleware/processor toolbox and ordering guidance | | [Codecs](docs/CODECS.md) | Serialization ownership, fast-by-default semantics, custom codecs | | [Performance](docs/PERFORMANCE.md) | Tuning guide: fast path, codec choice, batching | | [Operations](docs/OPERATIONS.md) | Privacy defaults, offline queues, crash paths, delivery reliability | | [Production Recipes](docs/PRODUCTION-RECIPES.md) | Browser HTTP/offline, Node stdout+OTLP, Loki/Datadog deployments | | [API Stability](docs/API-STABILITY.md) | v1 stable API subset and pre-1.0 compatibility policy | | [Benchmarks](docs/BENCHMARKS.md) | Methodology, measured snapshot, regression gate, size budgets | | [Comparison](docs/COMPARISON.md) | How LoggerJS compares with Pino, Winston, LogTape, Bunyan, and lighter tools | | [Migration](docs/MIGRATION.md) | Coming from pino, winston, or console.log | | [AI Skill](docs/AI-SKILL.md) | Install and use the LoggerJS AI skill with coding agents | | [Architecture](docs/ARCHITECTURE.md) | The full design document and recorded decisions | | [Contributing](docs/CONTRIBUTING.md) | Repo workflow, CI gates, engineering conventions | | [Release](docs/RELEASE.md) | Versioning and publish workflow | Runnable examples live in [`examples/`](examples): [Node basics](examples/node-basic), [browser basics](examples/browser-basic), [OpenTelemetry](examples/otel-basic), [Sentry](examples/sentry-basic). ## Development Use **Node >=22.13** for repository development and the full `pnpm check` gate. Published packages are smoke-tested as packed consumers on **Node 20.19, 22, and 24**; Node 20.19 is the runtime compatibility floor, not the repo toolchain floor. ```bash pnpm install pnpm check # format, lint, typecheck, test, build, size budgets, API reports, pack checks pnpm bench # node + browser benchmarks pnpm bench:gate # performance regression gate (also runs in CI) ``` See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for conventions and the rules CI enforces. ## License [MIT](LICENSE) © JS Kits --- # Getting Started Source: https://github.com/jskits/loggerjs/blob/main/docs/GETTING-STARTED.md # Getting Started LoggerJS is an isomorphic structured logging SDK. The same core API runs in Node, browsers, workers, and edge runtimes; platform packages add transports and automatic collection on top. ## Install Pick the package for your platform. Each platform package re-exports everything from `@loggerjs/core`, so one install is enough to start. ```bash # Node services pnpm add @loggerjs/node @loggerjs/processors # Browser apps pnpm add @loggerjs/browser @loggerjs/processors ``` All packages ship ESM and CJS entry points with full TypeScript declarations. For Node consumers, packed packages are smoke-tested on Node 20.19.0, 22, and 24. Repository development uses Node >=22.13.0 for the full toolchain. ## First Logger (Node) ```ts import { captureProcessIntegration, createLogger, stdoutTransport } from "@loggerjs/node"; import { redactProcessor } from "@loggerjs/processors"; const logger = createLogger({ category: ["api"], level: "info", tags: { service: "checkout", env: process.env.NODE_ENV ?? "dev" }, processors: [redactProcessor()], transports: [stdoutTransport()], integrations: [captureProcessIntegration()], }); logger.info("order created", { orderId: "ord_123" }); logger.error("payment failed", new Error("card declined")); await logger.flush(); ``` `stdoutTransport()` writes one NDJSON line per log. `captureProcessIntegration()` turns uncaught exceptions, unhandled rejections, and process warnings into log events automatically. ## First Logger (Browser) ```ts import { browserHttpTransport, captureBrowserErrorsIntegration, captureConsoleIntegration, createLogger, memoryBrowserHttpOfflineQueue, pageLifecycleIntegration, } from "@loggerjs/browser"; const logger = createLogger({ category: ["web"], level: "info", transports: [ browserHttpTransport({ url: "/api/logs", offlineQueue: memoryBrowserHttpOfflineQueue({ maxEntries: 500 }), useBeaconOnPageHide: true, }), ], integrations: [ captureConsoleIntegration({ levels: ["warn", "error"] }), captureBrowserErrorsIntegration(), pageLifecycleIntegration(), ], }); logger.info("page loaded"); ``` The HTTP transport batches logs, queues them while offline, replays on `online`, and falls back to `navigator.sendBeacon` when the page is closing. ## Levels Six enabled levels plus `silent`: | Name | Value | | --- | ---: | | `trace` | 10 | | `debug` | 20 | | `info` | 30 | | `warn` | 40 | | `error` | 50 | | `fatal` | 60 | ```ts logger.setLevel("debug"); logger.isLevelEnabled("trace"); // false ``` Disabled levels cost one numeric comparison — no allocation, no context lookup, no message formatting. ## Lazy Messages Pass a function when building the message is expensive. It is only called if the level is enabled, and at most once: ```ts logger.debug(() => `cart state: ${JSON.stringify(cart)}`); ``` ## Errors An `Error` as the first argument becomes the record's error, with an optional explicit message: ```ts logger.error(err); logger.error(err, "payment failed", { orderId: "ord_123" }); ``` Errors are normalized (name, message, truncated stack, enumerable properties, cause chain) before transports see them. ## Child Loggers and Tags ```ts const checkoutLogger = logger.child({ category: ["api", "checkout"], tags: { domain: "checkout" }, }); ``` Children inherit level, tags, bindings, middleware, processors, and transports; integrations are not inherited. `withTags()` and `withType()` are shorthands for common child shapes. ## Ambient Context Bind request-scoped values once instead of threading them through every call: ```ts import { withContext } from "@loggerjs/core"; import { installAsyncLocalStorageContext } from "@loggerjs/node"; installAsyncLocalStorageContext(); // once at startup await withContext({ requestId: "req_123" }, async () => { logger.info("request started"); // context: { requestId: "req_123" } }); ``` In the browser the default stack-based context manager covers synchronous scopes; in Node, AsyncLocalStorage carries context across `await` boundaries. ## Typed Events Define reusable, typed event shapes: ```ts import { defineEvent } from "@loggerjs/core"; const CheckoutCompleted = defineEvent<{ orderId: string; amountCents: number }>({ type: "checkout.completed", message: (event) => `checkout completed ${event.orderId}`, tags: { domain: "checkout" }, }); logger.event(CheckoutCompleted, { orderId: "ord_123", amountCents: 4999 }); ``` ## Library Authors: the Registry Libraries should not construct loggers; they look one up by category and stay silent until the host application configures output: ```ts // In the library import { getLogger } from "@loggerjs/core"; const logger = getLogger(["my-lib", "client"]); logger.debug("handshake started"); // no-op until configured // In the application import { configure } from "@loggerjs/core"; await configure({ transports: { stdout: stdoutTransport() }, loggers: [{ category: ["my-lib"], level: "warn", transports: ["stdout"] }], }); ``` ## Shutdown ```ts await logger.flush(); // drain pending transport work await logger.close(); // tear down integrations, close transports ``` For crash paths, transports that support it expose `flushSync()`; see [OPERATIONS.md](OPERATIONS.md). ## Next Steps - [CONCEPTS.md](CONCEPTS.md) — the pipeline model: records, events, middleware, processors, transports, codecs. - [TRANSPORTS.md](TRANSPORTS.md) — every built-in transport and how to write your own. - [PRETTY.md](PRETTY.md) — browser DevTools and Node terminal pretty output. - [INTEGRATIONS.md](INTEGRATIONS.md) — automatic collection for browser and Node. - [PROCESSORS.md](PROCESSORS.md) — the middleware/processor toolbox. - [CODECS.md](CODECS.md) — serialization ownership and the codec contract. - [PERFORMANCE.md](PERFORMANCE.md) — configuring for throughput. - [OPERATIONS.md](OPERATIONS.md) — privacy, offline queues, crash paths. - [PRODUCTION-RECIPES.md](PRODUCTION-RECIPES.md) — browser HTTP/offline, Node stdout+OTLP, Loki/Datadog deployments. - [API-STABILITY.md](API-STABILITY.md) — v1 stable API subset and pre-1.0 compatibility policy. - [MIGRATION.md](MIGRATION.md) — coming from pino, winston, or console. --- # Concepts Source: https://github.com/jskits/loggerjs/blob/main/docs/CONCEPTS.md # Concepts LoggerJS is organized around three user-facing concepts — **integrations**, **middleware/processors**, and **transports** — plus one boundary rule: **codecs belong to transports**. This page explains the pipeline that connects them. ## The Pipeline ``` logger.info("msg", data) │ ├─ level gate one numeric comparison; disabled levels stop here │ ├─ LogRecord built lazy message kept unevaluated, raw error kept, │ category/type/tags/context attached │ ├─ middleware sync, ordered, can mutate or drop the record │ ├─ processors? if any processors exist, the record is projected │ │ to a LogEvent (id assigned, message resolved, │ │ error normalized) and processors run on the event │ │ │ └─ no processors: the record goes to transports directly │ (the "record fast path" — no projection cost) │ └─ transports each transport receives the record (write/ │ writeBatch) or the event (log/logBatch); │ conversion happens once and is shared │ └─ codec the transport serializes with its codec ``` Integrations sit outside this flow: they hook platform behavior (console calls, errors, fetch, process events) and feed captured input into the same pipeline through `api.capture()`. ## LogRecord vs LogEvent `LogRecord` is the hot-path shape. It preserves raw values so no work happens before it is needed: - `lazy` — an unevaluated message function, resolved at most once. - `err` — the raw error value, not yet normalized. - `props` — the user data object, shared by reference unless middleware, processors, or a transport explicitly clone it. - `ctx` — a frozen bound context object, shared by reference. - `tags` — possibly the logger's frozen tags object, shared by reference. - No `id` — id computation is deferred to event projection. `LogEvent` is the transport-facing compatibility shape: `id`, `time`, `seq`, `level`, `levelName`, `logger` (dotted category), `message` (resolved string), `type`, `tags`, `data`, `error` (normalized `SerializedError`), `context`, `trace`, `source`. `recordToEvent()` / `eventToRecord()` convert between them. Conversion is lossy in documented ways: a `runtime` source collapses to an integration source, and scalar event data is wrapped as `{ value }`. Object data is not snapshotted by default; clone before logging when later mutation must not affect deferred transports. ### Mutation contract Middleware may mutate a record, with one rule: **replace fields, never mutate shared objects in place**. `record.ctx` and logger-level `record.tags` are frozen and shared across records; write `record.tags = { ...record.tags, extra }`, not `record.tags.extra = ...`. In-place mutation of a frozen field throws and is reported as a middleware error without corrupting other records. ## Middleware vs Processors Both are synchronous and error-isolated. They differ in what they see and when: | | Middleware | Processor | | ---------------- | ---------------------------- | ----------------------- | | Input | `LogRecord` | `LogEvent` | | Runs | before id/message/error work | after projection | | Drop | return `null` | return `false` | | Mutate | in place (replace fields) | return a new event | | Cost of dropping | cheapest possible | projection already paid | Prefer middleware for enrichment and early filtering. Use processors when you need the resolved event shape — routing by event fields, fingerprinting normalized errors, buffering events for fingers-crossed delivery. **Configuring any processor disables the record fast path** for that logger, because every log must then be projected to an event. That is the correct trade when you need event-level behavior; see [PERFORMANCE.md](PERFORMANCE.md) for the numbers. ## Transports A transport implements any of four methods: ```ts interface Transport { name?: string; minLevel?: LoggerLevel; ready?(): void | Promise; write?(record: LogRecord, context: TransportContext): void | Promise; writeBatch?( records: LogRecord[], context: TransportContext, ): void | Promise; log?(event: LogEvent, context: TransportContext): void | Promise; logBatch?( events: LogEvent[], context: TransportContext, ): void | Promise; flush?(): void | Promise; flushSync?(): void; close?(): void | Promise; } ``` - Record-aware transports (`write`/`writeBatch`) participate in the fast path and may encode records directly. - Event transports (`log`/`logBatch`) receive projected events. - `context.toEvent(record)` converts on demand; the result is memoized per record, so several transports share one projection and ids stay stable across conversions. - Errors thrown by a transport (sync or async) are caught and reported to logger meta; one failing transport never blocks the others. - `ready()` is explicit and opt-in. Normal log calls do not wait for transport startup; callers that need startup confirmation call `logger.ready()`. - `close()` must include its own best-effort flush before resource release. Core calls `close()` when it exists and falls back to `flush()` only when a transport has no `close()`. ## Codecs Belong to Transports Serialization is owned by the transport, configured through its codec. Middleware and processors keep values raw — never pre-stringify in the pipeline. This keeps redaction working on structured data, lets each destination pick its own wire format, and lets batching amortize serialization. ```ts stdoutTransport({ codec: ndjsonCodec() }); browserHttpTransport({ url: "/api/logs", codec: fastEventJsonCodec() }); ``` See [CODECS.md](CODECS.md) for the contract and the fast-by-default safety semantics. ## Integrations An integration is a named `setup(api)` function that hooks a platform surface and returns a teardown: ```ts interface Integration { name: string; setup(api: IntegrationSetupContext): void | Teardown; } ``` The setup context provides the logging API plus three safety tools: - `api.capture(input)` — feed a captured signal into the pipeline, tagged with `source: "integration:"`. - `api.guard(fn)` — re-entrancy guard: if the patched code path ends up calling the logger, which calls the patched code again, the inner invocation is dropped and counted instead of looping forever. - `api.unpatched` — a registry of original functions (`console.*`, `fetch`, `XMLHttpRequest`) so transports and integrations can call the real implementation under patching. Integrations are installed at logger construction (or `addIntegration()`), set up exactly once per integration instance, and torn down in reverse order on `close()`. ## Routing Processors can pin an event to named transports: ```ts import { routeProcessor } from "@loggerjs/processors"; routeProcessor([{ minLevel: "error", transports: ["alerts"] }]); ``` Routes are attached as non-enumerable event metadata and consulted at dispatch. The record fast path performs no route filtering — routes can only be attached by processors, and the record path only runs when a logger has zero processors. ## Levels, Categories, Sources - Levels are numbers (`trace` 10 … `fatal` 60) with names; custom numeric levels work everywhere. - Categories are string arrays (`["api", "checkout"]`) joined to a dotted logger name in events; the registry routes configuration by category prefix. - `source` distinguishes app logs from integration captures, so console capture can be excluded from console output and loops are detectable. ## Internal Errors and Meta Counters The pipeline never throws into application code. Failures in middleware, processors, codecs, and transports are reported through `onInternalError` and counted in logger meta: ```ts import { getLoggerMetaStats } from "@loggerjs/core"; getLoggerMetaStats(); // { "transport.errors": 1, "transport.dropped.queue-full": 2, "codec.fallback": 1, ... } ``` Use these counters to alert on silent degradation: queue drops, codec fallbacks, integration re-entrancy drops. `getLoggerSelfMetrics()` returns counters and gauges together, including queue depth and circuit-breaker state gauges exposed by shared transport helpers. ## Trace and Semantic Events `trace-propagation` helpers parse/format W3C `traceparent` and baggage headers, and `addContextProvider()` lets integrations attach ambient context without replacing the app's context provider. `semanticEvents` defines common event families (`error`, `http`, `db`, `job`, `ui`, `action`, `security`, `performance`) so integrations and app logs can share field names. ## Further Reading - [ARCHITECTURE.md](ARCHITECTURE.md) — the full design document, invariants, and decisions. - [TRANSPORTS.md](TRANSPORTS.md), [INTEGRATIONS.md](INTEGRATIONS.md), [PROCESSORS.md](PROCESSORS.md), [CODECS.md](CODECS.md) — reference catalogs. --- # Transports Source: https://github.com/jskits/loggerjs/blob/main/docs/TRANSPORTS.md # Transports A transport delivers log records or events to a destination. This page catalogs every built-in transport and shows how to write your own. Exact option types live in each package's TypeScript declarations and `api-reports/`. For an auditable map from each transport to source files, public entries, and contract tests, see [TRANSPORT-CONTRACTS.md](TRANSPORT-CONTRACTS.md). ## Runtime Support | Runtime | Transport support | Notes | | --- | --- | --- | | Core / runtime-neutral | `consoleTransport`, `memoryTransport`, `testTransport`, `batchTransport`, `retryTransport`, `fallbackTransport` | These do not require browser or Node.js-only APIs. Wrappers work around any transport available in the current runtime. | | Pretty / developer UX | `prettyConsoleTransport`, `prettyStreamTransport`, `prettyStdoutTransport`, `prettyStderrTransport` | Browser DevTools and Node terminal display transports from `@loggerjs/pretty`. They are for human-readable output, not durable production delivery. | | Browser / frontend | `browserHttpTransport`, IndexedDB queues/store, WebSocket, service worker, BroadcastChannel, offline-first replay | Uses browser APIs such as `fetch`, `sendBeacon`, `IndexedDB`, `navigator.onLine`, service workers, and BroadcastChannel with feature detection and fallbacks where available. | | Node.js / server | `stdoutTransport`, `stderrTransport`, `fileTransport`, `rotatingFileTransport`, `nodeHttpTransport`, `nodeSyslogTransport`, `workerTransport` | Uses Node.js streams, filesystem, worker threads, network sockets, and Node fetch. | | Vendor / observability | OTLP, Sentry, Datadog, Elastic, Loki, CloudWatch | HTTP wire transports run where their `fetch`/crypto/runtime requirements are present; SDK/provider adapters require the application-provided SDK object or provider. Vendor credentials are usually safer on servers or trusted workers. | | Database / local app / backend | `databaseTransport`, `postgresTransport`, `sqliteTransport` | Driver-agnostic at the LoggerJS layer, but the application must provide database drivers; intended for Node.js, Electron, CLIs, or backend workers. | ## Stability Levels Transport stability describes the public API promise, not an absolute delivery guarantee. Browser storage, process shutdown, network collectors, and vendor backends can still fail; the reliability table below is the delivery contract. | Level | Meaning | | --- | --- | | Stable | Intended for v1-compatible application use. Option names and high-level semantics are protected by API reports, tests, and docs. | | Compatible | Public and tested, but exact runtime behavior or message shape may still be tuned before v1. Use when the documented caveats fit your deployment. | | Experimental | Public and tested, but not part of the v1 compatibility promise yet. Names, options, payload mapping, or batching guidance may change before v1. | | Runtime-dependent | Public API is stable, but practical reliability depends heavily on browser, worker, storage, network, SDK, or database behavior outside LoggerJS. Validate in your target environment. | | Test-only | Built for assertions and fixtures, not production delivery. | | Transport | Stability | Why | | --- | --- | --- | | `consoleTransport()` | Stable | Runtime-neutral local sink with loop prevention for console capture. | | `memoryTransport()` | Stable | Bounded in-memory diagnostics cache; intentionally non-durable. | | `testTransport()` | Test-only | Assertion helper with wait/snapshot APIs. | | `batchTransport()` / `retryTransport()` / `fallbackTransport()` | Stable | Core reliability wrappers used by first-party transports. | | Pretty transports | Stable | Developer display API is stable; exact colors/layout remain presentation details. | | `stdoutTransport()` / `stderrTransport()` / `fileTransport()` | Stable | Production local sinks with drain and crash-path behavior. | | `rotatingFileTransport()` | Stable | Local size rotation; use one writer process per file. | | `nodeHttpTransport()` | Stable | Self-wrapped batched HTTP delivery with shared reliability options. | | `otlpHttpTransport()` | Experimental | OTLP mapping is public and tested, but observability adapter packages are not frozen before v1. | | `nodeSyslogTransport()` | Stable | Wire formatting is stable; UDP/TCP reliability follows syslog transport semantics. | | `workerTransport()` | Compatible | Message protocol is public, but ready/ack/fallback lifecycle tuning may evolve. | | `browserHttpTransport()` | Stable | Primary browser remote transport; pagehide beacon remains best effort. | | `memoryBrowserHttpOfflineQueue()` | Stable | Stable API for temporary offline periods; not reload-durable. | | `indexedDbBrowserHttpOfflineQueue()` / `indexedDbTransport()` / `offlineFirstTransport()` | Runtime-dependent | Stable API, but persistence depends on browser IndexedDB, quota, eviction, private mode, and storage policy. | | `browserWebSocketTransport()` | Compatible | Useful for live/debug channels; reconnection and final durability are caller-owned. | | `browserServiceWorkerTransport()` | Runtime-dependent | API is public, but delivery depends on service worker registration, activation, and lifetime. | | `browserBroadcastChannelTransport()` | Compatible | Same-origin tab fan-out is intentionally lossy and receiver-dependent. | | Datadog / Elastic / Loki / CloudWatch transports | Experimental | Wire payloads are tested, but vendor packages are not frozen before v1; production durability requires batching/retry around raw transports. | | `sentryTransport()` / `openTelemetryLogBridgeTransport()` | Experimental | Adapter contracts are public and tested, but SDK/provider mapping may still change before v1. | | `databaseTransport()` / `sqliteTransport()` / `postgresTransport()` | Experimental | Adapter APIs are public and tested, but driver transaction and schema expectations need more design-partner validation before v1. | ## Import Boundaries Root package imports are convenience presets. Public transport subpaths are documented so users can choose narrower bundles and so new built-in transports cannot silently expand the surface without matching docs. | Runtime | Public transport subpaths | | --- | --- | | Core | `@loggerjs/core/transport-console`, `@loggerjs/core/transport-batch`, `@loggerjs/core/transport-reliability`, `@loggerjs/core/transport-test` | | Browser | `@loggerjs/browser/transport-http`, `@loggerjs/browser/transport-broadcast-channel`, `@loggerjs/browser/transport-service-worker`, `@loggerjs/browser/transport-websocket`, `@loggerjs/browser/transport-indexeddb`, `@loggerjs/browser/offline-first-transport` | | Node.js | `@loggerjs/node/transport-http`, `@loggerjs/node/transport-file`, `@loggerjs/node/transport-rotating-file`, `@loggerjs/node/transport-stdout`, `@loggerjs/node/transport-syslog`, `@loggerjs/node/transport-worker` | | Pretty | `@loggerjs/pretty/transport-console`, `@loggerjs/pretty/transport-stream` | | Observability and data | `@loggerjs/otel/transport-http`, `@loggerjs/sentry/transport`, `@loggerjs/datadog/transport`, `@loggerjs/elastic/transport`, `@loggerjs/loki/transport`, `@loggerjs/cloudwatch/transport`, `@loggerjs/database/transport` | `pnpm verify:component-docs` fails when a public transport subpath is exported without being listed here. New entries should also update the stability and reliability tables above. ## Reliability Posture Transports are composable by default. Some transports include batching or durable local storage internally; raw vendor wire transports do not retry unless you wrap them. Treat this table as the production delivery contract: | Transport or wrapper | Default posture | Production note | | --- | --- | --- | | `consoleTransport()` | immediate local write | Human/dev output; no retry or durability beyond the console target. | | `prettyConsoleTransport()` / `prettyStdoutTransport()` / `prettyStderrTransport()` | immediate human-readable local write | Developer UX only. Use structured transports for production delivery. | | `memoryTransport()` | in-memory ring buffer | Diagnostic cache only; lost on process/page exit. | | `testTransport()` | in-memory assertion sink | Test-only; not a production delivery mechanism. | | `batchTransport(inner)` | batched queue with optional retry/circuit breaker | Use around raw I/O transports when you need queue bounds, retries, backoff, or drop accounting. | | `retryTransport(inner)` | retried immediate delivery | Use when the inner transport already owns batching or when per-call retry is acceptable. | | `fallbackTransport(primary, fallback)` | fallback after primary failure | Use for local backup sinks, not as a replacement for queueing. | | `stdoutTransport()` / `stderrTransport()` | immediate stream write with drain-aware `flush()` and optional `minLength` buffering | Local process sink; `EPIPE` is treated as clean shutdown by default. | | `fileTransport()` | shared file destination with async stream mode, optional `sync: true`, `mkdir`, `append`, `minLength`, and crash-path `flushSync()` | Local durability path; prefer one writer process per file. | | `rotatingFileTransport()` | synchronous shared file destination with size rotation | Local durability path with size rotation; blocks the caller while writing. | | `nodeHttpTransport()` | self-wrapped batched HTTP delivery | Uses `batchTransport`; tune queue, retry, and circuit options for production. | | `nodeSyslogTransport()` | immediate UDP/TCP syslog write | UDP can drop; TCP still depends on socket state and close/flush behavior. | | `workerTransport()` | worker offload with optional ready/ack lifecycle | Fire-and-forget by default; configure `readyTimeoutMs`, `ackTimeoutMs`, fallback, and `autoEnd` when worker acceptance must be observable; `ready()` waits for worker startup when a ready handshake is configured. | | `browserHttpTransport()` | batched fetch with optional offline queue and beacon pagehide mode | Use an IndexedDB queue for reload survival; beacon mode is best-effort and size limited. | | `memoryBrowserHttpOfflineQueue()` | in-memory offline queue | Survives network drops, not reloads or tab close. | | `indexedDbBrowserHttpOfflineQueue()` | IndexedDB offline queue | Survives reloads while quota/storage remains available. | | `offlineFirstTransport(remote)` | remote delivery plus persistent queue replay | Queues on offline or remote failure, then replays later. | | `indexedDbTransport()` | local IndexedDB persistence | Local support/export store; durability depends on browser storage policy and quota. | | `browserWebSocketTransport()` | queued while socket is closed | Reconnection is caller-owned; queued events can drop when bounded queues fill. | | `browserServiceWorkerTransport()` | queue until active service worker is available; `ready()` can wait for `serviceWorker.ready` when `target: "ready"` | Delivery depends on registration, activation, and worker lifetime. | | `browserBroadcastChannelTransport()` | lossy tab broadcast | Receivers must already be listening; not durable. | | `otlpHttpTransport()` | self-wrapped batched OTLP/HTTP delivery | Uses `batchTransport`; tune retry and circuit options for production. | | Datadog / Elastic / Loki / CloudWatch transports | raw HTTP wire delivery | Wrap with `batchTransport()` / `retryTransport()` for queueing, retry, and circuit breaking. | | `sentryTransport()` / `openTelemetryLogBridgeTransport()` | SDK/provider adapter | Reliability follows the SDK/provider you pass in. | | `databaseTransport()` / `sqliteTransport()` / `postgresTransport()` | batched database writes | Adapter/driver owns actual transaction and connection behavior. | ## Core / Runtime-Neutral (`@loggerjs/core`) | Transport | What it does | | --- | --- | | `consoleTransport()` | Pretty per-level console output, or single-line JSON with `pretty: false`. Writes through the unpatched console so console capture cannot loop. Filters out events captured *from* the console by default. | | `memoryTransport()` | Ring buffer of recent events (`maxEvents`, default 1000). Useful for diagnostics endpoints and tests. | | `testTransport()` | Assertion-friendly sink: snapshots, call stats, `waitForEvent()`/`waitForCount()`, injectable failures. | | `batchTransport(inner, options)` | Wraps any transport with batching, retry, and reliability controls (below). | | `retryTransport(inner, options)` | Wraps any transport with retries, exponential backoff, optional circuit breaker, and optional fallback. | | `fallbackTransport(primary, fallback)` | Sends to a fallback transport when the primary throws. | ### `batchTransport` reliability options Every batch-based transport in the ecosystem shares this option set: ```ts batchTransport(inner, { maxRecords: 100, // flush when this many queued maxBytes: 64 * 1024, // per-batch byte budget (estimation only runs when set) maxWaitMs: 2000, // flush timer maxQueueSize: 1000, // backpressure bound dropPolicy: "drop-oldest" /* | "drop-newest" | "throw" */, concurrency: 2, // parallel in-flight batches maxRetries: 3, retryBaseDelayMs: 250, // exponential backoff base retryMaxDelayMs: 5000, circuitBreakerFailureThreshold: 5, circuitBreakerResetMs: 30000, onDrop: (event, reason) => metrics.increment(`log_drop.${reason}`), }); ``` Notes: - Byte estimation walks the payload; it is skipped entirely unless `maxBytes` is finite. - Drops are always counted in logger meta (`transport.dropped.*`); the `onDrop` event conversion only happens when a listener is registered. - A failed batch is re-queued at the head; the circuit breaker stops hammering a dead endpoint. ## Pretty / Developer UX (`@loggerjs/pretty`) | Transport / helper | What it does | | --- | --- | | `prettyConsoleTransport()` | Browser DevTools and local console output with level labels, readable details, optional `%c` browser styles, raw object arguments, and console-capture loop filtering. | | `prettyStreamTransport({ stream })` | Writes human-readable lines to any writable stream-like target. Uses ANSI colors when configured or when auto-detected. | | `prettyStdoutTransport()` / `prettyStderrTransport()` | Node terminal helpers over `process.stdout` / `process.stderr`; honor `NO_COLOR` and `FORCE_COLOR`, support `minLevel`, and let `flush()` wait for `drain`. | | `formatPrettyEvent()` | Shared formatter for custom display transports. Returns plain text, ANSI text, browser console args, and raw details. | Pretty transports are display sinks. They do not batch, retry, persist, or speak collector protocols. See [PRETTY.md](PRETTY.md) for examples and option guidance. ## Node.js / Server (`@loggerjs/node`) | Transport | What it does | | --- | --- | | `stdoutTransport()` / `stderrTransport()` | NDJSON lines with write backpressure tracking, clean `EPIPE` handling, and optional `minLength` buffering; `flush()` waits for pending writes. | | `fileTransport({ path })` | Append NDJSON to a file by default; supports `mkdir`, `append: false`, async `minLength` buffering, `sync: true`, and `flushSync()` for crash paths. | | `rotatingFileTransport({ path, maxBytes, maxFiles })` | Size-based rotation with numbered archives through the same file destination. Synchronous writes; use one logger process per file. | | `nodeHttpTransport({ url })` | fetch-based HTTP delivery wrapped in `batchTransport` (Node 18+). | | `nodeSyslogTransport()` | RFC syslog formatting over UDP/TCP; `formatSyslogMessage()` is exported separately. | | `workerTransport({ workerScript })` | Encodes batches with a codec and posts them to a worker thread, optionally transferring buffers; supports ready timeout, batch ack waiting, fallback, and `autoEnd`. | `nodeHttpTransport()` accepts `transformPayload` for post-codec wire transforms. Use `nodeCompressionPayloadTransform()` for gzip, brotli, or deflate: ```ts import { nodeCompressionPayloadTransform, nodeHttpTransport } from "@loggerjs/node"; nodeHttpTransport({ url: "https://collector.example/logs", transformPayload: nodeCompressionPayloadTransform({ format: "brotli" }), }); ``` `fileTransport().flushSync()` is a crash-path primitive. In async stream mode it writes currently buffered or pending payloads through a synchronous fd so fatal records can reach disk before process exit; if the process continues, the original async stream may still complete. Use `await flush()` for normal drain-and-continue shutdowns, or configure `sync: true` when every write must be synchronous. `workerTransport()` remains compatible with simple workers that only receive object messages. Lifecycle is opt-in: - Set `readyTimeoutMs` when the worker will send `{ type: "loggerjs:ready" }`. If readiness times out, LoggerJS fails the worker and sends the batch to the configured fallback or counts it as `transport.dropped.worker-ready-timeout`. Explicit `transport.ready()` / `logger.ready()` also waits for this startup handshake. - Set `ackTimeoutMs` when the worker will acknowledge each batch with `{ type: "loggerjs:batch:ack", id }`. `flush()` waits for those acks. - The main thread posts `{ type: "loggerjs:batch", id?, codec, contentType, count, payload }`. - A worker can report failure with `{ type: "loggerjs:error", message, error }`; pending batches fall back or are counted as dropped. - `autoEnd` defaults to `true`; set `autoEnd: false` if the worker is shared and should not be terminated by transport `close()`. Worker lifecycle updates the standard transport gauges `transport.ready.` and `transport.queue.depth.`, and pending ack failures count `transport.worker.pending-dropped` plus `transport.dropped.`. For Node runtime diagnostics, call `installLoggerDiagnosticsChannel()` from `@loggerjs/node`. It publishes subscribed LoggerJS internals to `diagnostics_channel` channels named `loggerjs.dispatch`, `loggerjs.transport`, `loggerjs.flush`, `loggerjs.encode`, and `loggerjs.worker`. ## Browser / Frontend (`@loggerjs/browser`) | Transport | What it does | | --- | --- | | `browserHttpTransport({ url })` | Batching HTTP delivery with offline queue, online replay with backoff, and `sendBeacon` on page hide (payloads chunked to `beaconMaxBytes`). | | `memoryBrowserHttpOfflineQueue()` | In-memory offline queue adapter (lost on reload). | | `indexedDbBrowserHttpOfflineQueue()` | Durable offline queue in IndexedDB; survives reloads. | | `offlineFirstTransport(remote)` | Standard remote + persistent queue wrapper; queues while offline or when remote delivery fails, then replays later. | | `indexedDbTransport()` | Persist logs locally in IndexedDB with session-aware indexes, TTL/count/byte pruning, durability hints, optional Storage Bucket isolation, an async `query()` API, `sessions()`, and `stats()` observability. | | `browserWebSocketTransport({ socket })` | Codec-encoded batches over a WebSocket; queues while the socket is closed (reconnection is the caller's responsibility). | | `browserServiceWorkerTransport()` | Posts events to a service worker, queueing until one is active; with `target: "ready"`, explicit `ready()` waits for `serviceWorker.ready`. | | `browserBroadcastChannelTransport({ channel })` | Fan logs out to other tabs (lossy by nature; receivers must be listening). | | `exportLogsToZip(source)` / `createLogZipBlob()` / `downloadBlob()` | Bundle logs (for example from `indexedDbTransport().query()`) into a ZIP with manifest, optional per-session files, optional `recent.ndjson`/`recent.json`, and CRC for support workflows. | `browserHttpTransport()` also accepts `transformPayload`. Use `browserCompressionPayloadTransform()` for browsers with `CompressionStream`: ```ts import { browserCompressionPayloadTransform, browserHttpTransport } from "@loggerjs/browser"; browserHttpTransport({ url: "/api/logs", transformPayload: browserCompressionPayloadTransform({ format: "gzip" }), }); ``` For high-throughput local browser capture on modern Chrome, prefer a dedicated IndexedDB log store with relaxed durability: ```ts indexedDbTransport({ durability: "relaxed", localStorageSpill: { maxBytes: 512 * 1024, maxEntries: 200, namespace: "loggerjs-support", }, storageBucketName: "loggerjs-logs", storageBucketDurability: "relaxed", }); ``` Browsers without Storage Buckets support fall back to the regular IndexedDB instance while keeping the same transport API. `indexedDbTransport()` assigns a page-session id by default, stores it as a top-level IndexedDB entry field, and mirrors it into `event.context.sessionId` when the event did not already provide one. Pass `session: false` to disable that materialized session field, or pass `session: { id, getId, contextKey }` to align the persisted session with your own browser context provider. `localStorageSpill` is a last-chance reload/close safety net, not a replacement for IndexedDB. Normal logging still batches in memory and flushes to IndexedDB asynchronously. On `pagehide` or `visibilitychange: hidden`, the transport synchronously writes the still-unconfirmed tail (`pendingFlushBatch` plus the current memory buffer) to a small `localStorage` temp entry. The next transport instance drains that temp entry into IndexedDB before its first flush and clears it only after the write succeeds. This lowers loss during ordinary reloads and tab closes, but it cannot protect against process kill, browser crash, disabled storage, quota exhaustion, or storage eviction. ### Browser failure boundaries Browser delivery is best effort unless the log has already been acknowledged by the destination you care about. These are the important loss windows: | Path | Failure boundary / loss window | Production guidance | | --- | --- | --- | | `browserHttpTransport()` | In-memory batches are lost on reload, tab close, process kill, or if the queue bound drops records before delivery. Fetch can be aborted by navigation. | Use bounded queues, retry options, and an IndexedDB offline queue when reload survival matters. | | `browserHttpTransport({ useBeaconOnPageHide: true })` | `sendBeacon` is fire-and-forget. Browsers cap payload size and can reject, truncate, or skip delivery under shutdown pressure. | Keep `beaconMaxBytes` conservative, treat pagehide flush as a last chance, and do not use it as the only durability path. | | `memoryBrowserHttpOfflineQueue()` | Survives temporary offline periods only while the page process stays alive. | Use for lightweight apps or tests; switch to IndexedDB for support/debug logs that must survive reload. | | `indexedDbBrowserHttpOfflineQueue()` | Stores replay payloads across reloads, but quota, private browsing mode, storage eviction, blocked upgrades, or unavailable IndexedDB can still prevent persistence. | Monitor queue/drop counters and keep payloads bounded; pair with HTTP replay and page lifecycle flush. | | `offlineFirstTransport(remote)` | Queues when remote delivery fails, then replays later. Replay is not a guarantee if local storage fails or is evicted. | Prefer a persistent queue adapter and call `flush()` during controlled shutdown/navigation when possible. | | `indexedDbTransport()` | Local persistence depends on IndexedDB availability, quota, eviction policy, durability hints, and browser support for Storage Buckets. Logs still in the memory buffer can be lost before the async IndexedDB write finishes. | Use `durability: "relaxed"` for throughput when acceptable; use TTL/count/byte pruning to stay below quota. Enable bounded `localStorageSpill` when support logs should survive ordinary reloads more reliably. | | `browserWebSocketTransport()` | Queued events can be lost when the page exits, the queue bound is exceeded, or the caller never reconnects the socket. | Own reconnection outside the transport and use queue bounds/drop counters to detect backpressure. | | `browserServiceWorkerTransport()` | Delivery depends on service worker registration, activation, message delivery, and worker lifetime. A terminating worker can drop in-flight work unless it persists its own queue. | Treat it as centralization, not durability, unless the service worker also writes to durable storage. | | `browserBroadcastChannelTransport()` | BroadcastChannel only reaches currently open, same-origin listeners. Messages are not durable and receivers can miss them during startup. | Use for multi-tab aggregation and debugging, not as a primary remote delivery guarantee. | The usual production browser stack is HTTP batching plus an IndexedDB offline queue plus page lifecycle flush. Add a service worker or BroadcastChannel when you need centralization across tabs, but keep a durable queue in the delivery path when logs must survive reloads. ## Payload transforms Payload transforms run after codec encoding and before a wire transport sends or stores the payload. They can return a replacement payload, or `{ payload, headers, contentType }`; HTTP transports persist those headers through offline queues and replay. ```ts import { composePayloadTransforms, encryptionPayloadTransform, } from "@loggerjs/core/payload-transforms"; import { browserCompressionPayloadTransform, browserHttpTransport } from "@loggerjs/browser"; browserHttpTransport({ url: "/api/logs", transformPayload: composePayloadTransforms( browserCompressionPayloadTransform(), encryptionPayloadTransform({ contentType: "application/octet-stream", headers: { "x-payload-encrypted": "1" }, encrypt: async (payload) => encryptForCollector(payload), }), ), }); ``` `encryptionPayloadTransform()` provides the hook; the encryption algorithm and key management remain application-owned. ## Vendor packages Vendor HTTP transports speak the wire protocol directly over `fetch`. SDK/provider adapters such as Sentry and the OpenTelemetry bridge use the SDK object or provider your app already initialized. `otlpHttpTransport()` wraps itself in `batchTransport`; Datadog, Elastic, Loki, and CloudWatch expose `logBatch`, so wrap them with core reliability wrappers when you need queueing, retry, or circuit-breaker behavior. Production vendor usage should make the reliability wrapper visible: ```ts import { batchTransport } from "@loggerjs/core"; import { datadogLogsTransport } from "@loggerjs/datadog"; const transport = batchTransport(datadogLogsTransport({ apiKey: process.env.DD_API_KEY }), { maxRecords: 100, maxWaitMs: 2000, maxQueueSize: 5000, maxRetries: 3, circuitBreakerFailureThreshold: 5, }); ``` | Package | Transport | Destination | | --- | --- | --- | | `@loggerjs/otel` | `otlpHttpTransport({ url })` | OTLP/HTTP JSON logs endpoint; `otlpJsonCodec()` and mapping helpers exported. | | `@loggerjs/otel` | `openTelemetryLogBridgeTransport()` | Bridge into an OpenTelemetry `LoggerProvider`. | | `@loggerjs/sentry` | `sentryTransport({ sentry })` | Sentry structured logs, breadcrumbs, exception/message capture. | | `@loggerjs/datadog` | `datadogLogsTransport({ apiKey })` | Datadog Logs intake API. | | `@loggerjs/elastic` | `elasticTransport({ url, index })` | Elasticsearch `_bulk` API with per-record index/pipeline/id selection. | | `@loggerjs/loki` | `lokiTransport({ url })` | Grafana Loki push API with stream labels and structured metadata. | | `@loggerjs/cloudwatch` | `cloudWatchLogsTransport({ ... })` | CloudWatch Logs `PutLogEvents` with built-in SigV4 signing. | | `@loggerjs/database` | `sqliteTransport()` / `postgresTransport()` / `databaseTransport(adapter)` | Batched inserts through driver-agnostic adapters. | ## Writing a Custom Transport Implement any of the four delivery methods. The simplest event transport: ```ts import type { Transport } from "@loggerjs/core"; const myTransport: Transport = { name: "my-sink", minLevel: "info", log(event) { push(JSON.stringify(event)); }, }; ``` A record-aware transport opts into the fast path (no event projection when the logger has no processors): ```ts import { fastEventJsonCodec } from "@loggerjs/codecs"; import { createPreparedRecordEncoder } from "@loggerjs/core"; const codec = fastEventJsonCodec(); const encodeRecord = createPreparedRecordEncoder(codec); const recordSink: Transport = { name: "record-sink", write(record, context) { push(encodeRecord(record)); // Need the event shape instead? context.toEvent(record) converts once // and is memoized, so other transports share the same projection. }, }; ``` Rules of the road: - Throwing (sync or rejected promise) is safe: errors are reported to logger meta and other transports keep running. Do not swallow your own errors silently — let them surface. - Implement `ready()` when callers can explicitly wait for startup. `logger.ready()` is opt-in; normal log calls never wait for transport readiness. - Implement `flush()` if you buffer, `flushSync()` if you can drain synchronously on crash paths, `close()` if you hold resources. - If you implement `close()`, include your own best-effort flush before releasing resources. Core calls `close()` when present and falls back to `flush()` only for transports without `close()`. - Prefer `logBatch`/`writeBatch` plus `batchTransport` for anything that does I/O; per-event network calls do not survive production traffic. - Encoding raw records directly skips the logger's `idFactory`; records get the documented `defaultRecordId`. Convert via `context.toEvent()` when custom ids matter. See [CODECS.md](CODECS.md). --- # Transport Contracts Source: https://github.com/jskits/loggerjs/blob/main/docs/TRANSPORT-CONTRACTS.md # Transport Contract Matrix This matrix pins every first-party transport to its public entry points, source files, and contract tests. It is meant to make reliability claims auditable: raw wire transports must surface delivery failures, production-grade delivery must be explicit about wrappers or queues, and lifecycle behavior must have a named test path. `pnpm verify:transport-contracts` checks this page against package exports and the repository file tree. When a transport source file or public transport subpath is added, update this matrix in the same change. ## Contract Rules - Raw HTTP/vendor transports propagate non-2xx responses and rejected `fetch` calls; they do not silently retry unless wrapped. - Production delivery for raw wire sinks requires `batchTransport()`, `retryTransport()`, `fallbackTransport()`, or a transport that documents its own queue/retry behavior. - Runtime-dependent transports must document the platform surface they depend on and test unavailable or failing dependencies. - Durable paths must expose `flush()` or persistence/replay behavior in tests. - Test-only and display-only transports must say so instead of implying delivery durability. ## Matrix | ID | Public entry | Export(s) | Source | Delivery contract | Failure and lifecycle contract | Contract tests | | --- | --- | --- | --- | --- | --- | --- | | `core-batch` | `@loggerjs/core/transport-batch` | `batchTransport` | [packages/core/src/transports/batch.ts](packages/core/src/transports/batch.ts) | Bounded queue, byte budget, timed flush, concurrency, retry, circuit breaker, and drop accounting around any inner transport. | Failed batches are retried or re-queued according to policy; `flush()` drains; `close()` stops timers and flushes pending work. | [packages/core/test/batch-transport.test.ts](packages/core/test/batch-transport.test.ts)
[packages/core/test/batch-coverage.test.ts](packages/core/test/batch-coverage.test.ts) | | `core-retry` | `@loggerjs/core/transport-reliability` | `retryTransport`, `fallbackTransport` | [packages/core/src/transports/reliability.ts](packages/core/src/transports/reliability.ts) | Immediate retry/fallback wrapper for transports that already own batching or need per-call retry. | Retries use bounded backoff; circuit breaker and fallback behavior are observable; `flush()`/`close()` delegate to wrapped transports. | [packages/core/test/reliability-transport.test.ts](packages/core/test/reliability-transport.test.ts)
[packages/core/test/reliability-coverage.test.ts](packages/core/test/reliability-coverage.test.ts) | | `core-console` | `@loggerjs/core/transport-console` | `consoleTransport` | [packages/core/src/transports/console.ts](packages/core/src/transports/console.ts) | Runtime-neutral local console sink. | Uses unpatched console functions to avoid capture recursion; filters console-origin events by default. | [packages/core/test/console-transport.test.ts](packages/core/test/console-transport.test.ts) | | `core-memory` | Root export | `memoryTransport` | [packages/core/src/transports/memory.ts](packages/core/src/transports/memory.ts) | Bounded in-memory diagnostics ring buffer; not durable. | Drops oldest entries beyond capacity; snapshots are synchronous and process-local. | [packages/core/test/logger.test.ts](packages/core/test/logger.test.ts)
[packages/core/test/integration-api.test.ts](packages/core/test/integration-api.test.ts) | | `core-test` | `@loggerjs/core/transport-test` | `testTransport` | [packages/core/src/transports/test.ts](packages/core/src/transports/test.ts) | Assertion-only in-memory sink with wait and snapshot helpers. | Injectable failures and wait timeouts are part of the test contract; not a production sink. | [packages/core/test/test-transport.test.ts](packages/core/test/test-transport.test.ts) | | `browser-http` | `@loggerjs/browser/transport-http` | `browserHttpTransport` | [packages/browser/src/http-transport.ts](packages/browser/src/http-transport.ts) | Browser fetch delivery with batching, retry options, optional offline queue, payload transforms, and page-exit beacon mode. | Non-2xx and rejected fetch attempts are failures; page-exit beacon is best effort and size/runtime dependent. | [packages/browser/test/http-transport.test.ts](packages/browser/test/http-transport.test.ts)
[tests/e2e/browser-production.spec.ts](tests/e2e/browser-production.spec.ts) | | `browser-indexeddb-queue` | `@loggerjs/browser/offline-indexeddb`, `@loggerjs/browser/transport-indexeddb` | `indexedDbBrowserHttpOfflineQueue`, `indexedDbTransport` | [packages/browser/src/indexeddb-offline-queue.ts](packages/browser/src/indexeddb-offline-queue.ts)
[packages/browser/src/indexeddb-transport.ts](packages/browser/src/indexeddb-transport.ts) | Persistent browser queue/store for reload-surviving replay and local export. | IndexedDB unavailability, quota, private mode, blocked upgrades, TTL, and pruning are runtime-dependent and tested. | [packages/browser/test/indexeddb-offline-queue.test.ts](packages/browser/test/indexeddb-offline-queue.test.ts)
[packages/browser/test/indexeddb-transport.test.ts](packages/browser/test/indexeddb-transport.test.ts)
[tests/e2e/browser-production.spec.ts](tests/e2e/browser-production.spec.ts) | | `browser-offline-first` | `@loggerjs/browser/offline-first-transport` | `offlineFirstTransport` | [packages/browser/src/offline-first-transport.ts](packages/browser/src/offline-first-transport.ts) | Remote-first delivery with queued replay after offline or remote failure. | Remote rejection queues payloads; `flush()` replays queued records when storage and network are available. | [packages/browser/test/offline-first-transport.test.ts](packages/browser/test/offline-first-transport.test.ts) | | `browser-page-exit` | `@loggerjs/browser/transport-http`, `@loggerjs/browser/integration-page-lifecycle` | `browserHttpTransport` page lifecycle flush | [packages/browser/src/http-transport.ts](packages/browser/src/http-transport.ts)
[packages/browser/src/page-lifecycle.ts](packages/browser/src/page-lifecycle.ts) | Last-chance pagehide/visibilitychange delivery for browser HTTP. | `sendBeacon` is best effort; unsupported or failed beacon falls back to configured HTTP behavior where possible. | [packages/browser/test/http-transport.test.ts](packages/browser/test/http-transport.test.ts)
[packages/browser/test/page-lifecycle.test.ts](packages/browser/test/page-lifecycle.test.ts)
[tests/e2e/browser-production.spec.ts](tests/e2e/browser-production.spec.ts) | | `browser-service-worker` | `@loggerjs/browser/transport-service-worker` | `browserServiceWorkerTransport` | [packages/browser/src/service-worker-transport.ts](packages/browser/src/service-worker-transport.ts) | Queue until a configured or ready service worker controller is available, then post messages. | Registration/controller availability is runtime-dependent; `ready()` and `flush()` expose acceptance boundaries. | [packages/browser/test/service-worker-transport.test.ts](packages/browser/test/service-worker-transport.test.ts)
[tests/e2e/browser-production.spec.ts](tests/e2e/browser-production.spec.ts) | | `browser-websocket` | `@loggerjs/browser/transport-websocket` | `browserWebSocketTransport` | [packages/browser/src/websocket-transport.ts](packages/browser/src/websocket-transport.ts) | Live WebSocket sink with bounded queue while connecting or closed. | Reconnection is caller-owned; queue bounds and close/error behavior are explicit. | [packages/browser/test/websocket-transport.test.ts](packages/browser/test/websocket-transport.test.ts) | | `browser-broadcast` | `@loggerjs/browser/transport-broadcast-channel` | `browserBroadcastChannelTransport` | [packages/browser/src/broadcast-channel-transport.ts](packages/browser/src/broadcast-channel-transport.ts) | Same-origin tab fan-out via BroadcastChannel. | Receiver presence is not guaranteed; missing API and close semantics are tested. | [packages/browser/test/broadcast-channel-transport.test.ts](packages/browser/test/broadcast-channel-transport.test.ts) | | `node-http` | `@loggerjs/node/transport-http` | `nodeHttpTransport` | [packages/node/src/http-transport.ts](packages/node/src/http-transport.ts) | Node fetch delivery self-wrapped in `batchTransport`. | Non-2xx/rejected fetch attempts are retried by the wrapper; `flush()` drains batched HTTP work. | [packages/node/test/http-transport.test.ts](packages/node/test/http-transport.test.ts) | | `node-file` | `@loggerjs/node/transport-file`, `@loggerjs/node/transport-rotating-file` | `fileTransport`, `rotatingFileTransport` | [packages/node/src/file-transport.ts](packages/node/src/file-transport.ts)
[packages/node/src/rotating-file-transport.ts](packages/node/src/rotating-file-transport.ts) | Local NDJSON file durability, optional synchronous writes, buffering, and size rotation. | `flush()` drains normal writes; `flushSync()` is the crash-path contract where supported; rotation is single-writer. | [packages/node/test/file-transport.test.ts](packages/node/test/file-transport.test.ts)
[packages/node/test/rotating-file-transport.test.ts](packages/node/test/rotating-file-transport.test.ts) | | `node-stdout` | `@loggerjs/node/transport-stdout` | `stdoutTransport`, `stderrTransport` | [packages/node/src/stdout-transport.ts](packages/node/src/stdout-transport.ts) | NDJSON stream sink for stdout/stderr with optional buffering. | Backpressure is drain-aware; `EPIPE` is treated as clean shutdown by default; `flush()` drains buffered writes. | [packages/node/test/stdout-transport.test.ts](packages/node/test/stdout-transport.test.ts) | | `node-syslog` | `@loggerjs/node/transport-syslog` | `nodeSyslogTransport` | [packages/node/src/syslog-transport.ts](packages/node/src/syslog-transport.ts) | RFC-style syslog formatting over UDP or TCP. | UDP is lossy; TCP depends on socket lifecycle; format, send, close, and error behavior are tested. | [packages/node/test/syslog-transport.test.ts](packages/node/test/syslog-transport.test.ts) | | `node-worker` | `@loggerjs/node/transport-worker` | `workerTransport` | [packages/node/src/worker-transport.ts](packages/node/src/worker-transport.ts) | Worker-thread offload with codec transfer and optional ready/ack handshake. | Startup, ack timeout, fallback, close, and auto-end behavior define the acceptance boundary. | [packages/node/test/worker-transport.test.ts](packages/node/test/worker-transport.test.ts) | | `database` | `@loggerjs/database/transport`, `@loggerjs/database/sqlite`, `@loggerjs/database/postgres` | `databaseTransport`, `sqliteTransport`, `postgresTransport` | [packages/database/src/transport.ts](packages/database/src/transport.ts)
[packages/database/src/sqlite.ts](packages/database/src/sqlite.ts)
[packages/database/src/postgres.ts](packages/database/src/postgres.ts) | Batched row mapping to an application-provided adapter, SQLite driver, or Postgres client. | Adapter/driver failures surface through batch/retry and internal error reporting; transaction semantics are driver-owned. | [packages/database/test/database-transport.test.ts](packages/database/test/database-transport.test.ts) | | `datadog` | `@loggerjs/datadog/transport` | `datadogLogsTransport` | [packages/datadog/src/index.ts](packages/datadog/src/index.ts) | Raw Datadog Logs HTTP payload sink. | Missing fetch, non-2xx, and rejected fetch fail the raw transport; production durability requires `batchTransport()` or `retryTransport()`. | [packages/datadog/test/datadog-transport.test.ts](packages/datadog/test/datadog-transport.test.ts) | | `elastic` | `@loggerjs/elastic/transport` | `elasticTransport` | [packages/elastic/src/index.ts](packages/elastic/src/index.ts) | Raw Elastic bulk HTTP payload sink. | Missing fetch, non-2xx, and rejected fetch fail the raw transport; production durability requires `batchTransport()` or `retryTransport()`. | [packages/elastic/test/elastic-transport.test.ts](packages/elastic/test/elastic-transport.test.ts) | | `loki` | `@loggerjs/loki/transport` | `lokiTransport` | [packages/loki/src/index.ts](packages/loki/src/index.ts) | Raw Loki push HTTP payload sink. | Missing fetch, non-2xx, and rejected fetch fail the raw transport; production durability requires `batchTransport()` or `retryTransport()`. | [packages/loki/test/loki-transport.test.ts](packages/loki/test/loki-transport.test.ts) | | `cloudwatch` | `@loggerjs/cloudwatch/transport` | `cloudWatchLogsTransport` | [packages/cloudwatch/src/index.ts](packages/cloudwatch/src/index.ts) | Raw CloudWatch Logs API payload sink. | Missing fetch, non-2xx, and rejected fetch fail the raw transport; sequence-token progression remains caller/service dependent. | [packages/cloudwatch/test/cloudwatch-transport.test.ts](packages/cloudwatch/test/cloudwatch-transport.test.ts) | | `sentry` | `@loggerjs/sentry/transport` | `sentryTransport` | [packages/sentry/src/index.ts](packages/sentry/src/index.ts) | Adapter to an application-provided Sentry SDK object. | SDK throws fail the raw transport and are observable by wrappers; capture-level mapping and disabled SDK methods are tested. | [packages/sentry/test/sentry-transport.test.ts](packages/sentry/test/sentry-transport.test.ts) | | `otel-otlp` | `@loggerjs/otel/transport-http`, `@loggerjs/otel/codec-otlp-json` | `otlpHttpTransport`, `otlpJsonCodec` | [packages/otel/src/transport.ts](packages/otel/src/transport.ts)
[packages/otel/src/otlp-json.ts](packages/otel/src/otlp-json.ts) | OTLP/HTTP JSON delivery self-wrapped in `batchTransport`; codec maps LoggerJS events to OTLP logs. | Non-2xx/rejected fetch attempts are retried by the wrapper; codec shape and metadata mapping are pinned. | [packages/otel/test/otlp-json.test.ts](packages/otel/test/otlp-json.test.ts) | | `pretty-console` | `@loggerjs/pretty/transport-console` | `prettyConsoleTransport` | [packages/pretty/src/console-transport.ts](packages/pretty/src/console-transport.ts) | Human-readable browser/console display sink. | Display-only; avoids console capture loops and preserves raw arguments for DevTools. | [packages/pretty/test/console-transport.test.ts](packages/pretty/test/console-transport.test.ts) | | `pretty-stream` | `@loggerjs/pretty/transport-stream` | `prettyStreamTransport`, `prettyStdoutTransport`, `prettyStderrTransport` | [packages/pretty/src/stream-transport.ts](packages/pretty/src/stream-transport.ts) | Human-readable stream sink for Node terminals or stream-like targets. | Backpressure and stream errors are visible; `flush()` waits for drain where supported. | [packages/pretty/test/stream-transport.test.ts](packages/pretty/test/stream-transport.test.ts) | --- # Pretty Output Source: https://github.com/jskits/loggerjs/blob/main/docs/PRETTY.md # Pretty Output Pretty output is a developer-experience layer for local consoles and terminals. It keeps LoggerJS records structured internally, then renders them at the transport boundary for humans. Use pretty transports for development, demos, local debugging, Storybook, browser DevTools, CLIs, and tests that need readable output. Use structured transports such as `stdoutTransport()`, `fileTransport()`, `browserHttpTransport()`, OTLP, Loki, or Datadog for production delivery. ## Browser DevTools Use `prettyConsoleTransport()` from `@loggerjs/pretty` when you want readable browser console output with inspectable objects: ```ts import { createLogger } from "@loggerjs/core"; import { prettyConsoleTransport } from "@loggerjs/pretty/transport-console"; const logger = createLogger({ transports: [ prettyConsoleTransport({ browserStyles: "auto", mode: "compact", includeData: true, includeContext: false, }), ], }); logger.info("cart updated", { itemCount: 3 }); ``` What it does: - Uses `%c` styles in browsers when `browserStyles: "auto"` detects DevTools. - Passes `data`, `error`, and other details as separate console arguments so objects stay expandable. - Writes through LoggerJS' unpatched console registry, so it can run beside `captureConsoleIntegration()` without loops. - Filters records captured from `captureConsoleIntegration()` by default. ## Node Terminal Use `prettyStdoutTransport()` or `prettyStderrTransport()` for local terminal output: ```ts import { createLogger } from "@loggerjs/core"; import { prettyStdoutTransport } from "@loggerjs/pretty/transport-stream"; const logger = createLogger({ transports: [ prettyStdoutTransport({ colors: "auto", mode: "expanded", minLevel: "debug", }), ], }); ``` What it does: - Emits ANSI colors when the target stream is a TTY. - Honors `NO_COLOR` and `FORCE_COLOR`. - Supports `minLevel`. - `flush()` waits for `drain` when the stream reports backpressure. - Does not end `process.stdout` / `process.stderr` on `close()` unless `endOnClose: true` is set. ## Shared Formatter Custom display transports can use the formatter directly: ```ts import { formatPrettyEvent } from "@loggerjs/pretty/formatter"; const rendered = formatPrettyEvent(event, { colors: "never", mode: "expanded", includeTrace: true, }); console.log(rendered.text); ``` `formatPrettyEvent()` returns: - `text` for plain output. - `ansiText` for terminal output when `colors: "always"` is used. - `browserArgs` for `console.*(...args)` with browser `%c` styles. - `details` with raw values and serialized text for custom sinks. ## Option Guidance | Option | Use | | ----------------------------------------------------------------- | -------------------------------------------------------------------------- | | `mode: "compact"` | One-line local output. Good for browser consoles and busy CLIs. | | `mode: "expanded"` | Multi-line output with one detail per line. Good for local Node debugging. | | `colors: "auto"` | Terminal default: use ANSI only when the stream looks interactive. | | `browserStyles: "auto"` | Browser default: use CSS console styles only in browser-like runtimes. | | `includeData` / `includeError` | On by default because pretty output is for humans. | | `includeContext` / `includeTrace` / `includeSource` / `includeId` | Off by default to keep output readable; enable when debugging correlation. | ## Production Boundary Pretty transports are intentionally not durability transports. They do not batch, retry, persist, or speak a collector wire protocol. For production Node services, prefer: ```ts import { stdoutTransport } from "@loggerjs/node"; stdoutTransport(); // NDJSON for collectors ``` For local development, it is normal to run both: ```ts import { createLogger } from "@loggerjs/core"; import { stdoutTransport } from "@loggerjs/node"; import { prettyStderrTransport } from "@loggerjs/pretty"; const logger = createLogger({ transports: [ stdoutTransport({ minLevel: "info" }), prettyStderrTransport({ minLevel: "debug", colors: "auto" }), ], }); ``` --- # Integrations Source: https://github.com/jskits/loggerjs/blob/main/docs/INTEGRATIONS.md # Integrations Integrations collect logs automatically from platform behavior. They are always **opt-in**: nothing is captured until you configure the matching integration. Every capture is tagged with `source: "integration:"` so downstream filtering and loop prevention work. Privacy guidance for what to enable and how to sanitize lives in [OPERATIONS.md](OPERATIONS.md). ## Runtime Support | Runtime | Support | Count | Notes | | ---------------------- | ------------------------------------------------------- | ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Browser / frontend | First-party automatic collectors in `@loggerjs/browser` | 19 | Console, script errors, fetch/XHR, WebSocket, Web Vitals, Performance API, routing, user actions, service worker, extension/Electron renderer hooks, and browser context propagation. | | Node.js / server | First-party automatic collectors in `@loggerjs/node` | 16 | Process crashes, diagnostics channels, HTTP frameworks, outgoing clients, CLI/serverless lifecycle, queues, and database clients. | | Runtime-neutral / core | Integration API only in `@loggerjs/core` | — | The core package defines the integration contract and loop-prevention helpers, but platform capture lives in the browser and Node.js packages. | Custom integrations should feature-detect their platform surface and no-op when the surface is unavailable. ## Stability Levels Integration stability describes the public setup/options contract and teardown behavior. It does not mean the underlying platform emits every signal in every runtime, browser version, framework version, or deployment mode. | Level | Meaning | | --- | --- | | Stable | Intended for v1-compatible application use. Option names, setup/teardown behavior, and high-level captured fields are protected. | | Compatible | Public and tested, but exact field shape or framework/runtime edge handling may still be refined before v1. | | Runtime-dependent | Public API is stable, but the signal itself depends on platform support, browser policy, framework hooks, or deployment lifecycle behavior. | | Integration | Stability | Why | | --- | --- | --- | | `captureConsoleIntegration()` | Stable | Core browser capture primitive with loop prevention and teardown coverage. | | `captureBrowserErrorsIntegration()` | Stable | Standard browser error and rejection capture; CSP details vary by browser. | | `captureFetchIntegration()` / `captureXHRIntegration()` | Stable | Request/response capture contract is stable with explicit sanitization hooks. | | `pageLifecycleIntegration()` | Runtime-dependent | API is stable, but pagehide/visibility timing is browser-controlled and best effort. | | `captureWebVitalsIntegration()` | Runtime-dependent | Depends on PerformanceObserver and browser metric support. | | `capturePerformanceIntegration()` | Runtime-dependent | Entry availability differs by browser, permission policy, and page lifecycle. | | `captureReportingIntegration()` | Runtime-dependent | ReportingObserver and report types vary across browsers. | | `captureRouterIntegration()` | Stable | History/hash capture is stable for generic browser routing. | | Framework router adapters | Compatible | Public adapters are tested, but framework-specific hook shapes may evolve. | | `captureFrameworkErrorsIntegration()` | Compatible | Public helper API is stable; framework error hook payloads remain framework-owned. | | `captureUserActionsIntegration()` | Compatible | Privacy-first defaults are stable; element metadata heuristics may be tuned. | | `captureWebSocketIntegration()` | Compatible | Constructor patching and event capture are public; sampled message details may evolve. | | `captureServiceWorkerIntegration()` | Runtime-dependent | Depends on service worker availability and lifecycle messages. | | `captureRuntimeHostIntegration()` | Runtime-dependent | Extension and Electron surfaces are host-specific and intentionally opt-in by channel. | | `browserContextPropagationIntegration()` | Stable | Ambient context binding contract is stable. | | `captureProcessIntegration()` | Stable | Node crash/warning/exit capture and bounded flush behavior are production commitments. | | `diagnosticsChannelIntegration()` | Runtime-dependent | Node channel names and payloads come from Node and instrumented libraries. | | HTTP framework integrations | Compatible | Express/Fastify/Koa/Nest/Hapi adapters are public; framework lifecycle details may be tuned. | | `nodeFetchIntegration()` / `nodeHttpClientIntegration()` | Compatible | Outgoing HTTP capture is public; Node/undici/http edge details may evolve. | | `captureCliIntegration()` / `serverlessIntegration()` | Compatible | Lifecycle contract is public; platform-specific invocation metadata may be refined. | | `queueIntegration()` / `bullMqIntegration()` | Compatible | Generic and BullMQ operation capture is public; queue payload metadata is intentionally configurable. | | `databaseIntegration()` / `prismaIntegration()` / `redisIntegration()` | Compatible | Data-client method wrapping is public; statement/command extraction heuristics may evolve. | ## Import Boundaries Root package imports are convenience presets. Public integration subpaths are documented so users can choose narrower bundles and so new built-in integrations cannot silently expand the surface without matching docs. | Runtime | Public integration subpaths | | --- | --- | | Browser | `@loggerjs/browser/integration-console`, `@loggerjs/browser/integration-context`, `@loggerjs/browser/integration-errors`, `@loggerjs/browser/integration-fetch`, `@loggerjs/browser/integration-xhr`, `@loggerjs/browser/integration-framework-errors`, `@loggerjs/browser/integration-framework-routers`, `@loggerjs/browser/integration-reporting`, `@loggerjs/browser/integration-router`, `@loggerjs/browser/integration-runtime-host`, `@loggerjs/browser/integration-service-worker`, `@loggerjs/browser/integration-user-actions`, `@loggerjs/browser/integration-websocket`, `@loggerjs/browser/integration-web-vitals`, `@loggerjs/browser/integration-performance`, `@loggerjs/browser/integration-page-lifecycle` | | Node.js | `@loggerjs/node/integration-process`, `@loggerjs/node/integration-cli`, `@loggerjs/node/integration-koa`, `@loggerjs/node/integration-nest`, `@loggerjs/node/integration-hapi`, `@loggerjs/node/integration-prisma`, `@loggerjs/node/integration-redis`, `@loggerjs/node/integration-queue`, `@loggerjs/node/integration-bullmq`, `@loggerjs/node/integration-serverless`, `@loggerjs/node/integration-database`, `@loggerjs/node/integration-express`, `@loggerjs/node/integration-fastify`, `@loggerjs/node/integration-fetch`, `@loggerjs/node/integration-http-client`, `@loggerjs/node/integration-diagnostics` | `pnpm verify:component-docs` fails when a public integration subpath is exported without being listed here. New entries should also update the stability table above and the runtime validation notes for that integration family. ## Browser / Frontend (`@loggerjs/browser`) | Integration | Captures | Notes | | ------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `captureConsoleIntegration()` | `console.debug/info/log/warn/error/trace` calls | Level allowlist (`levels`), rate limit (default 100/s). Patched methods restore on teardown; the console transport writes through unpatched methods, so no loops. | | `captureBrowserErrorsIntegration()` | `window.onerror` script/resource errors, `unhandledrejection`, optional CSP violations | Deduplicates rapid identical script errors. | | `captureFetchIntegration()` | Failed (status ≥ `minStatus`, default 400) and sampled successful `fetch` calls | Header allowlists, URL sanitizer. Errors re-throw to the app after capture. | | `captureXHRIntegration()` | `XMLHttpRequest` lifecycle with status and duration | Same sanitization options as fetch. | | `pageLifecycleIntegration()` | Flushes transports on `pagehide` / `visibilitychange` | Coalesces rapid flushes; pair with the HTTP transport's beacon mode. | | `captureWebVitalsIntegration()` | CLS, FCP, INP, LCP, TTFB | Emits incremental and final values via PerformanceObserver. | | `capturePerformanceIntegration()` | navigation, resource, longtask, measure, mark entries | Deduplicated, capped by `maxEntries`. | | `captureUserActionsIntegration()` | clicks, inputs, submits | Per-element throttling; text/value capture is off by default. | | `captureRouterIntegration()` | route changes (`pushState`/`replaceState`/`popstate`/`hashchange`) | Optional state normalization. | | `captureReportingIntegration()` | ReportingObserver reports (CSP, deprecation, intervention, crash) | Drains pending reports on teardown. | | `captureServiceWorkerIntegration()` | service worker lifecycle, messages, message errors | Message data capture off by default. | | `captureWebSocketIntegration()` | WebSocket connect/open/close/error and sampled messages | Wraps the constructor; sockets created before setup are not tracked. | | `captureFrameworkErrorsIntegration()` | React/Vue/Solid/Svelte error hooks | Exposes `reactComponentDidCatch()`, `vueErrorHandler()`, etc.; buffers errors raised before the logger exists (`maxPending`). | | `captureRuntimeHostIntegration()` | browser-extension messages, Electron IPC on configured channels | Conservative default: no channels monitored. | | `browserContextPropagationIntegration()` | session/request/action and trace context | Adds ambient context providers for traceparent, baggage, session id, request id, and recent user action. | | `nextRouterIntegration()` / `reactRouterIntegration()` / `vueRouterIntegration()` / `nuxtRouterIntegration()` | framework router transitions | Thin adapters over common router APIs; sanitize URLs before logging. | ## Node.js / Server (`@loggerjs/node`) | Integration | Captures | Notes | | ------------------------------------------------------------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | | `captureProcessIntegration()` | `uncaughtException` (fatal), `unhandledRejection`, warnings, exit | With `exitOnUncaught`, captures a fatal record, calls `flushSync()`, waits up to `flushTimeoutMs` for async `flush()`, then exits with code `1`. | | `diagnosticsChannelIntegration()` | Node `diagnostics_channel` messages (http, undici, custom channels) | Message payload capture off by default. | | `expressIntegration(logger)` | request completion with status, route, duration, request id | Returns an Express middleware; optional `withContext` binding per request. | | `fastifyIntegration(logger)` | request lifecycle via onRequest/onError/onResponse hooks | Returns a Fastify plugin; state keyed in a WeakMap. | | `nodeFetchIntegration()` | outgoing `fetch` calls with status and duration | Errors re-throw after capture. | | `nodeHttpClientIntegration()` | `http.request` / `http.get` calls | | | `captureCliIntegration()` | CLI start, exit code, SIGINT/SIGTERM | Sanitizes argv for token/password/secret patterns. | | `serverlessIntegration(logger, handler)` | wraps a serverless handler: invocation, duration, cold start, errors | Supports promise, callback, and sync handlers. | | `queueIntegration()` | queue client operations (publish/consume/ack/nack) with duration | Patches the methods you list per client. | | `databaseIntegration()` | database client calls (query/execute/...) with statement and duration | Statement extracted from the first string arg or `.sql`/`.text`/`.query` properties. | | `koaIntegration()` / `nestMiddlewareIntegration()` / `hapiIntegration()` | framework request lifecycle | Thin adapters for Koa, Express-compatible Nest middleware, and Hapi request hooks. The Nest adapter does not hook Nest exception filters, interceptors, guards, or the original thrown `Error`. | | `prismaIntegration()` | Prisma raw-query methods | Wraps `$queryRaw` / `$executeRaw` raw-query variants only. It does not subscribe to `$on("query")` and does not capture typed model operations such as `prisma.user.findMany()`. | | `redisIntegration()` | Redis command methods | Captures selected command methods, duration, errors, and optional payload metadata. | | `bullMqIntegration()` | BullMQ Queue method calls | Wraps `add`, `addBulk`, and a legacy `process` method when present. It does not hook `Worker` or `QueueEvents` lifecycle events such as `completed`, `failed`, or `stalled`. | ### Context manager Not an integration, but installed the same way once at startup: ```ts import { installAsyncLocalStorageContext } from "@loggerjs/node"; installAsyncLocalStorageContext(); ``` After this, `withContext()` values follow async execution across `await` boundaries. ## Writing a Custom Integration ```ts import type { Integration } from "@loggerjs/core"; export function captureThingIntegration(): Integration { return { name: "thing", setup(api) { const original = thing.onEvent; const capture = api.guard((payload: unknown) => { api.capture({ level: "info", message: "thing event", data: { payload }, }); }); thing.onEvent = (payload) => { capture(payload); return original(payload); }; return () => { thing.onEvent = original; }; }, }; } ``` The setup context (`api`) gives you: - `capture(input)` — the main entry point; stamps `source: "integration:thing"`. - `log/trace/debug/info/warn/error/fatal/event/captureException` — direct logging methods when capture semantics do not fit. - `guard(fn)` — wraps a callback with a re-entrancy counter. If your patched surface is itself triggered by the logging pipeline (the classic case: console capture + console transport), the recursive invocation is dropped and counted in meta (`integration.dropped.reentrant`) instead of recursing. - `unpatched` — registry of original `console.*` / `fetch` / `XMLHttpRequest` implementations, shared across all integrations so double patching composes. - `flush/flushSync/close` — for lifecycle-driven integrations like page hide. Rules of the road: - Always return a teardown that restores what you patched. Teardowns run once, in reverse setup order, on `logger.close()`. - Setup is idempotent per integration _instance_; creating two instances patches twice. Export a factory and document it. - Degrade gracefully: feature-detect the platform surface and no-op when it is missing. - Capture raw structured data and let processors redact; do not pre-format messages. --- # Processors Source: https://github.com/jskits/loggerjs/blob/main/docs/PROCESSORS.md # Processors and Middleware `@loggerjs/processors` is the runtime-neutral toolbox for the pipeline's middle layer. Everything here is synchronous, ordered, and error-isolated: a throwing processor is reported to logger meta and the pipeline continues. Two flavors exist (see [CONCEPTS.md](CONCEPTS.md) for the full model): - **Middleware** run on `LogRecord` before id/message/error work — cheapest place to enrich or drop. - **Processors** run on `LogEvent` after projection — required when you need the resolved event shape. Configuring any processor turns off the record fast path for that logger; middleware do not. ## Runtime Support All 27 processors and middleware are supported in browser/frontend and Node.js/server runtimes. The package itself does not depend on DOM, IndexedDB, filesystem, streams, worker threads, or any vendor SDK. | Runtime | Support | Notes | | --- | --- | --- | | Browser / frontend | Supported | Use for enrichment, privacy scrubbing, sampling, dedupe, routing, breadcrumbs, schema checks, and browser-captured errors before data leaves the page. | | Node.js / server | Supported | Use the same processors with Node transports and integrations; stack parsing and error normalization work on standard JavaScript errors. | | Workers / edge / libraries | Supported | Keep custom provider functions synchronous. Routing and fingers-crossed targets must reference transports available in that runtime. | ## Enrichment | Export | What it does | | --- | --- | | `tagsProcessor(tags)` / `tagsMiddleware(tags)` | Merge fixed tags into every log. | | `typeProcessor(type)` / `typeMiddleware(type)` | Set the event `type`. | | `contextProcessor(ctx)` / `contextMiddleware(ctx)` | Merge fixed context fields. | | `enrichProcessor(input)` / `enrichMiddleware(input)` | General patching: pass a static patch or a function returning `{ message, type, tags, data, context, trace, source }`; return `false` to drop. | | `traceContextProcessor(provider)` / `traceContextMiddleware(provider)` | Attach `{ traceId, spanId, … }` from a provider function on every log. | ## Privacy and Normalization | Export | What it does | | --- | --- | | `redactProcessor(options)` | Mask or remove values by key name, exact dot path, or regex across data/context/tags/structured errors. `censor` is a non-breaking alias for `replacement`. Copy-on-write so async transports never see half-redacted objects. | | `privacyGuardProcessor(options)` | Blanket PII scrubbing with built-in patterns (emails, bearer tokens, card-like digits) plus custom patterns. | | `normalizeErrorProcessor(options)` | Force error shape: stack truncation, cause-chain depth limits, enumerable property capture. | | `stackParserProcessor(options)` / `parseStack(stack)` | Parse stacks into structured frames (file, line, column, function). | | `schemaDevCheckProcessor(options)` | Development-only event shape validation; flags drift between typed events and actual payloads. | ### Redaction behavior `redactProcessor()` is intentionally synchronous and interpreter-based. LoggerJS does not compile user-supplied paths with `eval` or `new Function`; custom matchers run as normal functions inside the processor error boundary. Options: - `keys`: case-insensitive key names, regexes that match key or full path, or a custom `(key, path, value) => boolean` matcher. - `paths`: exact dot paths relative to each redacted event field, such as `user.password` or `request.headers.authorization`; these are not glob patterns. - `replacement`: value written for matches; default `"[REDACTED]"`. - `censor`: Pino-compatible alias for `replacement`; ignored when `replacement` is set. - `remove`: omit matched object properties instead of replacing them. Depth-limit truncation is not a key/path match; when `maxDepth` is reached, the too-deep subtree is still collapsed to `replacement`. - `maxDepth`: maximum traversal depth; default `8`. Reached depth fails closed by replacing the whole subtree instead of emitting unknown nested values. Cost is proportional to traversed object size times matcher count. Prefer exact keys and paths for hot loggers; reserve broad regexes and deep traversal for edge loggers or lower-volume error paths. ## Volume Control | Export | What it does | | --- | --- | | `sampleProcessor(options)` | Probabilistic sampling with per-level rates; `error`/`fatal` are kept by default. | | `dynamicSamplerProcessor(options)` | Adaptive sampling per category over a sliding window — throttles noisy loggers, leaves quiet ones alone. | | `rateLimitProcessor(options)` | Token bucket per category; `error`/`fatal` exempt by default. | | `dedupeProcessor(options)` | Fold repeated identical logs inside a time window into one event with a count. | | `coalesceProcessor(options)` | Suppress repeated events in a window and emit the previous repeat count on the next matching event. | | `fingerprintProcessor(options)` | Compute a stable fingerprint from configurable parts (`logger`, `error.name`, `stack.top`, custom functions) for grouping and dedupe keys. | | `filterProcessor(input)` | Keep/drop by predicate or declarative rules (`minLevel`, `logger`, `type`, `tags`, integration source…). | | `levelOverrideProcessor(input)` | Raise or clamp levels per category pattern (for example demote a chatty dependency). | ## Buffering and Routing | Export | What it does | | --- | --- | | `fingersCrossedProcessor(options)` | Hold low-level logs in per-key ring buffers; when a trigger level fires, flush the buffered history to a target transport. The classic "give me the debug logs, but only when something breaks". | | `breadcrumbBufferProcessor(options)` | Maintain a bounded breadcrumb trail and attach/replay it on triggering events. | | `routeProcessor(input)` | Pin events to named transports or exclude transports, by rule (`[{ minLevel: "error", transports: ["alerts"] }]`). | | `symbolicateStackProcessor(options)` | Hook source-map or release-service symbolication into parsed stack frames without bundling a source-map parser. | ## Ordering Guidance Order matters; each stage sees the previous stage's output: 1. **Enrich first** (tags, context, trace) so later stages can match on the fields. 2. **Normalize** (errors, stacks) before anything that fingerprints or matches on error shape. 3. **Redact before sampling decisions that inspect data**, and always before anything leaves the process. 4. **Volume control last** (sample, rate-limit, dedupe) so you drop fully-formed events and your counters mean what they say. ```ts createLogger({ middleware: [tagsMiddleware({ service: "checkout" })], processors: [ normalizeErrorProcessor(), redactProcessor({ keys: ["password", /token/i] }), sampleProcessor({ rates: { debug: 0.1 } }), ], }); ``` ## Writing Your Own Middleware: ```ts import { createMiddleware } from "@loggerjs/core"; const requestIdMiddleware = createMiddleware("request-id", (record) => { record.props = { ...record.props, requestId: currentRequestId() }; return record; // or null to drop }); ``` Processor: ```ts import type { Processor } from "@loggerjs/core"; const dropHealthChecks: Processor = (event) => { if (event.data && (event.data as { path?: string }).path === "/healthz") return false; return event; }; ``` Contract reminders: - Synchronous only — no `await` in the pipeline. - Replace shared objects, never mutate them in place (`record.tags`, `record.ctx` may be frozen and shared). - Throwing is reported and skipped; do not rely on a processor to always run. --- # Codecs Source: https://github.com/jskits/loggerjs/blob/main/docs/CODECS.md # Codecs A codec turns log records or events into bytes (and optionally back). Codecs belong to **transports** — the pipeline keeps values raw so redaction and routing operate on structured data, and each destination chooses its own wire format. ## The Contract ```ts interface Codec { name: string; contentType: string; encode(input: LogEvent | LogRecord | readonly (LogEvent | LogRecord)[], context?: EncodeContext): TPayload; decode?(payload: TPayload): LogEvent | LogEvent[]; prepareRecordEncoder?(hints: RecordEncoderHints): PreparedRecordEncoder; } ``` - `encode` accepts single items or batches, events or records. `normalizeCodecInput()` from core projects records to events for codecs that only understand events. - `decode` is optional; built-in codecs implement it with `JSON.parse` for symmetric round trips of their own output. - `prepareRecordEncoder` is optional. Record-aware transports can call `createPreparedRecordEncoder(codec)` to let the codec cache stable category/tags fragments while keeping serialization owned by the transport. ## Built-in Codecs | Codec | Package | Behavior | | --- | --- | --- | | `jsonCodec()` | core | Bare `JSON.stringify` after input normalization. Fast, throws on circular/BigInt — pick it only when payloads are guaranteed clean. | | `safeJsonCodec(options)` | core | Full safe normalization every time: circular → `"[Circular]"`, BigInt → string, Error → `{name, message, stack}`, depth/array/key truncation, Map/Set conversion. Default codec of `consoleTransport({ pretty: false })`. | | `ndjsonCodec(options)` | core | One JSON object per line. **Fast-by-default contract** (below). Default codec of the Node stdout/file transports. | | `fastEventJsonCodec(options)` | `@loggerjs/codecs` | The performance codec: native fast path, fragment caches (level, logger, tags, time), scan-based string escaping, flat-data direct writer, lean envelope options. | | `pinoCompatCodec(options)` / `pinoNdjsonProjector(options)` | `@loggerjs/codecs` | Pino-shaped NDJSON for migration paths: `level`, `time`, optional `pid`/`hostname` base fields, `msg`, `err`, and opt-in root data merging with reserved-key protection. | | `msgpackrCodec(options?)` | `@loggerjs/codecs` | Built-in MessagePack codec backed by `msgpackr`; returns `Uint8Array`. Passing `{ pack, unpack }` is still supported for custom runtimes. | | `projectorCodec(options)` | `@loggerjs/codecs` | Generic project → serialize (→ parse → unproject) adapter for custom wire schemas. | | `otlpJsonCodec(options)` | `@loggerjs/otel` | OTLP/HTTP JSON log payloads with resource attributes. | ## The Fast-by-Default Contract `ndjsonCodec()` and `fastEventJsonCodec()` share one documented behavior model: - **No options set** — encode runs on a native fast path. Output matches native `JSON.stringify` semantics: nested raw `Error` values in data serialize as `{}`, no depth truncation. Inputs that make native stringify *throw* (circular references, BigInt) are transparently re-encoded with the safe stringifier instead — a log line is never lost to encoding, and each fallback increments the `codec.fallback` meta counter. - **Any `SafeStringifyOptions` field set** (`maxDepth`, `maxArrayLength`, `maxObjectKeys`, `includeStack`, `stable`, `space`) — the codec opts into full safe normalization for every item, which also preserves `Error` name/message/stack inside data payloads. Choose explicitly: native-fast with throw-protection, or fully normalized. `safeJsonCodec` remains always-safe. ## Lean Envelope Options `fastEventJsonCodec` can trim the envelope for minimal NDJSON output: ```ts fastEventJsonCodec({ includeId: false, // also skips id computation entirely on the record path includeSeq: false, includeLevelName: false, // includeData / includeError / includeContext / includeTrace / includeSource }) ``` With the three header flags off, output is a lean LoggerJS envelope: `{"time":...,"level":30,"logger":"api","message":"...","data":{...}}`. This is the configuration behind the headline benchmark numbers in [BENCHMARKS.md](BENCHMARKS.md), and record-aware custom transports can pair it with `createPreparedRecordEncoder(codec)` for the fastest stable-fragment path. Use `pinoCompatCodec()` when you need Pino field names such as `msg`, `err`, `pid`, and `hostname`. ## Pino Compatibility `pinoCompatCodec()` emits newline-delimited JSON for migration paths that need Pino-shaped output: ```ts import { pinoCompatCodec } from "@loggerjs/codecs"; pinoCompatCodec({ base: { pid: process.pid, hostname: "api-1" }, mergeData: true, }); ``` Root data merging is opt-in. By default, LoggerJS keeps payloads under `data`; when `mergeData: true` is enabled, reserved keys such as `time`, `level`, `msg`, `pid`, `hostname`, `err`, `logger`, and `data` are nested instead of overwriting transport-owned fields. Set `collision: "throw"` if you prefer to reject those payloads during migration testing. This codec is intentionally encode-only: Pino-shaped NDJSON is a migration wire format, not the native LoggerJS event envelope. ## Records, Events, and IDs Encoding raw `LogRecord`s (the fast path) has one semantic difference from encoding events: records carry no id, so the codec stamps `defaultRecordId(record, levelName)` — a `time36-seq36-levelName` string identical to what `recordToEvent()` would assign. Consequences: - With the default id factory, record-encoded and event-encoded output are identical. - A custom `idFactory` on the logger is **bypassed** by record-direct encoding. If custom ids matter, have your transport convert via `context.toEvent(record)` (memoized, id factory applied) instead of encoding the record directly. ## Writing a Custom Codec ```ts import { normalizeCodecInput, type Codec } from "@loggerjs/core"; export function csvCodec(): Codec { return { name: "csv", contentType: "text/csv", encode(input) { const events = normalizeCodecInput(input); const list = Array.isArray(events) ? events : [events]; return list .map((e) => `${e.time},${e.levelName},${JSON.stringify(e.logger)},${JSON.stringify(e.message)}`) .join("\n"); }, }; } ``` Guidelines: - **Never throw out of `encode`.** Wrap risky paths and fall back to `safeJsonStringify` from core; count fallbacks with `incrementLoggerMetaCounter("codec.fallback")`. A throwing codec turns into a transport failure and, inside a batch transport, a poison batch that burns retries. - Use `normalizeCodecInput()` unless you deliberately implement a record fast path. - If you implement `prepareRecordEncoder`, its output must be byte-for-byte identical to `encode(record)` for the same record and must keep the same fallback behavior. - For binary formats return `Uint8Array` and set an accurate `contentType` — HTTP transports send it. - Implement `decode` only when symmetric round trips are part of your feature (replay, local query); it is not required for delivery. --- # Production Recipes Source: https://github.com/jskits/loggerjs/blob/main/docs/PRODUCTION-RECIPES.md # Production Recipes These recipes are starting points for production deployments. They intentionally show queue bounds, privacy processors, shutdown behavior, and where credentials belong. Tune names, tags, and endpoint URLs to your application. ## Browser to HTTP With IndexedDB Offline Replay Use this when browser logs should survive network drops and normal reloads. The browser still cannot guarantee delivery during process kill, storage eviction, private browsing restrictions, or quota exhaustion. ```ts import { browserHttpTransport, captureBrowserErrorsIntegration, captureConsoleIntegration, captureFetchIntegration, captureWebVitalsIntegration, createLogger, indexedDbBrowserHttpOfflineQueue, pageLifecycleIntegration, } from "@loggerjs/browser"; import { privacyGuardProcessor, redactProcessor } from "@loggerjs/processors"; const offlineQueue = indexedDbBrowserHttpOfflineQueue({ dbName: "checkout-web-http-offline", storeName: "http-offline", maxEntries: 5000, dropPolicy: "drop-oldest", }); export const logger = createLogger({ category: ["web"], level: "info", tags: { service: "checkout-web", env: "production", runtime: "browser", }, processors: [ redactProcessor({ keys: ["password", "token", "authorization", "cookie", /secret/i], }), privacyGuardProcessor({ maxStringLength: 8192, }), ], transports: [ browserHttpTransport({ name: "browser-http", url: "/api/logs", maxBatchSize: 50, flushIntervalMs: 2000, maxQueueSize: 2000, dropPolicy: "drop-oldest", offlineQueue, offlineReplayMaxRetries: 3, offlineReplayBaseDelayMs: 250, offlineReplayMaxDelayMs: 5000, useBeaconOnPageHide: true, beaconMaxBytes: 60 * 1024, }), ], integrations: [ captureConsoleIntegration({ levels: ["warn", "error"], captureArguments: false, maxCapturesPerSecond: 50, }), captureBrowserErrorsIntegration({ captureSecurityPolicyViolation: true, }), captureFetchIntegration({ minStatus: 400, captureRequestHeaders: ["content-type", "x-request-id"], captureResponseHeaders: ["content-type", "x-request-id"], sanitizeUrl: (url) => { const parsed = new URL(url, location.origin); parsed.search = ""; return parsed.toString(); }, }), captureWebVitalsIntegration({ flushOnHidden: true }), pageLifecycleIntegration(), ], }); ``` Production notes: - `/api/logs` should be your own collector endpoint. Do not put vendor API keys in the browser bundle. - Keep fetch/XHR header capture allowlisted. Do not capture cookies, authorization headers, request bodies, or form values by default. - Alert on logger meta counters such as `transport.dropped.*` and offline queue depth when your app exposes them. - Keep the HTTP offline queue `dbName` separate from any queryable support-log store. The two helpers use independent IndexedDB schemas and version lifecycles. ## Browser Support Export With Session-Aware IndexedDB Use this when support or QA needs a local log bundle that survives reloads and can be exported by session. The local store is separate from the HTTP delivery queue: IndexedDB is the queryable source of truth, while `localStorageSpill` only protects the small tail that has not finished its async IndexedDB write when the user refreshes or closes the page. ```ts import { createLogger, downloadBlob, exportLogsToZip, indexedDbTransport, } from "@loggerjs/browser"; import { privacyGuardProcessor, redactProcessor } from "@loggerjs/processors"; const supportStore = indexedDbTransport({ name: "support-indexeddb", dbName: "checkout-web-support-logs", storeName: "support-logs", maxEntries: 20_000, maxBytes: 25 * 1024 * 1024, ttlMs: 7 * 24 * 60 * 60 * 1000, batchSize: 50, flushIntervalMs: 1000, durability: "relaxed", localStorageSpill: { namespace: "checkout-support-logs", maxEntries: 200, maxBytes: 512 * 1024, minLevel: "info", }, }); export const supportLogger = createLogger({ category: ["web"], level: "info", processors: [ redactProcessor({ keys: ["password", "token", "authorization", "cookie", /secret/i], }), privacyGuardProcessor({ maxStringLength: 8192 }), ], transports: [supportStore], }); export async function downloadSupportLogZip() { await supportLogger.flush(); const zip = await exportLogsToZip(supportStore, { groupBySession: true, includeRecent: { maxEvents: 500 }, query: { from: Date.now() - 7 * 24 * 60 * 60 * 1000, order: "asc", }, source: "indexeddb", }); downloadBlob(zip, `checkout-logs-${new Date().toISOString().replace(/[:.]/g, "-")}.zip`); } ``` Production notes: - Keep privacy processors before the IndexedDB transport. Anything persisted locally can be exported by a user or support flow. - `indexedDbTransport()` creates a page-session id by default and stores it in both the IndexedDB entry metadata and `event.context.sessionId` when absent. If your app already owns session ids, pass `session: { id, getId, contextKey }`. - `localStorageSpill` is bounded and best effort. It improves normal reload and close behavior but cannot protect against process kill, crash, disabled storage, quota exhaustion, or storage eviction. ## Node to Stdout Plus OTLP Use stdout as the local, platform-native sink and OTLP as the remote observability path. Stdout remains useful for container runtimes and fatal events even if the OTLP endpoint is degraded. ```ts import * as otelApi from "@opentelemetry/api"; import { captureProcessIntegration, createLogger, installAsyncLocalStorageContext, stdoutTransport, } from "@loggerjs/node"; import { openTelemetryTraceProcessor, otlpHttpTransport } from "@loggerjs/otel"; import { redactProcessor } from "@loggerjs/processors"; installAsyncLocalStorageContext(); export const logger = createLogger({ category: ["api"], level: "info", tags: { service: "checkout-api", env: process.env.NODE_ENV ?? "production", runtime: "node", }, processors: [ openTelemetryTraceProcessor({ api: otelApi }), redactProcessor({ keys: ["password", "token", "authorization", "cookie", /secret/i], }), ], transports: [ stdoutTransport({ name: "stdout", minLength: 4096, }), otlpHttpTransport({ name: "otlp", url: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ?? "http://localhost:4318/v1/logs", headers: process.env.OTEL_EXPORTER_OTLP_AUTHORIZATION ? { authorization: process.env.OTEL_EXPORTER_OTLP_AUTHORIZATION } : undefined, resource: { "service.name": "checkout-api", "deployment.environment": process.env.NODE_ENV ?? "production", }, maxRecords: 100, maxWaitMs: 2000, maxQueueSize: 5000, maxRetries: 3, circuitBreakerFailureThreshold: 5, circuitBreakerResetMs: 30000, }), ], integrations: [ captureProcessIntegration({ exitOnUncaught: true, flushTimeoutMs: 500, }), ], }); export async function closeLogger() { await logger.close(); } ``` Production notes: - Keep at least one local sink (`stdoutTransport()` or `fileTransport()`) for fatal process paths. Remote OTLP should not be the only crash-path sink. - Install `@opentelemetry/api` and initialize tracing before constructing the logger when you want active span correlation. - Use your deployment platform's graceful shutdown hook to call `logger.close()`. ## Full Stack to Loki and Datadog Use this when browser and server logs should land in the same vendor backends. The browser sends logs to your own collector; the server owns Loki and Datadog credentials and forwards both server-side events and accepted browser batches. ```ts import { batchTransport, createLogger, recordToEvent, type LogEvent, type Transport, type TransportContext, } from "@loggerjs/core"; import { datadogLogsTransport } from "@loggerjs/datadog"; import { lokiTransport } from "@loggerjs/loki"; import { redactProcessor } from "@loggerjs/processors"; const service = "checkout"; const env = process.env.NODE_ENV ?? "production"; function reliableVendorTransport(transport: Transport): Transport { return batchTransport(transport, { maxRecords: 100, maxWaitMs: 2000, maxQueueSize: 10000, dropPolicy: "drop-oldest", maxRetries: 3, retryBaseDelayMs: 250, retryMaxDelayMs: 5000, circuitBreakerFailureThreshold: 5, circuitBreakerResetMs: 30000, }); } const vendorTransports = [ reliableVendorTransport( lokiTransport({ url: process.env.LOKI_URL ?? "http://localhost:3100/loki/api/v1/push", tenantId: process.env.LOKI_TENANT_ID, labels: { service, env }, labelTags: ["runtime"], structuredMetadata: true, }), ), reliableVendorTransport( datadogLogsTransport({ apiKey: process.env.DD_API_KEY, site: process.env.DD_SITE ?? "datadoghq.com", service, source: "loggerjs", tags: { env }, eventTagKeys: ["runtime"], }), ), ]; export const serverLogger = createLogger({ category: ["api"], level: "info", tags: { service, env, runtime: "node" }, processors: [ redactProcessor({ keys: ["password", "token", "authorization", "cookie", /secret/i], }), ], transports: vendorTransports, }); const collectorContext: TransportContext = { loggerName: "browser-log-collector", now: () => Date.now(), toEvent: recordToEvent, reportInternalError(error, detail) { serverLogger.warn("browser log collector failed", { error, detail }); }, }; export async function forwardBrowserLogs(events: LogEvent[]) { for (const transport of vendorTransports) { if (transport.logBatch) await transport.logBatch(events, collectorContext); else { for (const event of events) await transport.log?.(event, collectorContext); } } } ``` Production notes: - Validate and bound the `/api/logs` request body before calling `forwardBrowserLogs()`. Reject oversized batches early. - Promote only low-cardinality fields to Loki labels and Datadog tags. Keep user ids, request ids, order ids, and URLs in structured metadata/data. - Apply the same redaction policy in the browser and the server collector. Treat browser-submitted logs as untrusted input. --- # Operations Source: https://github.com/jskits/loggerjs/blob/main/docs/OPERATIONS.md # Operations This guide covers the parts of LoggerJS that most affect production behavior: privacy, browser buffering, crash paths, and remote delivery reliability. ## Privacy Automatic integrations are opt-in. Enable only the capture surfaces your product needs. Recommended defaults: - Use `redactProcessor()` before any remote transport. - Allowlist HTTP headers in fetch/XHR integrations. Do not ship cookies, authorization headers, or full request bodies by default. - Sanitize URLs when query strings may contain tokens or user data. - Keep console capture to `warn` and `error` unless debug collection is explicitly needed. - Prefer stable tags such as `service`, `env`, and `runtime`; put high-cardinality values in event data, not tags. Example: ```ts import { captureFetchIntegration } from "@loggerjs/browser"; import { redactProcessor } from "@loggerjs/processors"; const processors = [redactProcessor({ keys: ["password", "token", /secret/i] })]; const integrations = [ captureFetchIntegration({ captureRequestHeaders: ["content-type", "x-request-id"], captureResponseHeaders: ["content-type", "x-request-id"], sanitizeUrl: (url) => new URL(url, location.origin).origin, }), ]; ``` `redactProcessor()` masks by key, exact dot path, regex, or custom matcher. Paths are relative to each redacted event field (`user.password`, not `data.user.password`). Use `replacement` (or the Pino-compatible `censor` alias) to mask values, and `remove: true` when the field should be omitted from the event. LoggerJS does not compile redaction paths with `eval` or `new Function`; wildcard-like regexes and deep traversal are safer than generated code but cost more than exact keys and paths. `privacyGuardProcessor()` is broader: it scans selected fields for built-in and custom string patterns such as emails, bearer tokens, and card-like values. Use it as a safety net, not as a replacement for capture allowlists. ## Browser Queue And Offline Replay `browserHttpTransport()` batches records in memory and can persist failed payloads into an offline queue adapter. The built-in memory queue is intentionally small and dependency-free; applications that need reload survival should provide an IndexedDB-backed adapter with the same interface. ```ts import { browserHttpTransport, memoryBrowserHttpOfflineQueue } from "@loggerjs/browser"; const transport = browserHttpTransport({ url: "/api/logs", maxBatchSize: 50, maxQueueSize: 1000, offlineQueue: memoryBrowserHttpOfflineQueue({ maxEntries: 500 }), useBeaconOnPageHide: true, beaconMaxBytes: 60 * 1024, }); ``` When the browser fires `online`, the transport replays stored payloads with retry and backoff. Page lifecycle integration should be enabled when logs matter during tab close or navigation: ```ts import { pageLifecycleIntegration } from "@loggerjs/browser"; const integrations = [pageLifecycleIntegration()]; ``` Browser storage and shutdown behavior are still best effort. `sendBeacon` can be size-limited or skipped during shutdown, in-memory queues disappear on reload, and IndexedDB can be unavailable, full, evicted, or blocked by an upgrade. For production browser delivery, combine: - `browserHttpTransport()` for normal remote delivery. - `indexedDbBrowserHttpOfflineQueue()` or `offlineFirstTransport()` for reload-surviving replay. - `pageLifecycleIntegration()` and `useBeaconOnPageHide` for last-chance flush. - Drop/queue metrics from logger meta so quota or backpressure is visible. ## Node Crash Path For process-level failures, combine `captureProcessIntegration()` with a transport that can flush synchronously when needed. ```ts import { captureProcessIntegration, fileTransport, stdoutTransport } from "@loggerjs/node"; const transports = [ stdoutTransport(), fileTransport({ path: "./logs/app.ndjson" }), ]; const integrations = [captureProcessIntegration({ exitOnUncaught: true })]; ``` Crash-path guidance: - Keep at least one local transport for fatal process events. - Prefer `flushSync()` for final synchronous shutdown when the transport supports it; use `await flush()` for normal drain-and-continue shutdowns. - Use `fileTransport({ sync: true })` when every write must reach the filesystem before the log call returns. - Use HTTP/OTLP remote transports for normal delivery, not as the only fatal-path sink. - Keep processor work synchronous and bounded; crash handlers should not perform slow enrichment. For `uncaughtException` with `exitOnUncaught: true`, the sequence is: 1. Capture a `fatal` record with `process.kind: "uncaughtException"`. 2. Call `flushSync()` on sync-capable transports. 3. Run a bounded async `flush()` race controlled by `flushTimeoutMs` (default `250` ms). 4. Exit with code `1`. For signals with `exitOnSignal: true`, LoggerJS captures a fatal signal record, uses the same sync-plus-bounded-async flush sequence, then exits with the signal exit code when known (`SIGTERM` -> `143`, `SIGINT` -> `130`). ## Remote Transport Reliability Batch-based transports support the same core reliability options: ```ts { maxRecords: 100, maxBytes: 64 * 1024, maxWaitMs: 2000, concurrency: 2, maxRetries: 3, retryBaseDelayMs: 250, retryMaxDelayMs: 5000, circuitBreakerFailureThreshold: 5, circuitBreakerResetMs: 30000, } ``` Use byte limits when payload size matters more than event count. Use `onDrop` to surface queue drops into your own metrics pipeline. ## Context And Trace Correlation Use explicit child context for values known at construction time and ambient context for request-scoped values: ```ts import { installAsyncLocalStorageContext } from "@loggerjs/node"; import { withContext } from "@loggerjs/core"; installAsyncLocalStorageContext(); await withContext({ requestId: "req_123" }, async () => { logger.info("request started"); }); ``` Use `openTelemetryTraceProcessor()` to attach the active OpenTelemetry span context when an OpenTelemetry API object is available. --- # Performance Source: https://github.com/jskits/loggerjs/blob/main/docs/PERFORMANCE.md # Performance Guide This page is the user-facing companion to [BENCHMARKS.md](BENCHMARKS.md) (measured numbers), [BENCHMARK-MATRIX.md](BENCHMARK-MATRIX.md) (checked-in machine evidence), and the Performance Budget section of [ARCHITECTURE.md](ARCHITECTURE.md) (targets and decisions). It tells you how to configure LoggerJS for throughput and which habits keep the hot path hot. Reference numbers (Apple M1 Max, Node v22.21.1 — see [BENCHMARKS.md](BENCHMARKS.md) for methodology and [BENCHMARK-MATRIX.md](BENCHMARK-MATRIX.md) for the checked-in row). The loggerjs-vs-pino figures come from the paired A/B harness; ranking vs pino is CPU/Node-V8 dependent — reproduce with `BENCH_AB=1 pnpm bench:node`: | Path | Cost | | --- | ---: | | Disabled level call | ~3 ns (pino parity) | | Enabled pipeline, record fast path, noop sink | ~83 ns | | Batch transport enqueue (default settings) | ~172 ns | | Prepared lean NDJSON line to a sink | ~224 ns (1.28× pino) | | Lean NDJSON line to a sink | ~242 ns (1.19× pino) | | Full NDJSON line with id/seq/levelName | ~307 ns | ## Free Wins (Defaults Already Do This) - **Disabled levels cost one comparison.** Leave `trace`/`debug` calls in your code; gate with `level`. - **Lazy messages** are only evaluated when the level is enabled, at most once: `logger.debug(() => expensive())`. - **Logger tags are frozen and shared** across records — no per-call copy. - **Default ids memoize** their timestamp segment per millisecond. - **Batch byte estimation is skipped** unless you set a finite `maxBytes`. - **`ndjsonCodec` runs the native fast path** by default with a safe fallback for inputs that would throw. ## The Record Fast Path The single biggest configuration lever. When a logger has **zero processors** and its transports are **record-aware** (`write`/`writeBatch`), no `LogEvent` is ever built: no id factory, no message-error projection, no second object. ```ts // Fast path: middleware + record-aware transport createLogger({ middleware: [tagsMiddleware({ service: "checkout" })], // middleware keep the fast path transports: [recordAwareTransport], }); // Leaves the fast path: any processor forces event projection per log createLogger({ processors: [sampleProcessor()], transports: [recordAwareTransport], }); ``` Practical guidance: - Prefer the middleware variants (`tagsMiddleware`, `enrichMiddleware`, `traceContextMiddleware`, …) over their processor twins when both exist. - Processors are still the right tool for event-shape behavior (routing, fingerprinting, fingers-crossed). Accept the projection cost when you need them — it is ~100ns, not a catastrophe. ## Codec Choice - Highest throughput: `fastEventJsonCodec()` from `@loggerjs/codecs`, optionally with the lean envelope (`includeId/includeSeq/includeLevelName: false`) when downstream does not need those fields. - `ndjsonCodec()` (the stdout default) is within ~10% of fast-event-json on the event path. - Prepared record encoders help custom sinks. When a record-aware transport writes a codec directly, wrap the codec once with `createPreparedRecordEncoder(codec)` so codec-owned logger/tag fragments can be reused without moving serialization into the logger. - `safeJsonCodec()` pays a full normalization walk per item — use it where hostile payloads are routine, not as the throughput path. - Custom `idFactory` (UUIDs etc.) costs per-log; the default id is near-free and sortable. ## Batching for Remote Destinations Per-event network calls are the dominant real-world cost; every remote transport here is built on `batchTransport`: - `maxRecords` / `maxWaitMs` trade latency for batch size; defaults (50 / 2000ms) suit most services. - Set `maxBytes` only when the destination enforces payload limits — enabling it turns on per-log byte estimation. - `concurrency: 2..4` overlaps slow endpoint round trips. - Watch `getLoggerMetaStats()` for `transport.dropped.*` — drops mean the queue bound and your traffic disagree. ## Habits That Hurt - **Heavy synchronous work in middleware/processors.** The pipeline is synchronous by design; a 1ms enrichment makes every log 1ms. - **Pre-stringifying in the pipeline.** Serialization belongs to the transport codec; stringified blobs also defeat redaction. - **Logging through one shared catch-all logger with many processors** when only one route needs them — split loggers by purpose; children are cheap. - **Unbounded data payloads.** Encoding cost is proportional to payload size; log identifiers, not entire entities. ## Import Boundaries The root `@loggerjs/browser` and `@loggerjs/node` entries are preset-style convenience imports: they re-export core plus every first-party runtime transport and integration. Use them when application simplicity matters more than the smallest possible module graph. For tighter bundles, import the documented subpaths. Browser and Node subpaths are built as physical entry bundles and verified by `pnpm verify:entry-boundaries`, so a focused import does not point back at the aggregate `dist/index` file: ```ts import { browserHttpTransport } from "@loggerjs/browser/transport-http"; import { captureFetchIntegration } from "@loggerjs/browser/integration-fetch"; import { stdoutTransport } from "@loggerjs/node/transport-stdout"; ``` Keep new runtime-specific features behind a subpath entry when they are not part of the common preset path. If a new feature makes the root browser/node bundle larger, the size-budget diff should explain why the preset entry needs it. ## Guardrails Performance is gated in CI: `pnpm bench:gate` runs interleaved A/B suites and enforces paired ratios against the matching pino baseline (see BENCHMARKS.md). If you contribute changes to the hot path, run it locally; structural regressions fail the pull request. The deliberate end-state of optimization is documented in ARCHITECTURE.md: keep the shared `LogRecord` pipeline as the default architecture, but allow codec/transport-owned preparation for stable fragments. Fusion paths that bypass the record remain rejected as the default because they would create a separate semantic hot path. --- # Benchmarks Source: https://github.com/jskits/loggerjs/blob/main/docs/BENCHMARKS.md # Benchmarks LoggerJS benchmarks are intentionally simple and reproducible. They measure public package builds from `dist`, not TypeScript source. ## Commands ```bash pnpm bench pnpm bench:node pnpm bench:browser pnpm bench:gate pnpm bench:matrix -- --runs=5 --rounds=120 --label="$(hostname)-node22" pnpm bench:matrix:aggregate -- benchmarks/matrix --out docs/BENCHMARK-MATRIX.md pnpm size:check ``` `pnpm bench:gate` runs interleaved A/B suites and enforces regression limits as paired per-round ratios against the matching pino baseline. It uses the same drift-canceling method as `BENCH_AB`, so CPU frequency, scheduler placement, and GC pauses affect each contender in the same round. Limits live in `scripts/check-bench-regression.mjs` and are generous on purpose: they catch structural regressions, not noise. CI runs the gate on every pull request. Tune the gate with `BENCH_GATE_AB_ROUNDS`, `BENCH_GATE_AB_BATCH`, and `BENCH_GATE_AB_WARMUP`. `pnpm bench` builds the workspace first, then runs Node and browser benchmarks. Browser benchmarks use a local headless Chrome binary. Set `CHROME_BIN` when Chrome is not installed in a standard location. ### Apples-to-apples cross-logger ratios (`BENCH_AB`) The normal suite times each logger **once**, at a different point in the run, so its loggerjs-vs-pino ratio drifts with CPU frequency scaling and P/E-core scheduling — a single sequential run can make either logger look better purely by *when* it was measured. To compare two loggers fairly, use the interleaved A/B mode: ```bash BENCH_AB=1 node scripts/bench-node.mjs # tune: BENCH_AB_ROUNDS (default 60), BENCH_AB_BATCH (5000), BENCH_AB_WARMUP (100000) BENCH_AB=1 BENCH_JSON=1 node scripts/bench-node.mjs # machine-readable ``` Each round times every contender in the selected suite **back-to-back** and rotates the start position, so drift hits them equally and cancels in the **paired per-round ratio**. The default suite compares pino, lean, prepared, and the full-envelope record sink; `BENCH_AB_SUITE=disabled` and `BENCH_AB_SUITE=enqueue` are used by the CI gate. The report prints per-contender ns/op plus the median ratio with its min/max spread, and warns when the baseline spread exceeds 25% — the signal that the machine is too noisy to trust the absolute ns (the ratios stay fair regardless). Quote a cross-logger ratio only from this mode with a stable baseline, never from a single sequential run. ### Cross-machine benchmark matrix When you need to support a stronger statement such as "LoggerJS was faster than pino on every machine we tested," collect multiple local A/B artifacts and aggregate them: ```bash pnpm build pnpm bench:matrix -- --runs=5 --rounds=120 --label="$(hostname)-node22" # after copying artifacts from other machines into benchmarks/matrix/ pnpm bench:matrix:aggregate -- benchmarks/matrix --out docs/BENCHMARK-MATRIX.md ``` `pnpm bench:matrix` wraps the `BENCH_AB=1 BENCH_JSON=1` harness, runs it several times, records CPU/OS/Node/dependency/Git metadata, and writes JSON plus Markdown artifacts under `benchmarks/matrix/` by default. That directory is ignored because it is local evidence. Commit only an intentionally curated aggregate such as [BENCHMARK-MATRIX.md](BENCHMARK-MATRIX.md). The checked-in matrix is the evidence file to cite when making cross-machine performance statements. For non-Apple-Silicon and multi-Node evidence, run the manual GitHub Actions workflow `Benchmark Matrix`. It collects Linux x64 rows for Node 20.19.0, 22, and 24, then uploads an aggregate Markdown artifact for review. Commit the aggregate only after verifying the JSON artifacts and runner metadata. Use the matrix wording carefully: it can prove the listed machine/runtime/dependency combinations, not a universal result for every future CPU, Node/V8 version, or pino release. ## Node Scenarios - Disabled debug log with a lazy message. - Enabled logger with no transports. - Enabled logger with a no-op transport. - Enabled logger with a record-aware no-op write transport (record fast path, no event projection). - Console transport with a no-op patched console. - Batch transport enqueue path. - Full-path NDJSON comparison against pino, winston, LogTape, and Node console (see below). - JSON, safe JSON, fast event JSON, and msgpackr encode/decode. - Fast event JSON encoding raw LogRecord batches (record transport boundary). ## Competitor Comparison The full-path scenarios log one structured info call per iteration and hand the serialized line to a discarding sink, so they compare pipeline plus serialization without terminal or filesystem I/O noise. pino, winston, and LogTape are dev dependencies pinned in the root lockfile. The Node console scenario uses a real `Console` instance backed by a discarding stream. Reference machine: **Apple M1 Max (64 GB), Node v22.21.1**, pino 10.3.1, winston 3.19.0, LogTape 2.1.3. The loggerjs-vs-pino rows come from the drift-canceling paired A/B harness (`BENCH_AB`, 22 runs x 120 rounds); the broader landscape is a single `BENCH_ITERATIONS=1000000` sequential run. ### Cross-logger comparison (paired A/B — the trustworthy method) Each round times pino, lean, and prepared back-to-back, so CPU frequency and core scheduling hit them equally and cancel in the ratio (see the `BENCH_AB` note above). Medians over 22 runs: | Path | ns/op | vs pino | | --- | ---: | --- | | pino ndjson noop sink | 287 | 1.00x baseline | | loggerjs lean record sink | 242 | **1.19x pino** (paired ratio 0.84, range 0.82-0.87) | | loggerjs prepared lean record sink | 224 | **1.28x pino** (paired ratio 0.78) | On this machine loggerjs lean and prepared are **faster than pino** for equivalent output, reproducibly: the paired lean/pino ratio stayed 0.84 +/- 0.02 across all 22 runs, and held even on rounds where a GC pause pushed the absolute spread past 80%. The prepared encoder is ~8% faster than plain lean. **This ranking is environment-dependent.** pino and loggerjs both use hand-tuned JSON hot paths, and small CPU, scheduler, and Node/V8 differences can change which one wins. The table above is an empirical result for the listed reference machine, not a mechanism claim or a universal ranking. Always reproduce on your own hardware: `BENCH_AB=1 pnpm bench:node`, then add durable rows with `pnpm bench:matrix`. ### Sequential suite (single 1,000,000-iteration run, same machine) Absolute per-scenario throughput. Cross-logger ratios here are **not** reliable (each logger is timed at a different point in the run) — use the A/B table above for loggerjs-vs-pino. This table is for the order-of-magnitude landscape and the codec paths. | Scenario | ns/op | | --- | ---: | | loggerjs disabled debug (lazy message) | 3 | | pino disabled debug | 9 | | loggerjs batch transport enqueue | 172 | | loggerjs prepared lean record sink | 252 | | loggerjs lean record sink | 273 | | loggerjs full-envelope record sink (`+id/seq/levelName`) | 307 | | loggerjs ndjson event sink | 812 | | loggerjs fast-event-json event sink | 897 | | node console info noop stream | 769 | | winston json noop sink | 2,726 | | logtape json lines noop sink | 6,584 | All loggerjs and pino full-path loggers carry the same base fields (`service`, `env`). The lean sink uses `fastEventJsonCodec({ includeId: false, includeSeq: false, includeLevelName: false })`; the prepared lean sink wraps it with `createPreparedRecordEncoder(codec)` to reuse codec-owned logger/tags fragments without moving serialization into the logger; the full-envelope sink additionally emits `id`, `seq`, and `levelName`. The CI-enforced figures are the **paired A/B ratios** in `pnpm bench:gate` (default 60 rounds x 5000 ops per contender). The gate covers disabled-level logging, record-write enqueue, batch enqueue, lean, prepared, and full-envelope record sinks. Honest read: - Disabled-level logging is at parity with pino (both single-digit ns). - For equivalent lean output, loggerjs is **faster than pino on the M1 Max reference machine** (paired A/B, lean 1.19x / prepared 1.28x) — but the ranking is CPU/V8-dependent, so treat it as "in pino's class, machine- dependent winner," not a universal claim. The prepared encoder adds ~8%. - The full-envelope path costs ~13% more than lean to carry `id`, `seq`, and `levelName`; choose the lean envelope when downstream does not need them. - loggerjs is roughly an order of magnitude faster than winston (~10x) and LogTape (~24x), and ~3x faster than Node console; these multiples swing with system load, so treat them as approximate. - An earlier snapshot showed pino at 442ns in the mixed suite; that was a JIT warmup artifact (10k warmup iterations), fixed by warming each scenario with a quarter of the measured iterations. Treat cross-logger comparisons as invalid unless warmup is proportionate. Re-run `pnpm bench:node` after hot-path changes and update this snapshot when the numbers move materially. Tune iteration counts with: ```bash BENCH_ITERATIONS=200000 pnpm bench:node BENCH_BROWSER_ITERATIONS=100000 pnpm bench:browser BENCH_BROWSER_IDB_ITERATIONS=5000 pnpm bench:browser ``` ## Browser Scenarios `pnpm bench:browser` runs in a local headless Chrome and measures browser-facing paths from the built `dist` packages: - Enabled browser logger with no transports. - Browser HTTP transport enqueue with a no-op `fetchFn`. - IndexedDB transport enqueue into the in-memory transport buffer. - JSON and fast event JSON encoding for browser batches. - IndexedDB transport flush of a persisted batch. - IndexedDB HTTP offline queue enqueue. The IndexedDB scenarios use a separate iteration count because they exercise real browser storage I/O. Tune it with `BENCH_BROWSER_IDB_ITERATIONS`; the default is intentionally smaller than `BENCH_BROWSER_ITERATIONS` so routine browser benchmark runs remain fast. Browser storage numbers are sensitive to Chrome version, profile state, device storage, private browsing policy, quota, and Storage Buckets support, so cite them only with the measured browser and hardware context. ## Size Budgets `pnpm size:check` runs after build and enforces raw plus gzip budgets for each package entry bundle. Budgets are stored in `scripts/check-size-budgets.mjs` and should be updated only with an intentional public surface or implementation-size change. --- # Benchmark Matrix Source: https://github.com/jskits/loggerjs/blob/main/docs/BENCHMARK-MATRIX.md # LoggerJS Benchmark Matrix Last updated: 2026-06-18 (two machine rows hand-merged; per-machine JSON artifacts live in the gitignored `benchmarks/matrix/`, so the aggregate command cannot regenerate a row whose artifact is not present locally). This table is the checked-in evidence matrix for loggerjs-vs-pino Node hot-path claims. Ratios are paired per-round latency medians from the interleaved A/B harness, not one-off sequential-run ratios. A ratio below `1.00x` means the LoggerJS path had lower latency than pino on that machine. **The two rows below deliberately disagree — that is the point:** the ranking is CPU/V8-dependent (LoggerJS faster on M1 Max, pino faster on M4 Pro), so no universal "faster than pino" claim is supportable. | Label | Platform | CPU | Node | Git | Runs | Pino ns | Lean ns | Prepared ns | Lean / pino | Prepared / pino | Result | | ----------------- | ------------ | ------------ | -------- | ------------ | ---: | ------: | ------: | ----------: | --------------: | --------------: | ------------------------------- | | macbookpro-node22 | darwin/arm64 | Apple M1 Max | v22.21.1 | 5d7e4e3bf423 | 5 | 286 | 244 | 223 | 0.843x (118.6%) | 0.773x (129.3%) | LoggerJS lean + prepared faster | | m4pro-node22 | darwin/arm64 | Apple M4 Pro | v22.22.2 | 1d3d51c21f86 | 6 | 197 | 223 | 211 | 1.137x (87.9%) | 1.054x (94.9%) | pino faster (lean + prepared) | ## Row Details | Label | Memory | Dependencies | Sampling | Baseline spread | Prepared / lean | | ----------------- | -----: | ------------------------------------------ | -------------------------------------------- | --------------: | --------------: | | macbookpro-node22 | 64 GB | pino 10.3.1, winston 3.19.0, LogTape 2.1.3 | 5 runs, 120 rounds x 5000 ops, 100000 warmup | 21.2% | 0.919x (108.8%) | | m4pro-node22 | 24 GB | pino 10.3.1, winston 3.19.0, LogTape 2.1.5 | 6 runs, 120 rounds x 5000 ops, 100000 warmup | 41.9% | 0.942x (106.1%) | ## Evidence Coverage | Requirement | Status | Rows | | --- | --- | --- | | At least one non-Apple-Silicon runtime | Missing | darwin/arm64 | | At least two Node major versions | Missing | 22 | Both gates remain **Missing**: the M4 Pro row is a second Apple-Silicon CPU on Node 22, so it does not satisfy the non-Apple or second-Node-major requirements. It does, however, already make the universal-claim guard concrete — within the same OS/arch/Node-major, swapping only the CPU (M1 Max → M4 Pro) flips the result from "LoggerJS faster" to "pino faster". Until a non-Apple and a second Node-major row are added, keep performance wording CPU-scoped and never frame it as "LoggerJS is always faster than pino". ## Reproduce ```bash pnpm build pnpm bench:matrix -- --runs=5 --rounds=120 --label="$(hostname)-node22" # after copying artifacts from other machines into benchmarks/matrix/ pnpm bench:matrix:aggregate -- benchmarks/matrix --out docs/BENCHMARK-MATRIX.md ``` For non-Apple-Silicon and multi-Node evidence, run the manual GitHub Actions workflow: ```bash gh workflow run benchmark-matrix.yml -f runs=5 -f rounds=120 -f batch=5000 -f warmup=100000 ``` Download the `benchmark-matrix-aggregate` artifact, review the generated `benchmark-matrix-ci.md`, and commit it to this file only if the rows are from the intended machine/runtime combinations. Do not hand-write benchmark rows without the matching JSON artifacts. Notes: - The matrix proves only the listed machine/runtime/dependency combinations. Do not turn it into a universal "always faster than pino" claim. - Add new rows when testing new CPUs, operating systems, Node/V8 versions, or pino releases. - If a row is captured from a dirty worktree, mark the Git column with `*`. --- # Comparison Source: https://github.com/jskits/loggerjs/blob/main/docs/COMPARISON.md # LoggerJS Compared With Other JavaScript Loggers This page compares the current LoggerJS workspace with common JavaScript logging libraries. It is written from the current repository state, not from a target roadmap. ## Scope The comparison uses first-party behavior unless a cell explicitly says "ecosystem". Sources were checked on 2026-06-12: - LoggerJS repository docs: [README](https://github.com/jskits/loggerjs/blob/main/README.md), [Concepts](CONCEPTS.md), [Transports](TRANSPORTS.md), [Integrations](INTEGRATIONS.md), [Processors](PROCESSORS.md), [Codecs](CODECS.md), [Benchmarks](BENCHMARKS.md). - Pino official docs: , , , , . - Winston official README: . - LogTape official docs and JSR package page: , , , , . - Bunyan official README: . - Lightweight and developer-experience tools: , , , . The benchmark numbers below are only for the scenarios in [BENCHMARKS.md](BENCHMARKS.md). They do not claim universal superiority across all sinks, runtimes, payload shapes, or third-party transports. ## Short Answer LoggerJS is best when the logging problem spans browser and server collection: automatic integrations, structured middleware, reliable transport delivery, offline browser persistence, codec choice per destination, and vendor/DB/OTLP delivery from one mental model. Its most defensible niche is **vendor-neutral, self-hosted delivery** — logs go to destinations you own (HTTP, files, your DB, Loki/Elasticsearch, OTLP), from a zero-dependency core that runs under strict CSP, on edge/Workers, and offline — where a Node-only logger or a managed APM SaaS is a poorer fit. Pino is still the mature default when the main requirement is a minimal, Node-first JSON logger with a large ecosystem. On the current M1 Max reference benchmark, LoggerJS's equivalent lean/prepared paths are faster, but the ranking is CPU/Node-V8 dependent. Winston is still the mature, flexible Node transport and format ecosystem. LogTape is the closest architectural peer for library-first usage and multi-runtime categories. Bunyan is a stable legacy JSON logger for Node services. ## At A Glance Legend: ✅ first-party fit, 🧩 ecosystem fit, ⚠️ partial or depends on the chosen configuration, ❌ no checked first-party equivalent, 📊 measured in this repo. | Capability | LoggerJS | Pino | Winston | LogTape | Bunyan | | ------------------------------- | ------------------------------ | --------------------------------------------------- | ------------------------ | --------------------- | --------------------- | | Node server logging | ✅ first-party | ✅ first-party | ✅ first-party | ✅ first-party | ✅ first-party | | Browser runtime | ✅ first-party | ⚠️ browser API | ⚠️ not primary | ✅ first-party | ⚠️ bundler support | | Library-safe default | ✅ silent until configured | ⚠️ app-oriented | ⚠️ app-oriented | ✅ core design | ⚠️ app-oriented | | Automatic browser capture | ✅ 19 first-party integrations | ❌ none checked | ❌ none checked | ❌ none checked | ❌ none checked | | Automatic Node collection | ✅ 16 first-party integrations | 🧩 ecosystem | ⚠️ exceptions/rejections | ✅ framework packages | ⚠️ stream/custom | | Multi-destination delivery | ✅ transports | ✅ transports | ✅ transports | ✅ sinks | ✅ streams | | Built-in batching/retry/offline | ✅ shared primitives | ⚠️ transport-dependent | ⚠️ transport-dependent | ⚠️ sink-dependent | ⚠️ stream-dependent | | Transport-owned codecs | ✅ explicit boundary | ⚠️ logger/transport formatting | ⚠️ format pipeline | ⚠️ sink formatting | ⚠️ serializers | | Privacy/redaction | ✅ processors + sanitizers | ✅ built-in redaction | ⚠️ custom formats | ✅ redaction package | ⚠️ serializers/custom | | Direct Node JSON path | ✅ 1.19× pino on M1 | 📊 baseline(same class; can lead on other CPUs/V8s) | ❌ slower measured | ❌ slower measured | Not measured here | ## Detailed Matrix | Axis | LoggerJS | Pino | Winston | LogTape | Bunyan | | ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | Primary fit | Isomorphic structured logging with automatic collection and reliable delivery | Low-overhead Node JSON logging | Flexible Node logger with mature format/transport model | Library-first structured logging across JS runtimes | Simple JSON logging for Node services | | Runtime posture | `@loggerjs/core` is platform-neutral; first-party Node and browser packages are split by runtime | Node-first with a documented browser API | Node-first; browser use is not the primary documented path | First-party support for Node, Deno, Bun, browsers, Cloudflare Workers, and edge | Node services; docs mention Browserify/Webpack/NW.js support | | Library-safe default | Yes: `getLogger()` is silent until the host configures logging | Partial: libraries can accept/inject a logger, but Pino itself is app-oriented | Partial: libraries can accept/inject a logger, but Winston itself is app-oriented | Yes: a core design goal | Partial: child loggers help, but app-level configuration is expected | | Structured data | Yes: records/events preserve message, data, context, tags, trace, source, type | Yes: JSON logs by default | Yes: mutable `info` objects plus formats | Yes: structured log records/properties | Yes: JSON records | | Levels | Pino-compatible numeric levels plus names | Built-in numeric levels and custom levels | RFC5424-style levels plus custom levels | Severity levels with category configuration | Numeric levels | | Category/logger model | Category arrays, child loggers, registry configuration | Child loggers and bindings | Logger instances, child loggers, containers | Hierarchical categories with sink inheritance | Logger name plus child loggers; docs say names are not hierarchical | | Middleware/filter layer | First-party middleware and processors for enrich, redact, sample, dedupe, route, rate-limit, fingerprint, normalize | Hooks, serializers, mixins, redaction; broader middleware is usually app/ecosystem code | Format chains and custom formats; mutable object pipeline | Filters, contexts, formatters, redaction package | Serializers and custom streams | | Serialization ownership | Codec belongs to each transport; built-ins include JSON, safe JSON, NDJSON, fast-event-json, msgpackr, OTLP JSON | Core JSON serialization with serializers/formatters and transport output | Format chain finalizes output per logger/transport | Sinks and formatters own output | JSON records plus serializers | | Transport/sink model | First-party console, pretty DevTools/terminal output, memory, test, batch, stdout/stderr, file, rotating file, HTTP, syslog, worker, browser HTTP, IndexedDB, WebSocket, service worker, BroadcastChannel, OTLP, Sentry, Datadog, Elasticsearch, Loki, CloudWatch, SQLite/Postgres/custom DB | Destination/transport API, multi-target transports, `pino/file`, `pino-pretty`, and ecosystem transports | Built-in console/file/http/stream-style transports and broad custom transport ecosystem | Sinks with console/stream in core and packages for file, OTEL, Sentry, syslog, CloudWatch, Windows Event Log, and more | Streams for stdout/file/rotation/raw/custom | | Automatic browser collection | 19 first-party browser/frontend integrations: console, script/resource errors, unhandled rejection, fetch, XHR, Web Vitals, performance entries, user actions, router adapters, ReportingObserver, service worker, WebSocket, framework error hooks, runtime host, and browser context propagation | Browser API exists for direct logging; no checked first-party equivalent to the LoggerJS browser capture suite | No checked first-party equivalent to the LoggerJS browser capture suite | Browser runtime support; checked docs do not show an equivalent browser capture/offline suite | No checked first-party equivalent | | Automatic Node collection | 16 first-party Node.js/server integrations: process, diagnostics_channel, Express, Fastify, Koa, Hapi, Nest middleware, fetch, http client, CLI, serverless, queue, BullMQ, Prisma, Redis, and generic DB clients | Ecosystem integrations such as Fastify/Pino and pino-http are common; core docs cover logger/transports | Built-in uncaught exception and unhandled rejection handling; framework request logging is usually ecosystem code | First-party framework integration packages include Express, Fastify, Hono, Koa, and Drizzle | No broad first-party instrumentation suite in checked docs | | Browser persistence/export | First-party IndexedDB transport, IndexedDB HTTP offline queue, pagehide flush, ZIP export | No checked first-party equivalent | No checked first-party equivalent | No checked first-party equivalent in the checked core docs | No checked first-party equivalent | | Delivery reliability | Shared batching, retry/backoff, byte limits, circuit breaker, flush/flushSync/close, offline queues where applicable | High-throughput stream/transport model; transport startup caveats documented | Transport model with exceptions/rejections, querying, streaming, and close/await guidance | Sink model with category/filter/context control; reliability depends on chosen sink packages | Stream model; reliability depends on chosen streams | | Privacy controls | Redaction, privacy guard, normalize-error, safe codecs, URL/header sanitizers in integrations | Built-in path redaction using fast-redact | Formatting and custom transforms; no built-in redaction claim in checked README | Redaction package and filters | Serializers/custom streams | | Context propagation | Child loggers, bindings, tags, `withContext()`, Node AsyncLocalStorage installer | Child loggers, bindings, mixins; async context is app/ecosystem code | Child logger metadata; async context is app/ecosystem code | Explicit and implicit contexts with configurable context local storage | Child loggers and serializers | | TypeScript posture | First-party TypeScript source and declarations, typed events, subpath exports | Types included in the package ecosystem | Types included in the package ecosystem | TypeScript-first package | Historical Node package with TypeScript ecosystem support | | Dependency posture | `@loggerjs/core` has no dependencies; full workspace packages add only targeted deps such as `msgpackr` in `@loggerjs/codecs` | Small core with focused dependencies | Mature but larger dependency graph | Zero dependencies for `@logtape/logtape` | Older package with optional deps for some features | ## Performance Snapshot Current measured snapshot from [BENCHMARKS.md](BENCHMARKS.md) and the checked-in [benchmark matrix](BENCHMARK-MATRIX.md) — reference machine Apple M1 Max (64 GB), Node v22.21.1, pino 10.3.1, winston 3.19.0, LogTape 2.1.3. The loggerjs-vs-pino rows are the drift-canceling paired A/B (22 runs); competitor rows are the sequential suite: | Scenario | ns/op | Read | | ------------------------------------- | ----: | ------------------------------------------------------------ | | loggerjs disabled debug, lazy message | 3 | Disabled level path is at pino parity | | pino disabled debug | 9 | Same class of overhead | | loggerjs prepared lean record sink | 224 | Codec-owned prepared encoder — 1.28x pino (paired A/B) | | loggerjs lean record sink | 242 | Lean JSON via `fastEventJsonCodec` — 1.19x pino (paired A/B) | | pino NDJSON noop sink | 287 | Direct JSON path; baseline | | loggerjs full-envelope record sink | 307 | Adds `id`, `seq`, and `levelName` (~0.9x pino) | | node console info noop stream | 769 | ~3x slower than the loggerjs lean sink | | winston JSON noop sink | 2,726 | ~11x slower than the loggerjs lean sink | | LogTape JSON lines noop sink | 6,584 | ~27x slower than the loggerjs lean sink | The honest interpretation: - On the M1 Max reference machine LoggerJS lean and prepared are **faster than Pino** for equivalent output (1.19x / 1.28x, paired A/B, reproducible across 22 runs). This is **not** a universal "beats Pino" claim: the ranking is CPU/Node-V8 dependent, and the docs treat the difference as an empirical benchmark result rather than a proven mechanism. Reproduce on your hardware with `BENCH_AB=1 pnpm bench:node` and use `pnpm bench:matrix` for durable cross-machine evidence. - LoggerJS reaches Pino's class on equivalent output **without** giving up its record pipeline — that pipeline is a deliberate design, not accidental overhead. - The record pipeline buys first-class middleware, integrations, multi-transport routing, codec selection, and browser/server symmetry. - These numbers do not compare every possible Pino transport, Winston format chain, LogTape sink, or browser scenario. ## Where LoggerJS Is Stronger ### Browser and Isomorphic Applications LoggerJS has first-party browser transports and integrations: console capture, script/resource errors, fetch/XHR failures, Web Vitals, page lifecycle flushing, router events, user actions, WebSocket lifecycle, service worker lifecycle, ReportingObserver, IndexedDB persistence, offline HTTP queues, and ZIP export. This is the biggest practical difference from Pino, Winston, and Bunyan. Those libraries can be used in browsers to varying degrees, but the checked docs do not show a first-party automatic browser collection and local persistence suite equivalent to LoggerJS. ### Transport-Owned Codecs LoggerJS keeps structured values raw until the transport boundary. Serialization is a transport concern, so stdout can use NDJSON, browser HTTP can use safe JSON or a lean fast codec, OTLP can use an OTLP shape, and a custom transport can use MessagePack or a domain-specific projection. This is different from the common logger-level formatter model. It makes multi-destination logging less surprising because each destination owns its wire contract. ### Built-In Reliability Primitives LoggerJS ships common delivery controls as reusable pieces: batch transport, retry/backoff, byte limits, circuit breaker behavior, flush/close lifecycle, browser `sendBeacon`, IndexedDB offline queues, and transport stats where applicable. The goal is that writing a remote transport means implementing the destination, not rewriting the reliability layer. ### Automatic Collection as a First-Class Concept LoggerJS integrations are explicit, reversible, and routed through the same pipeline as manual logs. Captured logs still pass through middleware, processors, routing, codecs, and transports. This matters for privacy because redaction and sampling stay centralized. ## Where Another Logger May Be Better ### Choose Pino When Minimal Node JSON Logging Is The Main Requirement Pino remains the reference point for low-overhead Node JSON logging and has a mature ecosystem for Node web services. Current LoggerJS paired A/B numbers put the lean/prepared equivalent-output paths ahead on the M1 Max reference machine, but that ranking is CPU/Node-V8 dependent. If the application only needs app-authored server logs to stdout or a Pino transport, Pino is still the simpler and more battle-tested choice. ### Choose Winston When You Need Its Mature Transport/Format Ecosystem Winston is broad, stable, and flexible. Its `format` chain and transport model are familiar in many Node applications, and its README documents exception handling, rejection handling, profiling, querying, streaming, custom formats, and custom transports. Existing Winston deployments should migrate only when LoggerJS's isomorphic collection, middleware model, or measured performance benefit matters enough to justify the change. ### Choose LogTape For Multi-Runtime Library-First Logging LogTape is the closest conceptual peer to LoggerJS for library authors. Its official package page emphasizes zero dependencies, library-first design, structured logging, hierarchical categories, runtime diversity, redaction, and framework integration packages. If Deno/Bun/edge parity and zero dependencies in the core package are the top priority, LogTape is a strong fit. Choose LoggerJS over LogTape when first-party browser telemetry capture, IndexedDB/offline workflows, Node process/client/server integrations, transport-owned codecs, and current pino-relative Node benchmarks are more important. ### Choose Bunyan For Legacy Node JSON Compatibility Bunyan remains relevant when an existing service already emits Bunyan-shaped JSON or relies on the Bunyan CLI/stream ecosystem. For new browser/server applications, LoggerJS covers a much wider built-in surface. ## Other Common Tools | Tool | Best fit | How it compares with LoggerJS | | ---------------- | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Native `console` | Development output and simple scripts | LoggerJS can capture console calls and route them, but direct console remains the simplest debug output. It is not a structured delivery pipeline. | | `loglevel` | Tiny browser/Node level filtering over console methods | Much smaller and simpler. It does not try to provide transports, codecs, integrations, offline storage, or vendor delivery. | | `debug` | Namespace-based debug traces toggled by environment/local storage | Excellent for library debug traces. It is not a structured production logging pipeline. | | `consola` | Pretty CLI/browser console output and developer tooling UX | Strong for human-facing console UX, tags, reporters, console redirection, and prompts. LoggerJS is more focused on structured observability delivery. | | `tslog` | TypeScript-friendly pretty/JSON logger for Node and browser | Closer to a full logger than `debug` or `loglevel`, with attachable transports. LoggerJS has a broader first-party automatic collection, transport reliability, and vendor/browser persistence surface. | ## Migration Friction LoggerJS intentionally differs from Pino and Winston in a few places: - LoggerJS takes `(message, data)` for normal logs; Pino commonly uses `(object, message)`. - Stable metadata is split between `tags`, `bindings`, and ambient context instead of one generic `defaultMeta` or `base` object. - Data shaping belongs in middleware/processors; serialization belongs in codecs attached to transports. - Automatic capture is opt-in. Adding `captureConsoleIntegration()` or `captureFetchIntegration()` is explicit and reversible. See [MIGRATION.md](MIGRATION.md) for examples. ## Claims We Should Not Make Keep marketing and README claims inside these boundaries unless new evidence is added: - Do not claim LoggerJS is universally faster than Pino. The measured direct Node JSON ranking is CPU/Node-V8 dependent; cite the benchmark matrix for the exact machines tested. - Do not claim full Deno/Bun first-party support until the repo has tests and package metadata for those runtimes. - Do not claim every vendor feature is richer than ecosystem plugins. LoggerJS intentionally ships wire-protocol transports for common destinations; mature vendor SDKs may expose deeper platform-specific behavior. - Do not claim browser automatic collection is unique across all packages. The supportable claim is that no equivalent first-party suite was found in the checked docs for Pino, Winston, LogTape, or Bunyan. - Do not use old benchmark snapshots. Re-run `pnpm bench:node` before changing performance claims. --- # Migration Source: https://github.com/jskits/loggerjs/blob/main/docs/MIGRATION.md # Migration Notes LoggerJS is still pre-1.0, but the current codebase has moved from the initial skeleton toward the v1 architecture. The first half of this page covers migrating from other loggers; the second half covers vocabulary changes inside LoggerJS itself. ## From pino Same levels, same numeric values, same NDJSON instinct — the mapping is mostly mechanical. ```ts // pino import pino from "pino"; const logger = pino({ level: "info", base: { service: "checkout" } }); logger.info({ orderId: "ord_123" }, "order created"); const child = logger.child({ requestId: "req_1" }); // loggerjs import { createLogger, stdoutTransport } from "@loggerjs/node"; const logger = createLogger({ level: "info", tags: { service: "checkout" }, transports: [stdoutTransport()], }); logger.info("order created", { orderId: "ord_123" }); // message first, data second const child = logger.child({ bindings: { requestId: "req_1" } }); ``` Key differences: - **Argument order flips**: pino takes `(mergeObject, message)`, LoggerJS takes `(message, data)`. Errors go first in both: `logger.error(err, "msg")`. - pino `base` fields split into `tags` (stable, low-cardinality) and `bindings` (context fields merged into `context`). - pino `serializers` become processors (`normalizeErrorProcessor`, `redactProcessor`, custom `enrichProcessor`) — applied to structured data before serialization. - pino redaction maps to `redactProcessor({ paths, censor, remove })`; `replacement` is the LoggerJS-native name for `censor`, and exact key/path matching is preferred on hot loggers. - pino `transport`/`destination` becomes a transport: `stdoutTransport()`, `fileTransport()`, `nodeHttpTransport()`. - pino-pretty's role is `prettyStdoutTransport()` / `prettyStderrTransport()` for terminals, or `prettyConsoleTransport()` for browser DevTools. The core `consoleTransport()` remains a basic local console sink. - For Pino-shaped NDJSON, use `pinoCompatCodec()` from `@loggerjs/codecs`. Root data merging is opt-in (`mergeData: true`) and reserved key collisions are nested by default instead of overwriting `time`, `level`, `msg`, `pid`, `hostname`, or `err`. - For the fastest LoggerJS lean envelope, use `fastEventJsonCodec({ includeId: false, includeSeq: false, includeLevelName: false })`. Record-aware custom transports can wrap it with `createPreparedRecordEncoder(codec)` to reuse stable logger/tag fragments. On the M1 Max reference machine the plain lean path measures ~1.19× pino and the prepared lean path ~1.28× (paired A/B; ranking vs pino is CPU/V8-dependent — reproduce with `BENCH_AB`, see [BENCHMARKS.md](BENCHMARKS.md)); on top of that throughput you get middleware, integrations, multi-transport fan-out, and an isomorphic browser story. ## From winston ```ts // winston import winston from "winston"; const logger = winston.createLogger({ level: "info", format: winston.format.json(), defaultMeta: { service: "checkout" }, transports: [new winston.transports.Console(), new winston.transports.File({ filename: "app.log" })], }); // loggerjs import { createLogger, fileTransport, stdoutTransport } from "@loggerjs/node"; const logger = createLogger({ level: "info", tags: { service: "checkout" }, transports: [stdoutTransport(), fileTransport({ path: "app.log" })], }); ``` Key differences: - winston `format` chains split into two concerns: **processors/middleware** (data shaping: redact, enrich, filter) and **codecs** (serialization, owned by each transport). `format.combine(timestamp, json)` is simply the default output. - `defaultMeta` → `tags` and/or `bindings`. - Per-transport `level` maps directly to `minLevel` on any transport. - Child loggers replace `winston.loggers` registries for per-module configuration; library authors should prefer `getLogger()` from core. - Throughput on the fastest comparable path measures roughly 11x winston in the current snapshot ([BENCHMARKS.md](BENCHMARKS.md)). ## From console.log Two migration styles, usable together. **Capture first, migrate incrementally** — turn existing console calls into structured logs without touching call sites: ```ts import { captureConsoleIntegration, createLogger, browserHttpTransport } from "@loggerjs/browser"; const logger = createLogger({ transports: [browserHttpTransport({ url: "/api/logs" })], integrations: [captureConsoleIntegration({ levels: ["log", "warn", "error"] })], }); ``` **Then replace call sites** where structure pays off: ```ts // before console.log("order created", orderId); console.error("payment failed", err); // after logger.info("order created", { orderId }); logger.error(err, "payment failed"); ``` What you gain at each step: levels and level gating, structured data instead of interpolated strings, redaction before anything leaves the process, batching/offline delivery, and crash-path capture via the error/process integrations. --- ## Processor To Middleware Vocabulary The `@loggerjs/processors` package remains supported for compatibility. New docs describe this layer as synchronous middleware because the behavior is broader than event processors: redaction, enrichment, sampling, tags, type, dedupe, and trace attachment all run before transport delivery. Existing code can continue to use: ```ts import { redactProcessor } from "@loggerjs/processors"; ``` New core middleware can use: ```ts import { createMiddleware } from "@loggerjs/core/middleware"; ``` ## LogEvent And LogRecord `LogEvent` remains the transport-facing compatibility shape. Core record helpers now use `LogRecord` internally so the hot path can preserve lazy messages, raw errors, bound context, and stable record shape before projecting to transport events. Transport authors should keep accepting `LogEvent` through the current public `Transport` interface. Codec authors should use the exported codec input helpers rather than reaching into logger internals. ## Context Use child loggers for explicit context: ```ts const requestLogger = logger.child({ requestId: "req_123" }); ``` Use ambient context for request scopes: ```ts import { withContext } from "@loggerjs/core"; import { installAsyncLocalStorageContext } from "@loggerjs/node"; installAsyncLocalStorageContext(); await withContext({ requestId: "req_123" }, async () => { logger.info("request started"); }); ``` ## Browser Integrations Browser collection remains opt-in. Existing manual logging code does not automatically capture console, errors, fetch, or XHR until the matching integration is configured. Prefer: ```ts captureConsoleIntegration({ levels: ["warn", "error"] }); captureBrowserErrorsIntegration(); captureFetchIntegration(); pageLifecycleIntegration(); ``` ## Transports And Codecs Serialization belongs to transports. Move JSON/stringification work out of processors and into a transport codec: ```ts browserHttpTransport({ url: "/api/logs", codec: safeJsonCodec() }); ``` Batch-based transports now share queue, retry, byte-limit, concurrency, and circuit-breaker options. ## Package Imports Root package imports still work: ```ts import { createLogger } from "@loggerjs/core"; ``` Stable subpaths are available for narrower imports: ```ts import { createMiddleware } from "@loggerjs/core/middleware"; import { browserHttpTransport } from "@loggerjs/browser/transport-http"; import { stdoutTransport } from "@loggerjs/node/transport-stdout"; ``` The current build publishes both ESM and CJS entry points. Type declarations are checked against NodeNext-style package resolution. --- # API Stability Source: https://github.com/jskits/loggerjs/blob/main/docs/API-STABILITY.md # API Stability LoggerJS is still pre-1.0. The checked-in `api-reports/` files describe every exported TypeScript declaration, but they are not a promise that every exported symbol is already frozen for v1. This page is the human contract. The machine-readable classification lives in [`docs/api-stability.policy.json`](api-stability.policy.json), and `pnpm verify:api-stability` fails when a package export is missing from that policy. ## Current Policy Before v1, the project is narrowing the compatibility promise instead of freezing the whole repository. The stable set is intentionally limited to the core logger model, core pipeline contracts, primary browser and Node delivery paths, pretty output, processors, and codecs. Everything else may still be public, tested, and useful, but it is not all part of the v1 compatibility promise yet. In particular, vendor, observability, and database packages remain experimental until they have more real-world usage and failure-mode validation. ## Status Levels | Status | Meaning | | --- | --- | | Stable v1 Candidate | Intended to carry into v1 without removals, renames, or signature breaks except for security, data-loss, or wire-protocol correctness fixes. Additive changes are allowed. | | Compatible Public Surface | Public and tested, but minor releases before v1 may refine option names, captured fields, or runtime edge behavior with release notes. | | Experimental Before v1 | Public packages or subpaths that may change before v1. Use them when the current behavior fits, but do not treat them as frozen compatibility contracts. | Internal source paths, `dist` file paths, generated bundle layout, private class fields, and behavior inferred only from tests are not public API in any status. ## Stable v1 Candidate Stable exports are tracked in `api-stability.policy.json`. The current stable packages and entry families are: | Package | Stable surface | | --- | --- | | `@loggerjs/core` | Root package and documented core subpaths for middleware, codecs, events, context, trace propagation, payload transforms, and core transports. | | `@loggerjs/browser` | Documented stable subpaths for HTTP delivery, IndexedDB/offline-first storage, payload transforms, and the primary console/error/fetch/XHR/context/performance/page-lifecycle integrations. | | `@loggerjs/node` | Documented stable subpaths for stdout/stderr/file/rotating-file/HTTP/syslog/worker transports, payload transforms, process capture, outgoing HTTP capture, diagnostics, and AsyncLocalStorage context. | | `@loggerjs/pretty` | Root package, formatter, console transport, and stream transports. | | `@loggerjs/processors` | Root package processor and middleware catalog. | | `@loggerjs/codecs` | Root package codec catalog. | Stable semantics include: - `createLogger(options)`, `getLogger(category)`, and `configure(...)` for application and library-safe logging. - Logger instance methods: `trace`, `debug`, `info`, `warn`, `error`, `fatal`, `log`, `capture`, `event`, `child`, `withTags`, `withType`, `setLevel`, `getLevel`, `isEnabled`, `isLevelEnabled`, `addTransport`, `addProcessor`, `addIntegration`, `ready`, `flush`, `flushSync`, and `close`. - Level names and numeric values: `trace=10`, `debug=20`, `info=30`, `warn=40`, `error=50`, `fatal=60`, and `silent`. - The pipeline interfaces for `Middleware`, `Processor`, `Transport`, `Integration`, and `Codec`, including `TransportContext.toEvent(record)` memoized projection. - Disabled levels return before record allocation and lazy message evaluation. - Middleware, processors, codecs, integrations, and transports are error-isolated from application code. - Serialization remains transport-owned; middleware and processors keep values structured. ## Compatible Public Surface Compatible exports stay documented and tested, but they are not frozen enough to be stable v1 candidates yet. Current compatible areas include: - Browser and Node root packages (`@loggerjs/browser`, `@loggerjs/node`) are compatible convenience aggregators because they re-export both stable and compatible components. Use the stable subpaths above when you need a v1 candidate compatibility boundary. - Browser secondary transports and collectors: BroadcastChannel, service worker, WebSocket, ZIP export, framework errors, framework routers, generic router capture, ReportingObserver, runtime host, service worker messages, user actions, and WebSocket capture. - Node framework and data integrations: Express, Fastify, Koa, Nest, Hapi, Prisma, Redis, generic queues, BullMQ, serverless lifecycle, database method wrapping, and CLI capture. The public import paths should remain available during pre-v1, but exact captured fields, hook coverage, and edge behavior may be refined. These are the right places to tighten names or reduce claims before v1 if real usage shows the current API is too broad. ## Experimental Before v1 These packages are public because they are useful for integration testing and early adopters, but they are not v1 compatibility commitments yet: | Package family | Experimental exports | | --- | --- | | Observability adapters | `@loggerjs/otel/*`, `@loggerjs/sentry/*` | | Vendor wire transports | `@loggerjs/datadog/*`, `@loggerjs/elastic/*`, `@loggerjs/loki/*`, `@loggerjs/cloudwatch/*` | | Database transports | `@loggerjs/database/*` | Experimental does not mean untested. It means minor releases before v1 may change option names, payload mapping, retry expectations, batching guidance, or subpath layout if design partners or live endpoints expose a better shape. Raw vendor transports are not durable by themselves. For production delivery, wrap them with `batchTransport()` and `retryTransport()` or use a collector endpoint that owns queueing, retry, authentication, and backoff. ## Change Policy For Stable v1 Candidate APIs: - No intentional removals, renames, or signature breaks before v1 without a deprecation note and migration path. - Additive changes are allowed: new options, fields, overloads, processors, transports, integrations, and subpaths. - Defaults that affect delivery, privacy, or performance require documentation and release notes. - Security fixes, data-loss fixes, and vendor wire-protocol correctness fixes may change edge-case behavior. Release notes must call those out. For Compatible and Experimental APIs: - Public exports stay typechecked, tested, API-reported, and documented. - Minor releases may adjust names, options, field shape, or exact behavior before v1. - Breaking changes should still include release notes and migration guidance, because public does not mean disposable. ## Adding Public API New package exports must: 1. Add or update tests at the closest practical runtime level. 2. Update docs and examples for import boundaries and caveats. 3. Add the export to `docs/api-stability.policy.json`. 4. Run `pnpm verify:api-stability` and `pnpm api:check`. Prefer examples and composition over new exports when an existing stable API can solve the use case. ## How to Evaluate a Future Upgrade 1. Read the package changelog and release notes. 2. Check this page and `api-stability.policy.json` for the export status you depend on. 3. Run `pnpm check` in this repository if you are contributing, or your application test suite if you are consuming LoggerJS. 4. For hot paths, reproduce your relevant benchmark with `pnpm bench:node` or `pnpm bench:browser`. 5. For remote delivery, test your actual collector/vendor endpoint and monitor `transport.dropped.*`, `transport.retry.*`, and queue-depth metrics. --- # Architecture Source: https://github.com/jskits/loggerjs/blob/main/docs/ARCHITECTURE.md # LoggerJS Architecture > Status: implementation architecture for the current v1-oriented codebase. > Source inputs: `DESIGN.md`, `log.md`, and the current monorepo skeleton. LoggerJS is an isomorphic structured logger for browser, Node, Bun, Deno, and edge runtimes. The product architecture is built around three user-facing concepts: - **Integration**: opt-in automatic collection, such as browser console capture, global script errors, HTTP errors, page lifecycle flush, Node process errors, and runtime diagnostics. - **Middleware**: synchronous record transforms and filters, such as redaction, sampling, tag/type enrichment, request correlation, dedupe, and route-specific policies. - **Transport**: the destination boundary, such as console, stdout, file, HTTP batch, OTLP, Sentry, DB, worker-hosted delivery, or any user custom sink. There is one additional technical boundary that must stay first-class: **Codec**. A codec belongs to a transport and owns serialization/deserialization. Middleware must not serialize records. Console transport should preserve raw values. HTTP/file/OTLP transports choose the codec they need. ## Current Repository Baseline The current repo now has the main v1 building blocks in place: ```txt packages/core Logger, LogRecord helpers, LogEvent projection, context, typed events, codecs, console/memory/batch transports packages/browser Browser HTTP transport, offline queue, beacon/page lifecycle flush, console/error/fetch/XHR integrations packages/node stdout/stderr/file/http/worker transports, AsyncLocalStorage context, process and diagnostics-channel integrations packages/processors redact/sample/tags/type/dedupe/trace processors packages/codecs fixed-shape JSON, built-in msgpackr, projector codec packages/otel OTLP JSON mapping, HTTP transport, active span trace processor packages/sentry Sentry structured logs, breadcrumbs, exception/message transport examples/* browser and node basic demos ``` Remaining architecture work is mostly about polish and package topology: - `Processor` is still supported as compatibility vocabulary while `Middleware` is the public mental model. - `LogEvent` remains the transport-facing compatibility envelope while the hot path constructs `LogRecord` and projects when needed. - Coarse browser/node packages can remain as presets, but stable v1 packages should split platform transports and integrations into smaller installable units. - The current dual ESM/CJS output is retained for compatibility. Declaration output is NodeNext-compatible and public subpath exports are verified. - Batch transports now cover bounded queues, byte limits, retry, drop counters, circuit breaking, pagehide/beacon behavior, and runtime flush semantics. ## Non-Negotiable Design Rules 1. **Core is platform-neutral.** `@loggerjs/core` must not import browser, Node, Bun, Deno, worker, filesystem, fetch, or diagnostics APIs. 2. **Disabled logging is almost free.** A disabled level call must do one numeric level comparison and return before record allocation, message stringification, context merge, or integration work. 3. **Serialization happens only at the transport boundary.** The pipeline keeps raw references. `resolveMessage(record)` is the only allowed middleware-triggered lazy evaluation. 4. **Middleware is synchronous.** No promises, no Koa-style `next`, and no async lookup in the hot path. 5. **Integrations use the same pipeline as manual logs.** Automatic records differ only by `source`; they still pass through middleware, routing, batching, codec, and transport policy. 6. **Integrations are explicit and reversible.** Any monkey patch must be opt-in, idempotent, guarded against reentry, and fully torn down. 7. **Logger errors never escape to application code.** Internal failures are counted and reported through a rate-limited meta logger. 8. **No object pool in v1.** Short-lived records should stay young-generation GC objects unless benchmarks prove otherwise. ## End-To-End Pipeline ```txt manual API / integration capture | v level gate | v create LogRecord | v global middleware | v transport router / fan-out | +--> per-transport middleware | | | v | transport buffer | | | v | codec.encode(batch) | | | v | sink: console / stdout / file / HTTP / OTLP / worker / custom | +--> ... ``` The record must never be stringified before the chosen transport is ready to ship. This is what lets console preserve interactive objects, HTTP choose JSON or binary, file choose NDJSON, and OTLP choose its wire mapping without penalizing other destinations. ## Core Record Model The target internal record is `LogRecord`, not the current `LogEvent` envelope. It is optimized for a stable hidden class and transport-owned projection: ```ts export interface LogRecord { time: number; level: number; category: readonly string[]; msg: string | null; lazy: (() => string) | null; props: Record | null; err: unknown; ctx: BoundContext | null; source: string; stack: string | null; seq: number; } ``` Implementation rules: - Construct records through a single `createRecord()` path. - Assign every field in the same order, including `null` fields. - Do not `delete` fields or attach ad hoc properties to the record. - Extra data belongs in `props` or immutable `ctx`. - `time` is `Date.now()`; ordering within equal timestamps is `seq`. - `err` stays separate from `props` because error encoding, stack truncation, cause handling, and dedupe are specialized. The current `LogEvent` shape can remain temporarily as a codec projection or compatibility type, but it should not drive the hot path once the v1 rewrite starts. ## Logger API LoggerJS supports two acquisition models: ```ts const log = createLogger({ category: "app", level: "info", transports: [consoleTransport()] }); ``` ```ts const log = getLogger(["library", "parser"]); await configure({ middleware: [redact({ paths: ["password", "*.token"] })], transports: { console: consoleTransport(), http: httpTransport({ url: "/v1/logs", codec: jsonCodec() }) }, loggers: [ { category: ["app"], level: "debug", transports: ["console", "http"] }, { category: ["library"], level: "warn", transports: ["http"] } ], integrations: [consoleIntegration(), globalErrorsIntegration()] }); ``` Required call forms: ```ts log.info("user logged in", { userId: 42 }); log.error(err, "save failed", { orderId }); log.debug(() => expensiveDebugMessage()); log.event(CheckoutCompleted, { orderId, amountCents }); log.child({ requestId }).warn("retrying"); await log.flush(); ``` The overload rule stays small: - first arg `string`: message - first arg `function`: lazy message - otherwise: error slot, with optional message and props No printf-style formatting belongs in core. Structured fields are first-class; formatting is a display concern. ## Registry And Configuration `getLogger(category)` exists for library authors. Before configuration, it returns a void logger. After `configure()`, it routes through the configured pipeline. Configuration requirements: - prefix matching by category, where `["app"]` applies to `["app", "checkout"]` - named transports and named middleware - explicit integration lifecycle management - optional early ring buffer for pre-config logs - `configure({ reset: true })` to replace old transports and call async disposal hooks - immutable runtime snapshots so hot path reads do not traverse mutable config structures This registry is a strategic feature: it lets third-party libraries log without coupling to any backend or forcing application configuration. ## Context There are two context modes: - **Explicit context** via `logger.child(bindings)`. Child bindings are flattened and frozen at child creation time. - **Implicit context** via `withContext(bindings, fn)`. Node/Bun/Deno use AsyncLocalStorage or equivalent conditional exports. Browser initially degrades to synchronous-scope context until TC39 AsyncContext is viable. Codec-level context optimization replaces pino-style global chindings: ```ts interface EncodeContext { levelName(level: number): string; ctxCache: WeakMap; schemaCache: WeakMap; } ``` Each codec may cache encoded fragments for immutable bound contexts. This preserves the performance benefit without making JSON serialization a global logger concern. ## Middleware Target interface: ```ts export interface Middleware { readonly name: string; process(record: LogRecord): LogRecord | null; } ``` Execution model: - global middleware runs once before fan-out and may mutate the single record in place - per-transport middleware runs after fan-out and must treat the record as shared - per-transport changes use `cloneRecord(record, patch)` to preserve shape and avoid cross-transport leakage - returning `null` drops the record - middleware exceptions are caught, counted, and do not stop the remaining pipeline unless the middleware explicitly drops Built-ins should cover: - `redact`: safe path/key redaction with copy-on-write on matched branches - `sample`: level/category/key-based sampling, with error and fatal defaulting to full retention - `rateLimit`: token bucket by category/level/source - `dedupe`: fingerprinted burst collapse - `fingersCrossed`: low-level ring buffer released by an error trigger - `enrich`: synchronous props/context enrichment - `tags` and `type`: thin compatibility helpers for current processor behavior - `traceContext`: OTel or user-provided trace/span injection Middleware must not call `JSON.stringify`, `String(record.props)`, or recursively normalize whole records. If it needs a message, it must call `resolveMessage(record)` intentionally. ## Transports Target interface: ```ts export interface Transport { readonly name: string; write(record: LogRecord): void; flush(): Promise; flushSync?(): void; dispose(): Promise; filter?(record: LogRecord): boolean; middleware?: Middleware[]; } ``` Transport responsibilities: - final routing filters - queue and backpressure policy - batching - retry and circuit breaking - codec selection and serialization - destination-specific delivery - drop/error counters - flush and disposal semantics ### Batching Base The shared batching implementation should support: - `maxRecords` - `maxBytes` - `maxWaitMs` - `concurrency` - retry with exponential backoff and full jitter - `drop-old` and `drop-new` - drop counters and hooks - circuit breaker with half-open recovery - no idle timer when the queue is empty - encoded-size accounting at ship time The current `batchTransport()` is acceptable as a bootstrap utility, but it is not the v1 reliability layer. ### Console Transport Console transport should not serialize in pretty mode. It should pass raw `msg`, `props`, and `err` references to the original console methods so browser devtools keep object inspection. It must use the unpatched console registry so it can coexist with console capture without feedback loops. ### HTTP Transport HTTP transport is a shared abstraction with platform implementations: - Browser: `fetch`, `keepalive`, `sendBeacon` on `pagehide`/`visibilitychange`, optional IndexedDB offline queue, and strict payload limits around the 64 KiB beacon budget. - Node: global `fetch`/undici, retry/circuit breaker, and no claim of sync crash flush. - Edge: `waitUntil` hook for response-lifetime-safe delivery. Privacy defaults: - no request/response body collection - no headers unless allowlisted - no offline disk persistence unless explicitly enabled ### File And Stdout Transports Node stdout/stderr/file transports should default to NDJSON. File transport needs a real `flushSync()` path using `fs.writeSync` or an equivalent crash-safe primitive. Async stream writes alone are not enough for fatal process events. ### Worker Transport Node worker transport should move IO and retry state off the main thread. The preferred path is: ```txt main thread batch -> codec.encode(batch) -> Uint8Array -> postMessage(buffer, [buffer]) ``` If the worker fails, transport should degrade to inline mode and emit a meta warning. `flushSync` remains unavailable across worker boundaries. ### OTLP And Sentry OTLP/HTTP JSON is a first-party transport because LoggerJS should integrate with existing observability backends rather than invent a logging backend protocol. Sentry support should be an adapter package. LoggerJS maps records to Sentry structured logs and optionally captures error records as Sentry events. ## Codecs Target interface: ```ts export interface Codec { readonly name: string; readonly contentType: string; encode(batch: readonly LogRecord[], ctx: EncodeContext): Out; decode?(data: Out): unknown[]; } ``` Required codecs: - `jsonCodec`: default NDJSON/log JSON codec with fixed-field ordering, native `JSON.stringify` for ordinary props, and safe fallback only for failing branches. - `structuredCodec`: rich value preserving codec with symmetric decode for Error, cause chains, AggregateError, circular/shared references, BigInt, Date, RegExp, URL, Map, Set, TypedArray, ArrayBuffer, `undefined`, `NaN`, infinities, and `-0`. - `msgpackCodec`: binary batch codec, either a benchmark-proven custom subset or a small adapter over `msgpackr`. - `projectorCodec`: utility adapter for custom wire schemas. The structured codec should use a flat value-pool format, not `eval`, `new Function`, or recursive revivers. Decode should be `JSON.parse` plus a deterministic pointer restoration pass, making it CSP-friendly and suitable for browser replay tools. ## Integrations Target interface: ```ts export interface Integration { readonly name: string; setup(api: IntegrationAPI): Teardown; } export interface IntegrationAPI { capture(input: CaptureInput): void; getLogger(category: string | readonly string[]): Logger; unpatched: UnpatchedRegistry; guard any>(fn: T): T; } ``` Loop prevention has three layers: 1. Register original console/fetch/XHR functions before patching. 2. Guard synchronous logger execution so reentrant capture is dropped and counted. 3. Preserve `record.source` and let transports filter self-generated records. Required browser integrations: - console capture for `log`, `info`, `warn`, `error`, `debug`, and `trace` - global script/resource errors - `unhandledrejection` - optional `securitypolicyviolation` - fetch and XHR HTTP error collection - page lifecycle flush hooks - optional offline replay hooks Required Node integrations: - `uncaughtException` - `unhandledRejection` - `warning` - `beforeExit`/`exit` flush handling - diagnostics_channel subscriptions for undici and Node HTTP where available Node crash behavior must be honest. If `exitOnUncaught` is enabled, fatal capture should flush sync-capable transports, attempt bounded async flush for the rest, and then preserve process exit semantics. The integration must not silently turn fatal crashes into zombie processes. ## Routing Routing uses category, level, source, tags/type, and explicit transport filters. Configuration should support: - category prefix rules - per-transport minimum levels - source exclusions such as excluding `integration:console` from console transport - per-transport middleware - named routes that can be reused in presets Routing must be resolved into immutable runtime snapshots so each log call does not perform expensive dynamic config lookup. ## Performance Budget Initial internal budget: | Path | Target | | --- | --- | | Disabled level call | one numeric comparison, zero allocation | | Enabled record to queue, 3 middleware, no stack | <= 1 microsecond per record on mainstream desktop CPU | | JSON/NDJSON codec | million-records-per-second class for ordinary objects | | Node NDJSON full path | at least 80% of pino for equivalent output before v1 | | Core size | <= 4 KB min+gzip (aspirational target, not yet met — see note below) | | Record allocation | one record object; no data copy unless middleware explicitly clones | Status as of 2026-06: the `<= 4 KB` core-size row above is an unmet aspiration, not the current state. Measured today (and enforced by `pnpm size:check`): the full `@loggerjs/core` barrel is ~18 KB gzip, and a minimal tree-shaken import (`createLogger` + a `consoleTransport`) is ~6 KB min+gzip. Keep public size wording anchored to these measured numbers until the budget is actually met. Benchmarking must cover Node and real browsers, not only synthetic Node loops. The suite should compare pino, winston, LogTape, native console, native `JSON.stringify`, current LoggerJS, and target LoggerJS paths. ### Decision: keep the record pipeline; optimize through codec-owned preparation Status as of 2026-06: on the reference machine (Apple M1 Max, Node v22.21.1), measured with the drift-canceling paired A/B harness, the lean Node NDJSON path runs at ~1.19x pino and the codec-owned prepared lean path at ~1.28x — i.e. **faster than pino** for equivalent output. The full-envelope path is ~0.9x pino while emitting `id`, `seq`, and `levelName` on top of pino's fields (see `docs/BENCHMARKS.md`). This ranking is **CPU/Node-V8 dependent**: pino and loggerjs both use hand-tuned JSON hot paths, and the current docs treat the difference as an empirical benchmark result rather than assigning a low-level cause. On a different chip or Node/V8 build pino can lead. The point is that loggerjs reaches pino's class **without** moving serialization into the logger. Getting here took a 2026-06 profiling pass that also corrected an earlier overstatement ("the gap is structural, not unoptimized code"). Three changes, none of which touch the architecture, moved the lean ratio from ~1.30x pino to ~0.84x on this machine: (1) `getContext` no longer runs an `addedProviders.map()` + spread + `mergeContext({})` on every call when no ambient context is configured (it had been allocating three objects to merge nothing); (2) `fastEventJsonCodec` bakes its `includeX` toggles once at codec creation and emits the header in a single template instead of a chain of `+=` concatenations (stable decisions happen outside the per-record call); and (3) codec-owned prepared record encoders let transports reuse logger/category/tags fragments without making the logger own JSON serialization. LoggerJS still allocates a `LogRecord` per log so middleware, processors, integrations, and multiple transports can observe one shared value, and the codec still owns a never-throw safe-fallback contract — and it now matches or beats pino on tested hardware anyway. A fusion fast path that bypasses the record whenever a logger has exactly one sync transport and no middleware is therefore rejected as the default, with even less reason than before, because it would: - create a performance cliff where adding the first middleware silently costs 30%+ of throughput, - move serialization into the logger, breaking the codec-belongs-to-transport boundary, - and double the hot-path surface that every semantic change must keep in sync (the id-drift and source round-trip bugs fixed in 2026-06 were exactly this class of dual-path defect). Remaining performance budget goes to the default paths (batch enqueue, default codecs, prepared codec contracts) and to regression gating, not to fusion-only peak numbers. Revisit only if a use case demonstrates that a separate semantic hot path matters in production. ## Reliability Default semantics are **best-effort at-most-once**. LoggerJS must not block application progress indefinitely to guarantee log delivery. Every loss path must be observable: - queue overflow - batch too large - retry exhausted - circuit breaker open - beacon failed - offline queue quota exceeded - flush deadline exceeded - integration loop guard drop - middleware/transport exception These counters should be available through the meta logger and optional stats APIs. ## Privacy And Security Defaults: - redact common sensitive keys: authorization, cookie, set-cookie, password, passwd, token, secret, apiKey, api_key, and `*_key` - fetch/XHR integrations do not capture bodies - fetch/XHR integrations do not capture headers unless allowlisted - browser offline queue is disabled by default - no `eval` or generated code in default builds - no runtime dependencies in core Any feature that writes logs to durable browser storage must be explicit because it changes the application's privacy posture. ## Testing Strategy Required test layers: - unit tests for core record construction, level gate, overloads, child context, middleware, router, and transport errors - codec property tests for safe JSON and structured round-trip behavior - loop prevention tests with console transport and console integration enabled together - browser Playwright tests for pagehide/beacon flush, fetch/XHR capture, global errors, and offline queue behavior - Node child-process tests for uncaught exception flush and exit semantics - runtime smoke tests for Node, Bun, Deno, and workerd/miniflare - size-limit checks for core and integration packages - benchmark regression checks with explicit thresholds No milestone is complete without examples, tests, and at least one benchmark or size measurement relevant to the changed layer. ## Package Direction The v0 package layout can support development, but the v1 public layout should move toward: ```txt @loggerjs/core @loggerjs/transport-http @loggerjs/transport-otlp @loggerjs/transport-file @loggerjs/transport-worker @loggerjs/codec-structured @loggerjs/codec-msgpack @loggerjs/integration-console @loggerjs/integration-global-errors @loggerjs/integration-fetch @loggerjs/integration-node @loggerjs/otel @loggerjs/sentry @loggerjs/pretty @loggerjs/browser preset/meta package @loggerjs/node preset/meta package ``` Preset packages are allowed, but ownership of platform APIs should live in small packages so users can install only the collection and transport surface they need. ## Completion Criteria For v1 LoggerJS reaches v1 readiness when: - core public API is locked by API report - disabled hot path and enabled queue path meet budget on Node and browser - codec JSON, structured, and msgpack paths have benchmark data - browser and Node integrations have loop and teardown tests - OTLP collector demo works end to end - crash flush behavior is tested by child process - privacy defaults are documented and tested - examples cover browser, Node service, edge worker, and OTLP collector - migration guide exists for console.log, pino, winston, and LogTape-style library logging --- # Contributing Source: https://github.com/jskits/loggerjs/blob/main/docs/CONTRIBUTING.md # Contributing ## Setup ```bash pnpm install # repository development uses Node >=22.13, pnpm >= 11.5.3 pnpm check # the full gate — run before pushing ``` `pnpm check` is the same gate CI runs on every pull request: format check (oxfmt), lint (oxlint), typecheck, tests (vitest), builds (rolldown + tsc), size budgets, export map verification, public type surface check, API report check, and npm pack validation. CI additionally runs `pnpm bench:gate`. ## Node Version Policy - **Repository development:** use Node `>=22.13.0`. The root `package.json` `engines` field and local tooling are intentionally set to this floor. - **Full CI gate:** runs `pnpm check` on Node 22 and 24, with releases built on Node 24. - **Published package runtime compatibility:** packed packages are smoke-tested as consumers on Node 20.19.0, 22, and 24. Node 20.19.0 is the runtime compatibility floor for Node consumers; it does not lower the repo development toolchain requirement. ## Repository Layout ``` packages/core platform-neutral kernel: logger, record/event model, registry, context, middleware, integration API, console/memory/test/batch transports, json/safe-json/ndjson codecs packages/browser browser transports + integrations packages/node node transports + integrations + AsyncLocalStorage context packages/processors middleware/processor toolbox packages/codecs fast-event-json, built-in msgpackr, projector packages/otel|sentry|datadog|elastic|loki|cloudwatch|database destination adapters examples/ runnable examples per platform scripts/ build/verify/bench/release tooling docs/ this documentation api-reports/ checked-in public API surface per package ``` Turbo orchestrates `build`/`test`/`typecheck` with caching; scope work with `pnpm exec turbo run test --filter=...@loggerjs/core` (the package and its dependents). ## Rules That Fail CI **Commits** follow Conventional Commits, enforced by commitlint. Allowed scopes: `browser`, `build`, `codecs`, `core`, `deps`, `docs`, `examples`, `node`, `otel`, `processors`, `release`, `repo`. **API reports**: any public-surface change (including JSDoc on exported symbols) requires regenerating reports — `pnpm build && pnpm api:report` — and committing the diff. `pnpm api:check` fails on drift. **Size budgets** (`scripts/check-size-budgets.mjs`): every package has raw + gzip ceilings checked after build. Raise a budget only together with the change that justifies it, in the same or an adjacent commit, with the reason in the message. **Component docs** (`scripts/verify-component-docs.mjs`): every public `transport-*`, `*-transport`, or `integration-*` subpath must be listed in the matching transport/integration import-boundary docs. New components also need stability and reliability notes in the same change. **Benchmark gate** (`pnpm bench:gate`): hot-path scenarios are limited as paired A/B ratios against the matching pino baseline. Limits are generous — they catch structural regressions (an accidental allocation per log, a dropped fast path), not noise. If your change legitimately shifts a ratio, update the limits in `scripts/check-bench-regression.mjs` with justification. **Changesets**: user-facing changes to published packages need a changeset (`pnpm changeset`); pure repo tooling does not. ## Engineering Conventions - **Core stays platform-neutral**: no DOM types, no Node built-ins, feature-detect via `globalThis`. The public type surface must compile without `lib.dom`. - **Prefer stabilization over surface expansion before v1**: harden existing transports/integrations first. A new built-in component needs a production use case, runtime-appropriate package placement, tests, stability docs, import-boundary docs, and size-budget evidence. - **The pipeline never throws into the app.** Middleware, processors, codecs, and transports are error-isolated; failures report through `onInternalError` and meta counters. New code keeps that property. - **Codecs must not lose logs**: wrap risky encodes and fall back to `safeJsonStringify`; count fallbacks in meta. - **Shared objects are frozen, replaced not mutated** (`record.tags`, `record.ctx`). - **Hot-path changes need numbers.** Run `pnpm bench:node` before/after and put the relevant line in the commit message; update `docs/BENCHMARKS.md` when the snapshot moves materially. Benchmark warmup must stay proportionate to iterations — see the warmup note in BENCHMARKS.md for the time we got this wrong. - **Performance has a documented boundary**: read the record-pipeline decision in [ARCHITECTURE.md](ARCHITECTURE.md) before proposing a record-bypassing fast path. ## Tests Vitest per package, `test/*.test.ts`. House style: - Pin behavior with hostile inputs (circular refs, BigInt, frozen objects, throwing callbacks) — most past regressions were caught by exactly these. - Use `testTransport()` from core for transport-side assertions; it provides snapshots, stats, and `waitForCount`. - New transports/integrations ship with teardown tests: patch, capture, restore, assert no double-capture. Additional CI gates cover the runtime and quality surface: - `pnpm test:e2e:browser` runs the browser E2E suite in Chromium, Firefox, and WebKit. - `pnpm compat:runtimes -- --runtime=bun|deno|workers` smoke-tests the packed packages in Bun, Deno, and a workerd/Miniflare runtime. - `pnpm test:quality` runs coverage thresholds, mutation testing, and the concurrent soak runner. - `pnpm test:live:local` starts Docker-backed Elasticsearch and Loki instances, writes real log events through the transports, and queries those services back. - `pnpm test:live:external` writes to and queries Datadog Logs and CloudWatch Logs. It requires `DATADOG_API_KEY`, `DATADOG_APP_KEY`, `AWS_REGION`, `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `CLOUDWATCH_LOG_GROUP`; use `pnpm test:live:config` to audit which variables are present without printing secret values. ## Releasing See [RELEASE.md](RELEASE.md). Short version: changesets accumulate on `main`; the release workflow versions, builds, runs the full gate, and publishes with provenance. --- # Release Source: https://github.com/jskits/loggerjs/blob/main/docs/RELEASE.md # Release LoggerJS uses Changesets for versioning and a GitHub Actions release workflow for npm publishing. ## Local Validation Run the full release dry-run before cutting a release: ```bash pnpm release:dry-run ``` This runs the normal quality gate, verifies public exports and API reports, checks every package with `npm pack --dry-run --json`, installs publish-style tarballs into a temporary consumer smoke project, prints Changesets status, and runs `pnpm publish -r --dry-run --access public --no-git-checks --json`. For canary validation without publishing: ```bash pnpm release:canary:dry-run ``` Use the real `canary` dist-tag only from a versioned prerelease branch or workflow: ```bash pnpm check changeset publish --tag canary ``` ## New Component Readiness Before a release that adds a public transport or integration subpath, verify the change includes: - stability classification in `docs/TRANSPORTS.md` or `docs/INTEGRATIONS.md`; - import-boundary coverage checked by `pnpm verify:component-docs`; - runtime validation appropriate to the component: browser E2E for browser lifecycle/storage behavior, runtime smoke for edge/runtime claims, Docker-backed live tests for local services, or external-provider smoke for hosted vendors; - size-budget evidence from `pnpm size:check`, with any budget increase justified in the change; - production docs that spell out delivery, retry, privacy, and credential placement caveats. ## NPM Publishing The release workflow is `.github/workflows/release.yml`. Each publishable `@loggerjs/*` package should be configured on npmjs.com with a Trusted Publisher entry for: - Organization or user: `jskits` - Repository: `loggerjs` - Workflow filename: `release.yml` - Allowed action: `npm publish` The workflow uses npm Trusted Publisher/OIDC for npm publishing; it does not read `NPM_AUTH_TOKEN`, `NPM_TOKEN`, or `NODE_AUTH_TOKEN`. - `permissions.id-token: write` lets GitHub Actions mint the OIDC token npm exchanges during `npm publish`. - `actions/setup-node` sets the npm registry for the publish command without configuring a long-lived token. - The release job runs on a GitHub-hosted Ubuntu runner with Node 24 and upgrades to npm 11 so Trusted Publisher support is current. - Before publishing, the release job runs `pnpm release:publish:preflight` to exchange a GitHub OIDC token for each unpublished package. This catches missing or mismatched npm Trusted Publisher settings before any new package version is published. - Every publishable package sets `publishConfig.provenance=true`, the publish step sets `NPM_CONFIG_PROVENANCE=true`, and the publish script passes `--provenance` explicitly to `pnpm publish`. - `npm whoami` is not a useful preflight for Trusted Publisher because OIDC authentication only exists during the publish operation. npm requires package provenance to come from a public source repository, and the package `repository` metadata must match that source repo. Commits do not trigger publishing. To publish, first consume pending changesets with `pnpm version-packages`, commit the versioned package metadata, and push that commit through normal CI. After the version commit is on `main`, create and push a release tag such as `v0.0.3`; `.github/workflows/release.yml` only listens to `v*` tag pushes and rejects tags whose commits are not reachable from `origin/main`. The release job blocks if pending changesets still exist, verifies npm OIDC access for each unpublished package, runs `pnpm release:publish`, publishes each not-yet-published workspace package with `pnpm publish --provenance`, then creates package release tags with the idempotent `changeset tag` command before pushing `@loggerjs/*` package tags. If npm returns an authentication error during publish or `pnpm release:publish:preflight`, verify that every package's Trusted Publisher settings exactly match the repository owner, repository name, workflow filename, optional environment name, and allowed action. npm checks these fields only when OIDC access is exchanged or publish is attempted. The publish script is idempotent for reruns: already published package versions are skipped before publishing the remaining packages. References: - https://docs.npmjs.com/trusted-publishers/ - https://docs.npmjs.com/generating-provenance-statements/ --- # AI Skill Source: https://github.com/jskits/loggerjs/blob/main/docs/AI-SKILL.md --- title: "AI Skill" description: "Install and use the LoggerJS AI skill with coding agents." --- # AI Skill LoggerJS ships an installable AI skill for coding agents. The skill teaches an agent how to choose LoggerJS packages, add runtime-specific logger modules, configure production logging, migrate from existing loggers, and validate the change in the target project. Use it when you want an agent to add LoggerJS to an application instead of only reading the docs. ## Install Install the skill from the GitHub repository: ```bash npx skills add jskits/loggerjs --skill loggerjs ``` Install it for a specific agent: ```bash npx skills add jskits/loggerjs --skill loggerjs --agent codex npx skills add jskits/loggerjs --skill loggerjs --agent claude-code ``` Use it once without installing: ```bash npx skills use jskits/loggerjs --skill loggerjs ``` For local development from a checkout: ```bash npx skills add . --skill loggerjs npx skills use . --skill loggerjs ``` ## What The Skill Includes - `skills/loggerjs/SKILL.md`: the main workflow for adding or migrating LoggerJS. - `skills/loggerjs/references/package-selection.md`: package and runtime selection rules. - `skills/loggerjs/references/runtime-recipes.md`: Node, browser, library, pretty, OpenTelemetry, and Sentry recipes. - `skills/loggerjs/references/production-checklist.md`: privacy, reliability, lifecycle, performance, and vendor-delivery guardrails. - `skills/loggerjs/references/migration.md`: migration from console, pino, winston, loglevel, debug, and wrappers. - `skills/loggerjs/references/troubleshooting.md`: common delivery, flush, browser, codec, and TypeScript issues. - `skills/loggerjs/scripts/inspect-loggerjs-project.mjs`: a read-only project inspector that recommends LoggerJS packages from the target app. ## Example Prompts ```text Use $loggerjs to add production-ready structured logging to this Node API. ``` ```text Use $loggerjs to migrate this React app from console warnings/errors to LoggerJS browser logging. ``` ```text Use $loggerjs to review this service's existing pino setup and propose the smallest safe LoggerJS migration. ``` ## How It Uses llms.txt The skill is intentionally short. It points agents to the LLM-friendly docs when exact API or broader design context is needed: - [llms.txt](/llms.txt): concise documentation map. - [llms-full.txt](/llms-full.txt): expanded docs and skill context for larger context windows. - [Reference](/reference/): generated package, API, and example reference. --- # LoggerJS AI Skill Source: https://github.com/jskits/loggerjs/blob/main/skills/loggerjs/SKILL.md --- name: loggerjs description: Use LoggerJS in JavaScript or TypeScript projects. Trigger when adding structured logging, choosing LoggerJS packages for Node.js, browser, worker, edge, CLI, or library code, configuring transports, integrations, processors, codecs, OpenTelemetry or vendor delivery, migrating from console, pino, winston, loglevel, debug, or other loggers, or troubleshooting LoggerJS logging behavior. --- # LoggerJS ## Overview Use this skill to integrate LoggerJS into a real JavaScript or TypeScript codebase with the smallest correct package set, a runtime-appropriate logger module, production guardrails, and repo-local validation. LoggerJS is an isomorphic structured logging toolkit: a zero-dependency core plus platform packages for Node.js, browsers, workers, edge runtimes, transports, integrations, processors, codecs, and vendor delivery. ## Workflow 1. Inspect the target project before changing code. - Run `node /scripts/inspect-loggerjs-project.mjs ` when the skill files are available locally. - Otherwise inspect `package.json`, lockfiles, framework config, existing logger dependencies, and current logging call sites manually. 2. Choose the minimum package set. - Read `references/package-selection.md` when runtime, package, or vendor choice is not obvious. - Prefer `@loggerjs/node` for Node services/CLIs, `@loggerjs/browser` for frontend apps, and `@loggerjs/core` for runtime-neutral libraries. 3. Add one centralized logger module per runtime boundary. - Keep app code importing from the local logger module, not directly from many LoggerJS package paths. - Platform packages re-export core APIs, so start with one platform package unless a vendor or processor package is needed. 4. Configure production safety before broad rollout. - Read `references/production-checklist.md` before adding browser delivery, vendor delivery, persistent queues, or automatic capture. - Add redaction/privacy processors before transports that leave the process. - Never place vendor API keys, long-lived tokens, or private ingestion credentials in browser code. 5. Migrate incrementally. - Read `references/migration.md` when replacing console, pino, winston, loglevel, debug, consola, or a custom wrapper. - Preserve existing call-site semantics first, then improve structure, context, and delivery. 6. Validate with the target repo's own gates. - Run the package manager install command if dependencies changed. - Run typecheck/build/tests or the closest project-local equivalents. - For browser apps, verify no secret-bearing environment variables are bundled client-side. ## Reference Map - `references/package-selection.md`: package matrix, runtime detection, install commands, and vendor package choices. - `references/runtime-recipes.md`: minimal Node, browser, library, local pretty, OpenTelemetry, and Sentry recipes. - `references/production-checklist.md`: privacy, reliability, lifecycle, performance, and browser credential guardrails. - `references/migration.md`: incremental migration patterns from existing loggers. - `references/troubleshooting.md`: common missing-log, duplicate-log, browser, flush, codec, and delivery failures. Use the public docs when exact API details or broader context are needed: - Concise map: `https://jskits.github.io/loggerjs/llms.txt` - Full context: `https://jskits.github.io/loggerjs/llms-full.txt` - Package/API reference: `https://jskits.github.io/loggerjs/reference/` ## Implementation Rules - Prefer TypeScript examples and ESM imports. - Keep LoggerJS setup in a small module such as `src/logger.ts`, `src/lib/logger.ts`, or the framework's existing observability/logging module. - Keep disabled-level hot paths cheap: avoid eager string interpolation, `JSON.stringify`, expensive context builders, or stack parsing before the logger level gate. - Use lazy messages or structured data for expensive values. - Use processors only when event-level behavior is needed. No-processor record paths are the fastest path. - Treat integrations as opt-in automatic capture. Add only the platform hooks the app actually needs. - Treat codecs as transport-owned. Do not pre-stringify records in application code before LoggerJS sees them. - Pair remote transports with bounded batching, retry/backoff, circuit-breakers, or offline queues where appropriate. - Make `flush()` part of controlled shutdown, tests that assert delivery, CLI exit paths, and requestless scripts. - In libraries, prefer `getLogger(["package-or-feature"])`; do not force host applications to configure transports. ## Minimal Patterns Node service: ```ts import { captureProcessIntegration, createLogger, stdoutTransport } from "@loggerjs/node"; import { redactProcessor } from "@loggerjs/processors"; export const logger = createLogger({ name: "api", level: process.env.LOG_LEVEL ?? "info", tags: { service: "api", env: process.env.NODE_ENV ?? "dev" }, processors: [redactProcessor()], transports: [stdoutTransport()], integrations: [captureProcessIntegration()], }); ``` Browser app: ```ts import { browserHttpTransport, captureBrowserErrorsIntegration, captureFetchIntegration, createLogger, pageLifecycleIntegration, } from "@loggerjs/browser"; import { redactProcessor } from "@loggerjs/processors"; export const logger = createLogger({ name: "web", level: "info", processors: [redactProcessor()], transports: [ browserHttpTransport({ url: "/api/logs", maxBatchSize: 20, flushIntervalMs: 1500, useBeaconOnPageHide: true, }), ], integrations: [ captureBrowserErrorsIntegration(), captureFetchIntegration(), pageLifecycleIntegration(), ], }); ``` Library code: ```ts import { getLogger } from "@loggerjs/core"; const logger = getLogger(["my-library"]); export function doWork() { logger.debug("work started"); } ``` ## Output Expectations When reporting the result, include the files changed, the selected LoggerJS packages, the runtime assumptions, and the validation commands that passed or could not be run. --- # LoggerJS AI Skill Package Selection Source: https://github.com/jskits/loggerjs/blob/main/skills/loggerjs/references/package-selection.md # Package Selection Use the smallest package set that matches the target runtime and delivery path. ## Runtime Matrix | Target | Start with | Add when needed | | --- | --- | --- | | Node service, API, worker process, CLI | `@loggerjs/node` | `@loggerjs/processors`, `@loggerjs/pretty`, `@loggerjs/otel`, vendor transports | | Browser SPA, frontend route, web widget | `@loggerjs/browser` | `@loggerjs/processors`, browser-safe vendor proxy transport | | Shared library with no delivery ownership | `@loggerjs/core` | Usually none; the host app owns transports | | Isomorphic app with separate client/server bundles | `@loggerjs/node` on server and `@loggerjs/browser` on client | Shared local wrapper types if needed | | Edge or worker runtime without Node APIs | `@loggerjs/core` or `@loggerjs/browser` if browser APIs exist | Custom fetch transport or platform-specific delivery | | Local developer pretty logs | Runtime package plus `@loggerjs/pretty` | Keep production structured transport separate | ## Common Add-ons | Need | Package | | --- | --- | | Redaction, sampling, routing, dedupe, fingerprinting, buffering | `@loggerjs/processors` | | MessagePack or richer serialization helpers | `@loggerjs/codecs` | | OpenTelemetry trace mapping or OTLP HTTP logs | `@loggerjs/otel` | | Sentry logs, breadcrumbs, or captured exceptions | `@loggerjs/sentry` | | Datadog Logs API | `@loggerjs/datadog` | | Elasticsearch bulk indexing | `@loggerjs/elastic` | | Grafana Loki push API | `@loggerjs/loki` | | Amazon CloudWatch Logs | `@loggerjs/cloudwatch` | | SQLite, PostgreSQL, or custom database sink | `@loggerjs/database` | ## Install Commands Use the package manager already used by the repo. ```bash npm install @loggerjs/node @loggerjs/processors pnpm add @loggerjs/node @loggerjs/processors yarn add @loggerjs/node @loggerjs/processors bun add @loggerjs/node @loggerjs/processors ``` For browsers: ```bash npm install @loggerjs/browser @loggerjs/processors ``` For libraries: ```bash npm install @loggerjs/core ``` ## Selection Rules - If the project has `express`, `fastify`, `koa`, `hapi`, `nestjs`, `prisma`, `bullmq`, or Node server scripts, choose `@loggerjs/node`. - If it has `react`, `vue`, `svelte`, `vite`, `next`, `nuxt`, `angular`, `astro`, browser entry files, or `window`/`document` logging, choose `@loggerjs/browser` for the client bundle. - If it has both SSR/server and browser code, configure separate logger modules for server and client. - If the package is a published library, has `exports` or `main`, and should not decide delivery, use `@loggerjs/core` with `getLogger()`. - If migrating from an existing logger, install LoggerJS beside it first and preserve the old API through a local adapter when that reduces call-site churn. - Do not install every vendor package up front. Add vendor transports only when the destination is known and credentials/configuration exist. ## Credential Boundaries - Browser code may send to your own ingestion endpoint, not directly to services that require private ingestion credentials. - Node services may own vendor credentials through server-side environment variables. - For edge runtimes, confirm whether the platform permits the needed network APIs and environment-secret model before adding vendor delivery. --- # LoggerJS AI Skill Runtime Recipes Source: https://github.com/jskits/loggerjs/blob/main/skills/loggerjs/references/runtime-recipes.md # Runtime Recipes Use these as starting points, then adjust names, tags, transports, and integrations to the target app. ## Node Service ```ts import { captureProcessIntegration, createLogger, stdoutTransport } from "@loggerjs/node"; import { redactProcessor, tagsProcessor } from "@loggerjs/processors"; const logger = createLogger({ name: "api", level: process.env.LOG_LEVEL ?? "info", tags: { service: "api", env: process.env.NODE_ENV ?? "dev" }, processors: [redactProcessor(), tagsProcessor({ runtime: "node" })], transports: [stdoutTransport()], integrations: [captureProcessIntegration()], }); logger.info("server started", { port: 3000 }); await logger.flush(); ``` Use stdout first for container platforms that already collect process output. Add OTLP, Loki, Datadog, CloudWatch, or database transports only when the deployment needs direct delivery. ## Browser App ```ts import { browserHttpTransport, captureBrowserErrorsIntegration, captureConsoleIntegration, captureFetchIntegration, createLogger, pageLifecycleIntegration, } from "@loggerjs/browser"; import { redactProcessor, sampleProcessor } from "@loggerjs/processors"; export const logger = createLogger({ name: "web", level: "info", tags: { app: "web" }, processors: [ redactProcessor(), sampleProcessor({ rates: { trace: 0.05, debug: 0.2, info: 1, warn: 1, error: 1, fatal: 1 } }), ], transports: [ browserHttpTransport({ url: "/api/logs", maxBatchSize: 20, flushIntervalMs: 1500, useBeaconOnPageHide: true, }), ], integrations: [ captureConsoleIntegration({ levels: ["warn", "error"] }), captureBrowserErrorsIntegration(), captureFetchIntegration(), pageLifecycleIntegration(), ], }); ``` Use a server-owned ingestion endpoint for browser logs. Add IndexedDB/offline persistence when reload survival matters and the app can tolerate storage quota failures. ## Library ```ts import { getLogger } from "@loggerjs/core"; const logger = getLogger(["my-library"]); export function parseInput(value: unknown) { logger.debug("parse input", { valueType: typeof value }); } ``` Library code should not configure transports globally. Let the host app call `configure()` or create application loggers. ## Local Pretty Output ```ts import { createLogger } from "@loggerjs/node"; import { prettyStdoutTransport } from "@loggerjs/pretty"; export const logger = createLogger({ name: "dev", level: "debug", transports: [prettyStdoutTransport()], }); ``` Keep pretty output for local development. Production should still have structured delivery. ## OpenTelemetry Delivery ```ts import { createLogger, stdoutTransport } from "@loggerjs/node"; import { openTelemetryTraceProcessor, otlpHttpTransport } from "@loggerjs/otel"; export const logger = createLogger({ name: "api", processors: [openTelemetryTraceProcessor()], transports: [ stdoutTransport(), otlpHttpTransport({ url: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT ?? "http://localhost:4318/v1/logs", resource: { "service.name": "api" }, }), ], }); ``` Use this when the app already has OpenTelemetry tracing or an OTLP collector. ## Sentry Delivery ```ts import { createLogger } from "@loggerjs/core"; import { sentryTransport } from "@loggerjs/sentry"; import * as Sentry from "@sentry/node"; export const logger = createLogger({ name: "api", transports: [sentryTransport({ sentry: Sentry, captureMessages: true })], }); ``` Use Sentry as an error/alerting path, not as the only high-volume structured-log sink unless the project has accepted that cost and retention model. --- # LoggerJS AI Skill Production Checklist Source: https://github.com/jskits/loggerjs/blob/main/skills/loggerjs/references/production-checklist.md # Production Checklist Apply this before broad rollout or before adding automatic capture. ## Privacy - Add `redactProcessor()` or a stricter privacy processor before any remote transport. - Redact tokens, cookies, authorization headers, passwords, secrets, session IDs, and user-provided free-form fields. - Keep raw request/response bodies out of default logs unless the app has a clear data classification rule. - In browser code, assume all bundled environment variables and network payloads are inspectable by users. ## Reliability - Use bounded queues. Choose an explicit drop policy instead of allowing unbounded memory growth. - Pair network transports with batch size, flush interval, retry/backoff, and circuit-breaker settings appropriate to the destination. - Call `flush()` on controlled shutdown, CLI exit, worker teardown, and tests that assert delivery. - Use crash-path synchronous flush only for local process-safe transports; remote network delivery is best-effort during crashes. - Track LoggerJS self-metrics, drop counters, and transport errors in the app's existing observability path. ## Browser Lifecycle - Enable page lifecycle integration when logs matter during tab close or navigation. - Treat `sendBeacon` as a last-chance attempt, not a delivery guarantee. - Use IndexedDB/offline queues when reload survival matters, but handle quota, private browsing, eviction, and blocked upgrades. - Prefer sampling or rate limits for high-volume browser debug/info events. ## Performance - Keep expensive message construction behind the logger level gate. - Prefer structured data over preformatted strings when fields are later redacted, routed, or indexed. - Use processors only when event-level behavior is required; processor use disables the pure record fast path. - Keep hot-path browser integrations scoped to needed signals. - Benchmark if changing codec, batching, sampling, or high-volume call sites. ## Vendor Delivery - Server-side vendor packages may use private credentials from environment variables. - Browser logs should flow through an application-owned ingestion endpoint or a public-safe token model. - Name the target service and region/site explicitly for Datadog, CloudWatch, Loki, Elasticsearch, Sentry, or OTLP. - Add a fallback or secondary transport when delivery is operationally critical. --- # LoggerJS AI Skill Migration Source: https://github.com/jskits/loggerjs/blob/main/skills/loggerjs/references/migration.md # Migration Migrate in slices. Keep existing call-site behavior working first, then improve structure. ## From `console` 1. Add a local logger module. 2. Replace new or touched `console.log`, `console.warn`, and `console.error` calls with `logger.info`, `logger.warn`, and `logger.error`. 3. For broad capture, add console integration with a narrow level set such as `["warn", "error"]`. 4. Keep debug/info console capture off by default in production unless sampled. Example: ```ts logger.info("order created", { orderId }); logger.error(error, "payment failed", { orderId }); ``` ## From pino Map these first: | pino concept | LoggerJS direction | | --- | --- | | `level` | `level` in `createLogger()` | | bindings/base fields | `tags`, context provider, or local wrapper | | serializers | processor or middleware | | destination/transport | LoggerJS transport | | `child()` | separate named/category logger or wrapper adding tags/context | | `flush()` | `await logger.flush()` | Preserve the existing wrapper API if many call sites use pino-specific argument ordering. Replace internals first, then gradually convert call sites to structured LoggerJS calls. ## From winston Map these first: | winston concept | LoggerJS direction | | --- | --- | | format pipeline | processors/middleware plus transport-owned codec | | transports array | LoggerJS transports array | | exception/rejection handlers | Node process integration | | defaultMeta | tags or context provider | | custom format redaction | `redactProcessor()` or privacy processor | Do not pre-stringify in a format step before LoggerJS. Keep raw fields available for redaction, routing, and transport-specific codecs. ## From loglevel/debug/consola/tslog - Keep the same public wrapper names if they are widely used. - Map namespaces to `name` or category tags. - Replace string-only calls with structured data where the surrounding code already has field values. - Use `@loggerjs/pretty` for local developer ergonomics when the old logger was mainly human-readable. ## Safe Rollout Pattern 1. Add LoggerJS alongside the old logger. 2. Create an adapter with the old logger's common methods. 3. Send to stdout or a test/memory transport first. 4. Add redaction and production transport. 5. Switch one module or route at a time. 6. Remove the old logger dependency only after call sites and tests no longer import it. --- # LoggerJS AI Skill Troubleshooting Source: https://github.com/jskits/loggerjs/blob/main/skills/loggerjs/references/troubleshooting.md # Troubleshooting ## No Logs Appear - Confirm the logger `level` allows the emitted level. - Confirm at least one transport is configured. - Confirm the app imports the configured local logger, not a fresh unconfigured logger. - In libraries using `getLogger()`, confirm the host app configured the registry. - Await `logger.flush()` in scripts, tests, and short-lived processes. ## Duplicate Logs - Check whether both direct LoggerJS calls and console integration are capturing the same message. - Ensure integration setup runs once per app lifecycle, not once per request or React render. - In hot reload/dev servers, keep teardown logic or guard initialization with module-level state. ## Browser Delivery Fails - Check network status, CORS, ingestion endpoint path, response status, and content type expectations. - Do not send browser logs directly to private vendor APIs that require secret credentials. - Enable page lifecycle flush for navigation/tab-close cases. - Use an offline queue only after deciding quota and drop behavior. ## Process Exits Before Delivery - Await `logger.flush()` before `process.exit()`. - For CLIs, set `process.exitCode` and return instead of calling `process.exit()` immediately. - Use stdout/file transports for crash-path reliability; remote network transports are best-effort during fatal shutdown. ## Encoding or Serialization Errors - Do not call `JSON.stringify()` on log data before passing it to LoggerJS. - Use built-in codecs or transport-owned codecs so Error, BigInt, circular values, and rich objects can be handled safely. - Check LoggerJS meta counters when payloads are dropped, coerced, or encoded with fallback behavior. ## TypeScript Import Errors - Use public package exports, not `dist` or source-internal paths. - Re-run install after adding a LoggerJS package. - For exact exports, check `https://jskits.github.io/loggerjs/reference/packages` and the package API report.