
Node.js 日志与可观测性
为什么选择结构化日志?
// 非结构化(难以查询)
[2024-01-15 10:23:45] ERROR: User 123 failed to login from 192.168.1.1
// 结构化 JSON(可查询、可解析)
{
"level": "error",
"time": "2024-01-15T10:23:45.000Z",
"msg": "Login failed",
"userId": "123",
"ip": "192.168.1.1",
"reason": "invalid_password",
"requestId": "req_abc123"
}
Pino 设置
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level(label) { return { level: label }; },
},
base: {
service: 'user-api',
env: process.env.NODE_ENV,
version: process.env.APP_VERSION,
},
// 开发环境下可读性更好
transport: process.env.NODE_ENV === 'development'
? { target: 'pino-pretty', options: { colorize: true } }
: undefined,
});
export default logger;
带上下文的子日志器
// 带有关联 ID 的请求作用域日志器
app.use((req, res, next) => {
const requestId = req.headers['x-request-id'] as string || generateId();
req.logger = logger.child({
requestId,
method: req.method,
url: req.url,
userAgent: req.headers['user-agent'],
});
res.setHeader('x-request-id', requestId);
next();
});
// 在控制器中使用
async function createUser(req: Request, res: Response) {
const log = req.logger;
log.info('Creating user', { email: req.body.email });
try {
const user = await userService.create(req.body);
log.info('User created', { userId: user.id });
res.json(user);
} catch (err) {
log.error({ err }, 'Failed to create user');
res.status(500).json({ error: 'Internal error' });
}
}
请求/响应日志记录
import pinoHttp from 'pino-http';
app.use(pinoHttp({
logger,
customLogLevel: (req, res) => {
if (res.statusCode >= 500) return 'error';
if (res.statusCode >= 400) return 'warn';
return 'info';
},
customSuccessMessage: (req, res) => {
return `${req.method} ${req.url} ${res.statusCode}`;
},
serializers: {
req: (req) => ({
id: req.id,
method: req.method,
url: req.url,
// 不记录认证头
headers: { ...req.headers, authorization: '[REDACTED]' },
}),
},
}));
错误日志记录
// 记录带有完整上下文的错误
function logError(logger: Logger, err: unknown, context?: Record<string, unknown>) {
if (err instanceof Error) {
logger.error({
err: {
message: err.message,
name: err.name,
stack: err.stack,
...('code' in err ? { code: err.code } : {}),
},
...context,
}, err.message);
} else {
logger.error({ err, ...context }, 'Unknown error');
}
}
日志级别
// 日志级别指南:
logger.trace({ query, params }, 'DB query executing'); // 非常详细
logger.debug({ userId, sessionId }, 'User session checked'); // 调试信息
logger.info({ userId }, 'User logged in'); // 正常操作
logger.warn({ attempts }, 'Rate limit approaching'); // 警告信号
logger.error({ err }, 'Payment processing failed'); // 错误
logger.fatal({ err }, 'Database connection lost'); // 系统严重
OpenTelemetry 集成
import { trace, context, propagation } from '@opentelemetry/api';
app.use((req, res, next) => {
const span = trace.getActiveSpan();
const traceId = span?.spanContext().traceId;
req.logger = logger.child({ traceId });
next();
});
结构化日志支持在 Elasticsearch、Loki 或 CloudWatch Insights 中进行强大的查询。