Roarr
A zero-configuration JSON logger for Node.js and the browser that decouples log production from transport.
Repository Health
Technical Analysis
Roarr is a JSON logger designed to be safe to use inside library code, not just application code. Unlike Winston, Bunyan, or Pino, it requires no instantiation or configuration at the call site — you simply import { Roarr as log } from 'roarr' and call it. Logging is disabled by default and only turned on via the ROARR_LOG environment variable, so libraries that depend on Roarr never impose logging overhead or configuration burden on the applications that consume them.
In Node.js, Roarr writes newline-delimited JSON to stdout (or stderr) and leaves filtering and pretty-printing to an external process, the companion @roarr/cli tool. In the browser, consumers implement a ROARR.write callback to receive messages. A built-in adopt() API layers on Node’s async_hooks to propagate context and hierarchical sequence IDs (in Postgres ltree format) across nested asynchronous operations, letting logs from concurrent async branches be reconstructed into a causal order after the fact.
What You Get
- A logger function usable identically in Node.js and browser code with zero setup
- Environment-variable-gated logging (
ROARR_LOG,ROARR_STREAM) so consuming applications opt in explicitly child()loggers that merge in additional context objects or apply message-transform functions (e.g. error serialization middleware)adopt()for propagating context and hierarchical sequence IDs across async boundaries viaasync_hooks- Level convenience methods (
tracethroughfatal) plus deduplicating*Oncevariants capped at 1,000 tracked keys - A pluggable
ROARR.write/ROARR.serializeMessagesurface so any transport or custom serializer can be swapped in
Common Use Cases
- Adding structured logging to a published npm package without forcing consumers to configure a logging framework
- Correlating logs across nested async operations in a Node.js service using
adopt()and its ltree-style sequence IDs - Piping stdout through the
@roarr/clicompanion tool to filter and pretty-print logs bycontext.logLevelduring local development - Capturing structured logs from browser code by overriding
globalThis.ROARR.write - Building a shared
Logger.jssingleton with achild()-derived instance carrying application-level context (name, instance ID)
Under The Hood
Architecture
Roarr centers on a single shared function prototype (loggerPrototype in src/factories/createLogger.ts) that every logger instance’s bound logMessage function inherits via Object.setPrototypeOf, rather than each logger closing over its own copy of the level methods — a deliberate micro-optimization documented inline to avoid per-instance allocation and V8 map-transition cost. State (parent context, transforms, onMessage callback) is threaded through .bind() and mirrored as own properties on the function object so prototype methods can read this.onMessage directly. child() and adopt() both read from either a global RoarrGlobalState (globalThis.ROARR) or, when async_hooks is available, an AsyncLocalStorage-backed AsyncLocalContext, merging parent, async-local, and per-call context in a fixed precedence order before handing the assembled packet to the injected onMessage writer — the same function is reused unchanged for Node.js (createNodeWriter.ts, writing newline-delimited JSON to stdout/stderr) and browser (browser.ts, delegating to a user-supplied ROARR.write) entry points, with isBrowser() as the sole branch point in createLogger.
Tech Stack
The package is plain TypeScript (98% by byte count) with a minimal runtime dependency surface: fast-printf for %s/%d-style message interpolation, safe-stable-stringify for circular-reference-safe JSON serialization, and semver-compare for internal version checks. It builds with tsc against a dedicated tsconfig.build.json, ships both src and compiled dist in the published package, and exposes separate main (dist/Roarr.js) and browser (dist/browser.js) entry points in package.json so bundlers pick the browser-safe build automatically. There is no framework dependency of any kind — the only Node.js-specific API used at runtime is async_hooks.
Code Quality
Tests run under AVA (ava --serial --verbose) with coverage tracked via nyc/@istanbuljs/nyc-config-typescript, and are organized under test/roarr/ mirroring the src/ layout, including a dedicated integration suite (test/roarr/integrations/) that exercises write-disabling behavior, sequence updates, and message-handler overrides end to end rather than only unit-level. Linting runs eslint-config-canonical’s strict ruleset plus knip for unused-export detection, and the codebase is fully typed with exported Logger/MessageContext/RoarrGlobalState types rather than any at its public boundaries (internal helper functions do use any for the variadic printf-argument plumbing, which is called out and permitted via an eslint override). Error handling favors explicit thrown Error/TypeError with descriptive messages over silent failures (e.g. the printf-format-mismatch check in logMessage), and a benchmark script (test/benchmark.ts, run via ROARR_LOG=true tsx) exists specifically to guard the performance-sensitive hot path.
What Makes It Unique
Roarr’s defining technical choice is refusing to support in-process transports at all — log lines go to stdout/stderr (or a browser callback) and nothing else, on the stated rationale that Node.js is single-threaded and in-process log shipping blocks the event loop; all fan-out to Elasticsearch, syslog, or other sinks is explicitly pushed to external processes like Beats or Fluentd. Combined with this is the adopt()/async_hooks mechanism for hierarchical, ltree-formatted sequence IDs, which lets concurrent async branches be causally ordered from the log stream alone after the fact — a capability oriented specifically at diagnosing async control flow that most JSON loggers (Pino, Winston, Bunyan) don’t attempt, since they assume single-threaded synchronous call-order logging is sufficient.