@broberg/logger

中文文档
0.3.0发布于 2026/10/31MITTypeScriptESM + CJS构建来源已验证近 30 天下载 3439

Fleet server logging that cannot leak a secret: four levels, one-line JSON in production or readable text locally, env-driven level, nesting child bindings — and every message and metadata value run through @broberg/secret-scan before it leaves the proces

原始使用文档
文档目录8 节
  1. Why this exists
  2. This is not the other two log-shaped packages
  3. API
  4. Redaction by field name (since 0.3.0, F063.2)
  5. The cost of redaction — measured, not asserted
  6. Hono
  7. Guarantees
  8. Non-goals

@broberg/logger

Server logging for the broberg.ai fleet. Four levels, one shape, and a credential that LOOKS like a credential is redacted before the line is written.

Read the limit before you rely on it. Redaction is pattern-based (~38 patterns via @broberg/secret-scan): API keys, tokens, JWTs, connection strings — anything with a recognisable shape. It does not redact by field name. Measured, on the published 0.1.0:

{ token: "sk-proj-AbCd…" }   → "token": "[REDACTED:openai-api-key]"
{ gh:    "ghp_AbCd…"     }   → "gh":    "[REDACTED:github-token]"
{ auth:  "Bearer eyJ…"   }   → "auth":  "Bearer [REDACTED:jwt]"

{ password: "hemmelig123" }  → "password": "hemmelig123"      ← NOT redacted
{ apiKey:   "kort"        }  → "apiKey":   "kort"             ← NOT redacted

A user password, a PIN, a national ID — anything whose only marker is the key it sits under — passes straight through. Do not log a request body and assume this catches it. Key-name redaction is tracked as a gap; until it ships, pick the fields you log.

This paragraph replaced the sentence "a secret cannot reach the log by accident", which promised more than the mechanism delivers. A guarantee is exactly why someone stops checking, so an over-stated one is worse than none.

npm i @broberg/logger
import { createLogger } from "@broberg/logger";

const log = createLogger({ name: "discovery", pretty: process.env.NODE_ENV !== "production" });

log.info("served", { route: "/api/search", ms: 12 });
log.error(err, { route: "/api/search" });

const reqLog = log.child({ requestId });   // on every line, nests

Why this exists

It is not a wrapper around console.log — that would be 200 lines of nothing and you should just use console. Two things justify a package:

  1. Redaction by default. Every message and every metadata value is run through @broberg/secret-scan before it leaves the process. An API key pasted into a debug line, a token in a query string, an authorization header nested three levels deep in a request dump — none of them reach stdout.
  2. One shape across every app, so a line is searchable regardless of which project emitted it.

If a change serves neither, it does not belong here.

This is not the other two log-shaped packages

PackageAnswers
@broberg/loggerWhat was the server doing?
@broberg/event-logWho did what? (GDPR audit trail, business events)
@upmetrics/sdkWhat broke? (crashes, error grouping)

API

createLogger(options) → { debug, info, warn, error, child, level }

OptionDefault
name–service name, on every line
levelLOG_LEVEL, else infoan explicit option beats LOG_LEVEL
prettyfalsefalse = one-line JSON, true = readable text
redacttruesee the cost below before turning it off
sinkconsolewhere the finished line goes (injectable for tests)
bindings{}fields merged into every line
nowDateinjectable clock

Levels are debug < info < warn < error. There is no trace or fatal: four is enough, and every extra level is a decision every consumer then has to make. warn/error go to stderr, the rest to stdout.

log.error(err) serialises an Error as { name, message, stack } — never {}. Errors nested in fields are serialised too.

Redaction by field name (since 0.3.0, F063.2)

The pattern pass recognises a credential by its shape (an sk-… key, a ghp_… token, a JWT). A user's password, a PIN or a CPR number has no shape — measured on the published 0.1.0, {password:'hemmelig123'} and {apiKey:'kort'} went out verbatim. So the logger now ALSO redacts by field name, whatever the value looks like:

log.info("login", { user: { name: "ane", password: "hemmelig123" } });
// → {"…","user":{"name":"ane","password":"[REDACTED:field]"}}
  • Default list (DEFAULT_REDACT_KEYS): password, passwd, pwd, passphrase, pin, cpr, cprnr, ssn, secret, clientSecret, privateKey, authorization, cookie, set-cookie, apiKey, x-api-key, accessToken, refreshToken, idToken.
  • Case- and separator-insensitive: apiKey, api_key, API-KEY are one entry. Authorization is caught as authorization.
  • Any depth, and in child bindings (log.child({ apiKey })).
  • Extend it with one option — the defaults stay on: createLogger({ redactKeys: ["memberNumber"] }) (then member_number is caught too).
  • Both passes run. Field names first, then the ~38 patterns on the serialised line. A field that is not on the list and has no secret shape is logged verbatim — passwordChangedAt is not password.
  • redact: false turns off both.

The cost of redaction — measured, not asserted

Redaction runs ~38 regexes over each serialised line. Run npm run bench yourself; on an M1, 50 000 calls with the sink discarded:

redact: false             1.36 µs/call       733,000 calls/s
redact: true (default)    7.79 µs/call       128,000 calls/s

overhead: 6.4 µs/call (≈5.7× the un-redacted path)

The field-name pass (0.3.0), measured as an interleaved A/B on one machine (cb-2, Intel i5-7360U under load, 30 Oct 2026; old and new dist alternated three times so load hits both):

                     0.2.3 (patterns only)   0.3.0 (field names + patterns)
redact: true         208–247 µs/call         239–260 µs/call

≈ +10 % (≈ +20 µs) on a redacted call. The absolute numbers on that machine are not comparable to the M1 table above — the same bench measured the pattern pass at ~230 µs there against 7.8 µs on the M1 in August. The M1 figures have not been re-measured since; treat them as August's, not as current.

Read that honestly: redaction is the dominant cost of a log call, and it is still 128 000 lines/second — far more than any app in this fleet produces. Turn it off with redact: false only if you have a measured hot path and have decided that line cannot carry a credential. The number is here so that is a decision, not a guess.

Hono

import { requestLogger, errorLogger } from "@broberg/logger/hono";

app.use("*", requestLogger(log, { skip: (c) => c.req.path === "/health" }));
app.onError(errorLogger(log));            // <- do not skip this, see below

One line per request: method, path, status, ms, requestId. The level follows the status (5xx → error, 4xx → warn, else info). A request id is taken from an incoming x-request-id if the caller sent one, otherwise generated, and is echoed back on the response. The handler gets a request-scoped child logger on c.get("log"), so anything it logs correlates with its request.

requestLogger alone cannot tell you WHY a 500 happened. Hono catches a throwing handler itself and turns it into a 500 response, so await next() resolves normally and the exception never reaches the middleware — you get status: 500 and no reason. Wire errorLogger as app.onError to log the exception with its stack, correlated by request id. This limitation is asserted in the test suite so it cannot be quietly forgotten.

hono is an optional peer — only importing /hono pulls it in.

Guarantees

  • It never throws. A broken sink, a circular reference, an object whose toJSON throws — all swallowed. This sits in the request path of every app, and a logger that crashes the request is strictly worse than no logger, because the app worked before you added observability.
  • pretty: false emits exactly one line of valid JSON per call.

Non-goals

No transports, rotation, buffering, sampling or aggregation. stdout is the contract; the platform collects it. If you find yourself adding any of those, this is the wrong package.


Plan: docs/features/F063-logger.md

版本与发布记录

@broberg/logger 共发布 6 个版本,首次发布于 2026/08/11;过去 12 个月发布了 6 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.3.0latest2026/10/319 天前
0.2.32026/09/111 个月前
0.2.22026/09/101 个月前
0.2.02026/08/282 个月前
0.1.12026/08/112 个月前
0.1.02026/08/112 个月前

关于 @broberg/logger 的常见问题

@broberg/logger 是什么?

Fleet server logging that cannot leak a secret: four levels, one-line JSON in production or readable text locally, env-driven level, nesting child bindings — and every message and metadata value run through @broberg/secret-scan before it leaves the proces

如何安装 @broberg/logger?

在项目目录运行 npm install @broberg/logger,或使用 pnpm add / yarn add / bun add。

@broberg/logger 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@broberg/logger 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@broberg/logger 可以商用吗?

@broberg/logger 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@broberg/logger 的下载量有多少?

根据 npm 官方下载统计,@broberg/logger 在 2026/10/09 – 2026/11/07 这 30 天内被下载 3,439 次,日均约 115 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@broberg/logger 还在维护吗?

最新版本 0.3.0 发布于 2026/10/31(9 天前);至今共发布 6 个版本,其中过去 12 个月发布了 6 个;首次发布于 2026/08/11。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。