日志记录

日志是应用程序运行过程中产生的记录,用于追踪事件、诊断问题、监控行为和审计。在生产环境中,日志是不可或缺的工具。本章将介绍 Node.js 中日志记录的最佳实践,包括选择合适的日志库、配置日志级别、结构化日志输出,以及与 Express 等框架集成。

1. 为什么需要日志?

日志的主要用途包括:

  • 调试:开发阶段快速定位问题。
  • 监控:实时了解应用状态,发现异常。
  • 审计:记录用户操作,满足合规要求。
  • 性能分析:记录关键操作的耗时。
  • 错误追踪:捕获未处理异常和错误。

相比简单的 console.log,专业的日志库提供了日志级别、格式化、输出目标(文件、控制台、远程服务)等功能。

2. 日志级别

日志级别用于区分日志的重要性,常见的级别从低到高依次为:

  • debug:详细的调试信息,开发环境使用。
  • info:常规信息,如服务启动成功、请求处理完成。
  • warn:警告,表示可能出现问题,但应用仍能运行。
  • error:错误,功能不可用,需要立即关注。
  • fatal:致命错误,导致应用崩溃。

通过设置日志级别,可以控制输出的信息量,例如生产环境通常只记录 warn 及以上级别。

3. 常用 Node.js 日志库

社区中有多个成熟的日志库,各有特点。以下是最常用的三个:

3.1 Winston

Winston 是最受欢迎的日志库之一,功能丰富,支持多种传输(transport),包括控制台、文件、HTTP、数据库等。它具有可扩展的架构和强大的日志级别管理。

安装:npm install winston

const winston = require('winston');

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

logger.info('应用启动');
logger.error('发生错误');

3.2 Pino

Pino 以性能著称,非常适合对性能要求高的场景。它生成 JSON 格式的日志,并通过与传输(如 pino-pretty)结合提供可读性。

安装:npm install pino

const pino = require('pino');
const logger = pino({
  level: 'info',
  transport: {
    target: 'pino-pretty',
    options: {
      colorize: true
    }
  }
});

logger.info('应用启动');

生产环境通常直接输出 JSON,由日志收集系统处理。

3.3 log4js

log4js 是 Apache log4j 的 JavaScript 移植,支持多 Appender(输出目标)和布局配置,适合习惯 Java 日志风格的开发者。

安装:npm install log4js

const log4js = require('log4js');
log4js.configure({
  appenders: {
    out: { type: 'stdout' },
    app: { type: 'file', filename: 'application.log' }
  },
  categories: {
    default: { appenders: ['out', 'app'], level: 'info' }
  }
});

const logger = log4js.getLogger();
logger.info('应用启动');

4. 在 Express 应用中使用日志

将日志集成到 Express 中可以记录每个请求的详细信息,便于调试和监控。可以使用 morgan 中间件记录访问日志,并与自定义日志库结合。

const express = require('express');
const morgan = require('morgan');
const winston = require('winston');

const app = express();

// 创建 Winston 日志实例
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [new winston.transports.Console()]
});

// 使用 morgan 记录 HTTP 请求,输出到 winston
app.use(morgan('combined', {
  stream: { write: message => logger.info(message.trim()) }
}));

app.get('/', (req, res) => {
  logger.info('处理根路由');
  res.send('Hello World');
});

app.listen(3000);

5. 结构化日志

结构化日志(通常为 JSON)便于机器解析,可被日志聚合工具(如 ELK Stack、Splunk)高效索引和查询。建议生产环境使用 JSON 格式输出。

logger.info({ event: 'user_login', userId: 123, ip: req.ip });

Pino 默认输出 JSON,Winston 可通过配置实现。

6. 日志轮转

如果不加管理,日志文件会无限增长,耗尽磁盘空间。日志轮转(log rotation)可以按大小或时间分割日志文件,并删除旧文件。对于 Winston,可以使用 winston-daily-rotate-file 传输。

npm install winston-daily-rotate-file
const DailyRotateFile = require('winston-daily-rotate-file');

const transport = new DailyRotateFile({
  filename: 'application-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  maxSize: '20m',
  maxFiles: '14d'
});

const logger = winston.createLogger({
  transports: [transport]
});

对于直接文件输出的日志,也可使用操作系统工具如 logrotate 进行轮转。

7. 集中式日志管理

在分布式系统或微服务架构中,需要将各服务的日志集中收集和分析。常见方案包括:

  • ELK Stack:Elasticsearch + Logstash + Kibana
  • Graylog
  • Splunk
  • 云服务:AWS CloudWatch、Azure Monitor 等

这些工具通常接收 JSON 格式日志,并提供强大的搜索和可视化能力。

8. 日志最佳实践

  • 选择合适的日志级别:生产环境避免输出过多 debug 信息,但确保 error 级别日志详尽。
  • 记录上下文信息:包括请求 ID、用户 ID、操作等,便于关联。
  • 避免记录敏感信息:如密码、信用卡号、个人身份信息(PII)。必要时脱敏处理。
  • 异步记录:日志操作不应阻塞主线程,好的日志库都支持异步。
  • 统一格式:团队内约定日志格式,便于自动化处理。
  • 定期审查日志:检查是否有异常模式,优化告警阈值。
  • 监控日志系统本身:避免因日志系统故障导致应用异常(如磁盘满)。

9. 示例:完整的 Express 日志配置

以下示例结合了 Winston、morgan 和日志轮转,实现了结构化日志、请求记录和错误日志分离。

const express = require('express');
const morgan = require('morgan');
const winston = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

const app = express();

// 配置 Winston
const logger = winston.createLogger({
  level: 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  defaultMeta: { service: 'my-app' },
  transports: [
    new winston.transports.Console({
      format: winston.format.simple()
    }),
    new DailyRotateFile({
      filename: 'logs/error-%DATE%.log',
      level: 'error',
      datePattern: 'YYYY-MM-DD',
      maxFiles: '30d'
    }),
    new DailyRotateFile({
      filename: 'logs/combined-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      maxFiles: '30d'
    })
  ]
});

// 将 morgan 的流指向 logger.info
const morganStream = {
  write: (message) => logger.info(message.trim())
};
app.use(morgan('combined', { stream: morganStream }));

// 请求级别添加 requestId
app.use((req, res, next) => {
  req.id = require('crypto').randomBytes(16).toString('hex');
  res.setHeader('X-Request-ID', req.id);
  next();
});

// 路由
app.get('/', (req, res) => {
  logger.info({ reqId: req.id, msg: '处理根路由' });
  res.send('Hello World');
});

// 错误处理中间件记录错误
app.use((err, req, res, next) => {
  logger.error({ reqId: req.id, err: err.stack });
  res.status(500).send('Something broke!');
});

app.listen(3000, () => {
  logger.info('应用启动成功');
});

小结

日志记录是应用可观测性的基石。通过选择合适的日志库、合理配置日志级别和输出,并遵循最佳实践,可以显著提高故障排查效率和应用稳定性。在生产环境中,建议结合集中式日志系统,将日志转化为可量化的监控数据。下一章我们将学习 Node.js 的性能优化技巧。