您现在的位置是:网站首页 > 日志系统的集成与配置文章详情

日志系统的集成与配置

日志系统是Express应用中不可或缺的一部分,它帮助开发者追踪请求、调试错误并监控应用状态。合理的日志配置能显著提升开发效率和运维能力,尤其是在生产环境中。

日志系统的重要性

日志记录是应用运行时的关键数据来源。通过日志可以分析用户行为、排查错误、监控性能瓶颈。在Express中,常见的日志需求包括:

  • 记录HTTP请求的详细信息(方法、路径、状态码)
  • 捕获未处理的异常和错误
  • 区分不同环境(开发/生产)的日志级别
  • 长期存储和日志轮转

没有日志的系统就像黑箱,出现问题难以定位。例如当API响应异常时,通过访问日志可以快速判断是客户端参数错误还是服务端处理失败。

常用日志库选择

Express生态中有多个成熟的日志解决方案:

  1. morgan - 专注于HTTP请求日志的中间件
const morgan = require('morgan');
app.use(morgan('combined')); // 使用Apache标准格式
  1. winston - 功能全面的日志库,支持多传输方式
const winston = require('winston');
const logger = winston.createLogger({
  level: 'info',
  transports: [
    new winston.transports.Console(),
    new winston.transports.File({ filename: 'error.log', level: 'error' })
  ]
});
  1. pino - 高性能JSON格式日志
const pino = require('pino');
const logger = pino({
  level: process.env.NODE_ENV === 'production' ? 'info' : 'debug'
});
  1. bunyan - 结构化日志库,适合复杂系统
const bunyan = require('bunyan');
const log = bunyan.createLogger({
  name: 'myapp',
  streams: [
    { level: 'error', path: '/var/log/myapp-error.log' }
  ]
});

选择时需考虑性能需求(如pino在高压环境下表现优异)和功能需求(如winston的多传输支持)。

集成morgan进行请求日志记录

morgan是Express最常用的请求日志中间件,支持多种预定义格式:

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

const app = express();

// 开发环境使用带颜色的简洁格式
if (process.env.NODE_ENV === 'development') {
  app.use(morgan('dev'));
} else {
  // 生产环境使用标准Apache组合格式
  app.use(morgan('combined'));
}

// 自定义token
morgan.token('host', (req) => req.headers['host']);
app.use(morgan(':method :host :url :status'));

高级配置示例,将日志写入文件并实现日志轮转:

const fs = require('fs');
const path = require('path');
const rfs = require('rotating-file-stream');

const logDirectory = path.join(__dirname, 'log');
fs.existsSync(logDirectory) || fs.mkdirSync(logDirectory);

const accessLogStream = rfs.createStream('access.log', {
  interval: '1d', // 每天轮转
  path: logDirectory
});

app.use(morgan('combined', { stream: accessLogStream }));

使用winston构建完整日志系统

winston的强大之处在于其灵活的配置能力:

const { createLogger, format, transports } = require('winston');
const { combine, timestamp, printf, colorize } = format;

const logFormat = printf(({ level, message, timestamp }) => {
  return `${timestamp} [${level}]: ${message}`;
});

const logger = createLogger({
  level: 'debug',
  format: combine(
    colorize(),
    timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    logFormat
  ),
  transports: [
    new transports.Console(),
    new transports.File({ 
      filename: 'logs/combined.log',
      level: 'info'
    }),
    new transports.File({
      filename: 'logs/errors.log',
      level: 'error',
      maxsize: 1024 * 1024 * 5 // 5MB
    })
  ],
  exceptionHandlers: [
    new transports.File({ filename: 'logs/exceptions.log' })
  ]
});

// 在Express中使用
app.use((err, req, res, next) => {
  logger.error(`${err.status || 500} - ${err.message}`);
  next(err);
});

实现按日期分文件的日志存储:

const { createLogger, transports, format } = require('winston');
const DailyRotateFile = require('winston-daily-rotate-file');

const logger = createLogger({
  transports: [
    new DailyRotateFile({
      filename: 'logs/application-%DATE%.log',
      datePattern: 'YYYY-MM-DD',
      zippedArchive: true,
      maxSize: '20m',
      maxFiles: '14d'
    })
  ]
});

错误处理与日志关联

将错误日志与请求上下文关联非常重要:

const uuid = require('uuid');

app.use((req, res, next) => {
  req.requestId = uuid.v4(); // 为每个请求生成唯一ID
  next();
});

app.use(morgan(':requestId :method :url :status', {
  stream: {
    write: (message) => logger.info(message.trim())
  }
}));

app.use((err, req, res, next) => {
  logger.error({
    requestId: req.requestId,
    error: err.stack,
    route: req.originalUrl
  });
  res.status(500).send('Internal Server Error');
});

日志级别与分类控制

合理的日志分级能有效管理日志量:

const logger = winston.createLogger({
  levels: {
    emergency: 0,
    alert: 1,
    critical: 2,
    error: 3,
    warning: 4,
    notice: 5,
    info: 6,
    debug: 7
  },
  transports: [
    new transports.Console({ level: 'info' }),
    new transports.File({ 
      filename: 'debug.log',
      level: 'debug' 
    })
  ]
});

// 业务代码中使用不同级别
logger.debug('Debugging info');
logger.info('User login');
logger.warn('Deprecated API called');
logger.error('Database connection failed');

生产环境最佳实践

生产环境中的日志配置需要考虑:

  1. 敏感信息过滤
morgan.token('body', (req) => {
  if (req.body.password) {
    const filtered = { ...req.body, password: '******' };
    return JSON.stringify(filtered);
  }
  return JSON.stringify(req.body);
});

app.use(morgan(':method :url :status :body'));
  1. 日志采样 - 在高流量时避免日志爆炸
app.use(morgan('combined', {
  skip: (req, res) => Math.random() > 0.5 // 50%采样率
}));
  1. 集中式日志管理 - 使用ELK或Splunk等工具
const { ElasticsearchTransport } = require('winston-elasticsearch');

const esTransport = new ElasticsearchTransport({
  level: 'info',
  clientOpts: { node: 'http://localhost:9200' }
});

logger.add(esTransport);
  1. 性能考量 - 使用异步日志写入
const asyncLogger = winston.createLogger({
  transports: [
    new winston.transports.File({
      filename: 'async.log',
      handleExceptions: true,
      maxsize: 5242880, // 5MB
      maxFiles: 5,
      tailable: true,
      zippedArchive: true,
      options: { flags: 'a' }
    })
  ],
  exitOnError: false
});

自定义日志格式与扩展

创建符合业务需求的日志格式:

const { format } = require('winston');
const util = require('util');

const customFormat = format((info) => {
  info.appName = 'MyExpressApp';
  info.environment = process.env.NODE_ENV || 'development';
  if (info.message instanceof Error) {
    info.stack = info.message.stack;
    info.message = info.message.message;
  }
  return info;
});

const logger = winston.createLogger({
  format: format.combine(
    customFormat(),
    format.timestamp(),
    format.json()
  ),
  transports: [new transports.Console()]
});

// 使用util.inspect深度打印对象
logger.debug('Current session: %o', req.session);

日志与监控系统集成

将日志指标导入监控系统:

const Prometheus = require('prom-client');
const httpRequestDurationMicroseconds = new Prometheus.Histogram({
  name: 'http_request_duration_ms',
  help: 'Duration of HTTP requests in ms',
  labelNames: ['method', 'route', 'code'],
  buckets: [0.1, 5, 15, 50, 100, 300, 500, 1000, 3000, 5000]
});

app.use((req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    const duration = Date.now() - start;
    httpRequestDurationMicroseconds
      .labels(req.method, req.route.path, res.statusCode)
      .observe(duration);
  });
  next();
});

多环境日志策略

不同环境应采用不同的日志策略:

function createLogger(env) {
  const commonTransports = [
    new transports.File({ filename: 'logs/errors.log', level: 'error' })
  ];
  
  if (env === 'development') {
    return winston.createLogger({
      level: 'debug',
      format: format.combine(
        format.colorize(),
        format.simple()
      ),
      transports: [
        ...commonTransports,
        new transports.Console()
      ]
    });
  } else {
    return winston.createLogger({
      level: 'info',
      format: format.json(),
      transports: [
        ...commonTransports,
        new transports.File({ filename: 'logs/combined.log' }),
        process.env.LOGSTASH_HOST && new LogstashTransport({
          host: process.env.LOGSTASH_HOST,
          port: 5044
        })
      ].filter(Boolean)
    });
  }
}

日志性能优化技巧

在高性能场景下的日志优化:

  1. 批量写入 - 减少IO操作
const { BatchTransport } = require('winston-transport');

class MemoryTransport extends BatchTransport {
  constructor(opts) {
    super(opts);
    this.batch = [];
    setInterval(() => this.flush(), 5000); // 每5秒刷一次
  }

  logBatch(batch, callback) {
    fs.appendFile('bulk.log', batch.join('\n'), callback);
  }
}

logger.add(new MemoryTransport());
  1. 使用worker线程 - 避免阻塞主线程
const { Worker } = require('worker_threads');

class WorkerTransport extends winston.Transport {
  constructor() {
    super();
    this.worker = new Worker('./logger-worker.js');
  }

  log(info, callback) {
    this.worker.postMessage(info);
    callback();
  }
}
  1. 选择性日志记录
app.use((req, res, next) => {
  // 跳过健康检查的日志
  if (req.path === '/health') return next();
  morgan('combined')(req, res, next);
});

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

  • 建站时间:2013/03/16
  • 本站运行
  • 文章数量
  • 总访问量
微信公众号
每次关注
都是向财富自由迈进的一步