@easyweb/logging

中文文档
1.0.2发布于 2026/09/18MITTypeScriptESM + CJS零依赖近 30 天下载 3419

Shared structured logging for Easyweb microservices: one pino configuration, request-scoped context over AsyncLocalStorage, an HTTP access log, and the redaction every service needs before its logs leave the pod

原始使用文档
文档目录6 节
  1. Why it exists
  2. The mixin
  3. Usage
  4. The access log
  5. Redaction
  6. What is deliberately NOT redacted

@easyweb/logging

One pino configuration for every Easyweb service, request-scoped context that does not have to be threaded by hand, an HTTP access log, and the redaction a log needs before it leaves the pod.

Why it exists

src/lib/logger.ts was copy-pasted into 21 services. It had drifted into two variants; domain and moderation both defaulted their service name to "billing-service"; and seven services were missing the guard that stops pino-pretty's worker thread making Jest exit 1 under a green report.

The bigger problem was correlation. getLogger(req) returned a child bound to requestId and had to be passed down as an explicit log parameter. Roughly 100 call sites did that; the other ~1,300 logged through the module-level root logger and carried no request id at all.

The mixin

createServiceLogger installs a pino mixin that reads an AsyncLocalStorage store. Every existing call site gains the ambient fields with no edit:

// unchanged call site
log.info({ projectId }, "Project created");

// what it now emits
{ "service": "project-service", "requestId": "…", "userId": "…", "projectId": "…" }

pino merges the mixin UNDER the object the call site passed, so an explicit field always wins over an ambient one.

The mixin hands pino a COPY of the scope (1.0.2). pino's default merge is Object.assign(mixinObject, mergeObject), and 1.0.1 returned the live store — so every call's fields were written into the request's context and appeared on every later line of that request, an err included. It also skips any key the logger already binds, which is why a getLogger(req) line or a subscriber's child({ requestId, queue }) line now carries requestId once, not twice.

Usage

// src/lib/logger.ts
import { createServiceLogger } from "@easyweb/logging";
import config from "../config";

const { logger, getLogger } = createServiceLogger({
  serviceName: config.serviceName,
  logLevel: config.logLevel,
});

export { getLogger };
export default logger;
// src/app.ts — the access log needs the scope, so it goes after requestContext
app.use(requestContext);
app.use(createHttpLogger(logger));

Opening a scope outside a request — a BullMQ job, a broker handler:

await runWithContext({ jobId: job.id, job: job.data.type }, () => handle(job));

Adding to a scope already running — authenticate does this once it has verified the token:

bindContext({ userId: decoded.sub });

The access log

One line per finished request, msg: "request".

route is the route pattern (/billing/me/invoices/:invoiceId), and that is a contract rather than a convenience: the RED metrics are recording rules over it, so a per-id value would make the series unbounded. path carries the real path beside it, from req.originalUrl — Express strips the mount prefix off req.path during router dispatch, so reading that in the finish callback names a route that does not exist.

The query string is never logged. ?token=… on the verification route is a live credential.

/health, /livez, /readyz and /metrics are skipped — the kubelet would otherwise make the readiness probe the largest log stream in the cluster.

Redaction

Two mechanisms, covering different shapes:

  • REDACT_PATHS — pino's fixed-path redaction, for our own payloads. Passwords, tokens, cookies and auth headers, at the top level and one level down.
  • scrubResponseData — a recursive, depth- and size-capped walk over err.response.data, for payloads we do not control. Xendit, Stripe, Resend, Meta, Apify, Cloudflare and Gitea all put personal data in an error body, and 407 logger.error({ err }) call sites fed it into the log verbatim and uncapped.

err.response.data is capped at 2 KB after scrubbing, not instead of it — truncating first would keep whatever fitted, which for a failed charge is the payer block.

What is deliberately NOT redacted

Email addresses. auth-service logs one on three lines — signup, an operator-create refusal, and a Google registration — and they are the signup audit trail: userId alone cannot answer "which address did they sign up with". The exposure is real and bounded by log retention. See docs/platform/ADR-0001.

版本与发布记录

@easyweb/logging 共发布 3 个版本,首次发布于 2026/08/27;过去 12 个月发布了 3 个版本。

版本发布日期距今
1.0.2latest2026/09/181 个月前
1.0.12026/08/272 个月前
1.0.02026/08/272 个月前

关于 @easyweb/logging 的常见问题

@easyweb/logging 是什么?

Shared structured logging for Easyweb microservices: one pino configuration, request-scoped context over AsyncLocalStorage, an HTTP access log, and the redaction every service needs before its logs leave the pod

如何安装 @easyweb/logging?

在项目目录运行 npm install @easyweb/logging,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:pino(^10.0.0)、express(^5.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@easyweb/logging 支持 TypeScript 吗?

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

@easyweb/logging 支持 ESM 还是 CommonJS?

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

@easyweb/logging 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=18.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@easyweb/logging 可以商用吗?

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

@easyweb/logging 的下载量有多少?

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

@easyweb/logging 还在维护吗?

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