Log

Structured JSON application logger meant to be ingested by a log aggregator. Each emitted line is a single JSON object terminated with \n; the log level is encoded as the string field level, not as console color. Distinct from b.log.boot — that path is framework-internal startup chatter to the TTY (humans watching npm start); create() is what apps wire into their request lifecycle.

Levels: debug (0) < info (1) < warn (2) < error (3) < fatal (4). Default routing: debug / info / warn → stdout; error / fatal → stderr. Multi-sink config (sinks: [...]) takes full control of routing — each sink gets every line at-or-above its own per-sink level, useful when the operator wants debug to a file but warn+ to stderr.

Redact-aware: extras passed to .info(msg, extras) flow through b.redact by default, so password / token / cardNumber-shaped keys never reach the log line. Operators opt out with redact: false only when the logger sits behind a downstream redactor.

Request correlation rides on Node's AsyncLocalStorage. The middleware allocates a requestId (or honors an inbound X-Request-Id header) and binds it for the entire async chain; every log.info inside the request automatically picks up the id without the caller threading it explicitly. OpenTelemetry trace correlation rides the same channel — runWithContext merges arbitrary fields (tenantId, traceId) into the bound store.

Child loggers via log.bind({ component: "auth" }) carry the bound fields into every emitted line; chains compose, so an auth-handler logger can bind its own userId on top.

Field merge order (last wins): base context → bound chain → ALS store → caller's extras → core fields (timestamp / level / message). Extras that try to clobber a core field are dropped and the line carries _overwriteAttempt: true so misconfig is visible.

Trojan-Source defense (CVE-2021-42574) is baked in: Unicode bidi / format controls in messages are escaped to \uXXXX literals before they reach the wire so a hostile message can't re-order the visible line in a TTY / syslog reader.

b.log.create(opts) #

stable0.1.70
{
  level:            "info",                      // string or 0-4
  base:             { service: "myapp" },        // merged into every line
  redact:           true,                        // run extras through b.redact
  sinks: [
    { stream: process.stdout, level: "info" },
    { stream: fs.createWriteStream("./errors.log"), level: "error" },
  ],
  destination:      process.stdout,              // legacy single-sink
  errorDestination: process.stderr,              // legacy two-sink split
  format:           "json",
  clock:            function () { return new Date(); }, // test seam
}

Build a structured JSON logger instance. Returns an object with .debug / .info / .warn / .error / .fatal emitters, plus .bind(extra) for child loggers, .middleware() for router-side request-id binding, .runWithRequestId(id, fn) / .runWithContext(ctx, fn) for ad-hoc AsyncLocalStorage scopes, and .setLevel / .getLevel / .isLevelEnabled for runtime level control. Level resolution is LOG_LEVEL env > opts.level > "info".

var log = b.log.create({
  level: "info",
  base:  { service: "myapp", version: "1.2.3" },
});
log.info("user logged in", { userId: "u-1" });
var authLog = log.bind({ component: "auth" });
authLog.warn("rate-limited", { ip: "203.0.113.7" });
// → {"timestamp":"...","level":"info","message":"user logged in",
//    "service":"myapp","version":"1.2.3","userId":"u-1"}

b.log.boot(name) #

stable0.7.0

Framework-internal boot logger for human-readable startup chatter ([blamejs:db] ready, [blamejs:vault] WARNING: ...). TTY-aware: when stdout is a terminal it emits a prefixed line; when stdout is piped it emits a one-line JSON object so log aggregators can ingest boot chatter as structured records. The returned value is a callable (info path) plus .debug / .info / .warn / .error / .prefix members so log("ready") and log.warn("...") both work.

var log = b.log.boot("db");
log("ready");
log.warn("connection slow");
// → "[blamejs:db] ready"   (TTY)
// → {"timestamp":"...","level":"info","message":"ready",
//    "component":"db","boot":true}  (piped)

b.log.makeViaOrFallback(operatorLog, fallbackLog) #

stable0.7.30

Closure factory for operator-log routing. Used by primitives (bundler, dev server, error-page renderer, pqc-gate, ...) that accept opts.log but must keep emitting through a per-module fallback when the operator didn't pass one. The operator log call is best-effort — a misbehaving log[level] is swallowed rather than crashing the caller. Fallback fires only when the operator log is absent or doesn't expose the requested level.

var fallback = b.log.boot("bundler");
var via = b.log.makeViaOrFallback(null, fallback);
via("error", "build-failed", { reason: "missing entrypoint" });
// → "[blamejs:bundler] build-failed {\"reason\":\"missing entrypoint\"}"

b.log.getRequestId() #

stable0.1.70

Read the current AsyncLocalStorage-bound request id, or null when called outside a runWithRequestId / middleware-wrapped scope. The module-level helper exists for code paths that don't have a logger instance handy but still need to read the request-correlation token (e.g. an external SDK callback that must include the id in a remote span).

await b.log.runWithRequestId("req-abc", async function () {
  var id = b.log.getRequestId();
  // → "req-abc"
});

b.log.runWithRequestId(id, fn) #

stable0.1.70

Run fn inside an AsyncLocalStorage scope where b.log.getRequestId() returns id. Every b.log.create-built logger inside the scope automatically picks up the id on each emitted line. Returns whatever fn returns (including a Promise); the binding propagates through await boundaries via Node's async-context plumbing.

var result = await b.log.runWithRequestId("req-abc", async function () {
  return b.log.getRequestId();
});
// → "req-abc"

b.log.enterRequestId(id) #

stable0.15.21

Bind id into the AsyncLocalStorage scope for the REMAINDER of the current async execution — without nesting a callback. Where runWithRequestId(id, fn) wraps a function (and the binding closes when fn returns), this uses AsyncLocalStorage.enterWith so the id survives a dispatch model that hands control back to its caller before the awaited work runs — a boolean-next middleware chain (b.router), where the route handler executes after the middleware returns. Call it once per request, inside the per-request async context, so each request stays isolated. The companion to b.middleware.requestId({ asyncContext: true }), which calls it for you.

// inside a per-request middleware, before next():
b.log.enterRequestId(req.requestId);
// any awaited handler downstream now sees b.log.getRequestId() === req.requestId

Last updated 2026-08-08T16:39:49.652Z by seeder.