生产级日志记录是 Node.js 应用程序开发中最容易被忽视的方面之一。虽然 console.log 适用于本地调试,但它在日志必须可搜索、结构化和可操作的分布式环境中会崩溃。本文介绍了构建可扩展的日志记录策略的基本模式。
为什么要结构化日志记录
传统的非结构化日志记录输出难以以编程方式解析的纯文本。考虑 console.log("User logged in:", userId)。在数百个服务实例中执行 grep 操作非常缓慢且容易出错。结构化日志记录将每个日志事件输出为 JSON 对象,使其可供机器读取并可由日志聚合系统查询。
// Unstructured - avoid in production
console.log("Payment processed:", paymentId, "for user:", userId);
// Structured - preferred
logger.info({ paymentId, userId, amount, currency }, "Payment processed");
JSON 格式可以在 Elasticsearch、Loki 和 Datadog 等工具中实现基于字段的过滤、警报和仪表板,而无需自定义解析逻辑。
选择记录器:Pino 与 Winston
两个库主导了 Node.js 日志记录领域。正确的选择取决于您的性能要求和生态系统需求。
| 特色 | 皮诺 | 温斯顿 |
|---|---|---|
| 速度 | 每条日志行约 0.5 µs | 每条日志行约 3–5 µs |
| 交通 | 内置较少,可通过 pino-multi-stream 扩展 | 丰富的生态系统(文件、HTTP、Syslog、自定义) |
| 儿童伐木者 | 占位符_0 | 占位符_1 |
| 编辑 | 通过 redact 选项内置 | 需要 fast-redact 或手动方法 |
| 开发经验 | pino-pretty 用于人类可读的输出 | 内置格式选项 |
Pino 是每一微秒都至关重要的高吞吐量服务的最佳选择。当您需要复杂的传输路由或将现有基础设施与其插件模型绑定时,Winston 会表现出色。
// Pino setup
const pino = require("pino");
const logger = pino({
level: process.env.LOG_LEVEL || "info",
redact: ["password", "authorization"],
transport: process.env.NODE_ENV !== "production"
? { target: "pino-pretty" }
: undefined,
});
日志级别以及何时使用它们
遵守 RFC 5424 日志级别可确保跨服务的一致性。每个级别都表示事件的严重性和可操作性。
| 水平 | 价值 | 何时使用 |
|---|---|---|
| 占位符_0 | 60 | 应用程序崩溃迫在眉睫 |
| 占位符_0 | 50 | 50请求失败,但进程继续 |
| 占位符_0 | 40 | 40意外但非危急情况 |
| 占位符_0 | 30 | 正常运营里程碑 |
| 占位符_0 | 20 | 详细诊断信息 |
| 占位符_0 | 10 | 10非常详细的执行流程 |
通过环境变量动态控制日志级别,以便您可以增加生产中的详细程度而无需重新部署:
const LOG_LEVEL_MAP = { fatal: 60, error: 50, warn: 40, info: 30, debug: 20, trace: 10 };
const currentLevel = LOG_LEVEL_MAP[process.env.LOG_LEVEL] || LOG_LEVEL_MAP.info;
请求关联 ID
在微服务架构中,单个用户请求可能会遍历多个服务。单独记录每个服务会使调试变得不可能。解决方案是为每个传入请求分配一个唯一的关联 ID,并将其跨服务边界传播。
const { AsyncLocalStorage } = require("async_hooks");
const asyncLocalStorage = new AsyncLocalStorage();
app.use((req, res, next) => {
const correlationId = req.headers["x-correlation-id"] || crypto.randomUUID();
res.setHeader("x-correlation-id", correlationId);
req.log = logger.child({ correlationId });
asyncLocalStorage.run(new Map([["correlationId", correlationId]]), () => next());
});
使用 AsyncLocalStorage 从任何异步上下文访问相关 ID,而无需将其传递给每个函数签名。通过 HTTP 客户端将标头转发到下游服务,以便端到端跟踪在整个架构中发挥作用。
交通一体化
传输定义日志输出的去向。控制台传输是容器化环境中的默认设置,但许多生产设置需要多个目的地。
// Example: logging to both console and a remote endpoint
const pino = require("pino");
const logger = pino({
level: "info",
}, pino.multistream([
{ stream: process.stdout },
{ stream: pino.transport({ target: "pino-http", options: { uri: "https://logs.example.com" } }) },
]));
避免生产中的同步传输——它们会阻塞事件循环并抵消异步 I/O 的性能优势。
使用 ELK 和 Loki 进行集中日志记录
将日志发送到集中式系统会将原始文本转变为可搜索的可观察平台。
ELK Stack: 通过 Filebeat 或 Logstash 将 JSON 日志发送到 Elasticsearch,然后在 Kibana 中进行可视化。典型的 Docker Compose 设置包括 Elasticsearch、Logstash(带有 JSON 输入插件)和 Kibana。
Loki + Grafana: Loki 按标签而不是全文对日志流进行索引,这使得 Kubernetes 环境中的大容量日志记录更具成本效益。将其与 Grafana 配对以进行仪表板和警报。
# docker-compose.yml snippet for local ELK
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.x
logstash:
image: docker.elastic.co/logstash/logstash:8.x
kibana:
image: docker.elastic.co/kibana/kibana:8.x
错误记录和编辑
始终记录错误及其完整堆栈跟踪。当在 err 键下传递时,Pino 会自动序列化 Error 对象:
try {
await processPayment(order);
} catch (err) {
logger.error({ err, orderId: order.id }, "Payment processing failed");
}
切勿记录敏感数据。 Pino 的内置修订从输出中删除指定的路径:
const logger = pino({
redact: ["password", "authorization", "req.headers.cookie", "creditCard"],
});
性能考虑因素
日志记录就是 I/O,过多的 I/O 会降低应用程序的吞吐量。关键基准测试显示,Pino 每行日志记录时间约为 0.5 µs,而 Winston 为 3–5 µs。对于高流量端点,请考虑采样(仅记录一部分事件)并始终使用异步传输。在 warn 级别,像 logger.debug() 这样的较低级别的调用应该几乎是免费的;皮诺通过电平短路来实现这一点。
结论
结构化 JSON 日志记录、仔细的库选择、相关 ID 和集中聚合是可观察 Node.js 应用程序的基础。首先为您的工作负载选择正确的记录器,从第一天开始实施相关 ID,并将日志路由到支持基于字段的查询的系统。有了这些模式,调试生产问题就变成了运行搜索查询的问题,而不是梳理平面文件。

