阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 中间件的日志记录与监控

中间件的日志记录与监控

作者:陈川 阅读数:44565人阅读 分类: Node.js

日志记录的必要性

日志记录是中间件开发中不可或缺的一环。它帮助开发者追踪应用运行状态,定位问题根源。在Express应用中,没有完善的日志系统就像在黑暗中调试代码,难以发现潜在的错误和性能瓶颈。

Express中的日志中间件

Express生态中有多个成熟的日志中间件可供选择。最常用的是morgan,它专门为HTTP请求日志设计。安装方式很简单:

npm install morgan

基本使用示例如下:

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

const app = express();

// 使用预定义的日志格式
app.use(morgan('combined'));

// 自定义日志格式
app.use(morgan(':method :url :status :res[content-length] - :response-time ms'));

morgan支持多种预定义格式:

  • 'combined':标准Apache组合日志格式
  • 'common':基本日志格式
  • 'dev':彩色开发日志
  • 'short':极简格式
  • 'tiny':最简格式

自定义日志中间件

有时预定义中间件不能满足需求,可以创建自定义日志中间件:

function requestLogger(req, res, next) {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`${req.method} ${req.originalUrl} ${res.statusCode} - ${duration}ms`);
  });

  next();
}

app.use(requestLogger);

这个自定义中间件记录了请求方法、URL、状态码和响应时间。

日志分级管理

生产环境需要区分日志级别,常用的有:

  • error:错误日志
  • warn:警告日志
  • info:普通信息日志
  • debug:调试信息
  • verbose:详细日志

可以使用winston库实现分级日志:

const winston = require('winston');

const logger = winston.createLogger({
  levels: winston.config.syslog.levels,
  transports: [
    new winston.transports.Console({
      level: 'debug',
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    }),
    new winston.transports.File({
      filename: 'error.log',
      level: 'error'
    })
  ]
});

// 使用示例
logger.error('数据库连接失败');
logger.info('服务器启动在3000端口');

日志存储策略

日志存储需要考虑几个方面:

  1. 本地文件存储
  2. 数据库存储
  3. 云服务存储

文件轮转是常见需求,可以使用winston-daily-rotate-file:

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

logger.add(new DailyRotateFile({
  filename: 'application-%DATE%.log',
  datePattern: 'YYYY-MM-DD',
  zippedArchive: true,
  maxSize: '20m',
  maxFiles: '14d'
}));

监控中间件性能

除了日志记录,性能监控同样重要。可以使用express-status-monitor:

const monitor = require('express-status-monitor')();

app.use(monitor);

这个中间件提供了可视化面板,展示:

  • 请求响应时间
  • 内存使用情况
  • CPU负载
  • 事件循环延迟

自定义性能监控

对于特定路由的性能监控,可以创建中间件:

function performanceMonitor(req, res, next) {
  const start = process.hrtime();
  
  res.on('finish', () => {
    const diff = process.hrtime(start);
    const duration = diff[0] * 1e3 + diff[1] * 1e-6; // 毫秒
    
    if(duration > 500) {
      logger.warn(`慢请求: ${req.method} ${req.url} 耗时 ${duration.toFixed(2)}ms`);
    }
  });

  next();
}

// 应用到特定路由
app.get('/api/complex', performanceMonitor, (req, res) => {
  // 复杂处理逻辑
});

错误追踪与日志关联

当应用出错时,需要将错误信息与请求关联。可以扩展错误处理中间件:

app.use((err, req, res, next) => {
  const requestId = req.headers['x-request-id'] || require('crypto').randomBytes(8).toString('hex');
  
  logger.error({
    requestId,
    method: req.method,
    url: req.url,
    error: err.stack,
    user: req.user ? req.user.id : 'anonymous'
  });

  res.status(500).json({
    error: 'Internal Server Error',
    requestId
  });
});

日志分析与可视化

收集日志后,可以使用ELK栈(Elasticsearch, Logstash, Kibana)进行分析:

  1. 使用Filebeat收集日志
  2. Logstash处理日志数据
  3. Elasticsearch存储日志
  4. Kibana可视化展示

配置示例(filebeat.yml):

filebeat.inputs:
- type: log
  paths:
    - /var/log/node-app/*.log

output.logstash:
  hosts: ["logstash:5044"]

实时监控与告警

对于关键指标,需要设置实时告警。可以使用Prometheus和Grafana:

  1. 使用express-prom-bundle收集指标
  2. Prometheus存储时间序列数据
  3. Grafana展示仪表盘
const promBundle = require("express-prom-bundle");
const metricsMiddleware = promBundle({
  includeMethod: true,
  includePath: true,
  customLabels: { project: 'my-app' }
});

app.use(metricsMiddleware);

日志安全考虑

日志记录需要注意安全事项:

  1. 不要记录敏感信息(密码、token等)
  2. 对用户数据进行脱敏处理
  3. 设置适当的日志访问权限
function sanitizeData(data) {
  if (typeof data !== 'object') return data;
  
  const sensitiveKeys = ['password', 'creditCard', 'token'];
  const sanitized = {...data};
  
  sensitiveKeys.forEach(key => {
    if (sanitized[key]) {
      sanitized[key] = '******';
    }
  });
  
  return sanitized;
}

// 使用示例
logger.info('用户登录', {
  user: req.body.username,
  ...sanitizeData(req.body)
});

分布式系统日志追踪

在微服务架构中,需要跨服务追踪请求。可以使用OpenTelemetry:

const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { SimpleSpanProcessor } = require('@opentelemetry/sdk-trace-base');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');

const provider = new NodeTracerProvider();
provider.addSpanProcessor(
  new SimpleSpanProcessor(
    new JaegerExporter({
      serviceName: 'express-app'
    })
  )
);
provider.register();

容器环境日志处理

在Docker或Kubernetes环境中,日志处理有所不同:

  1. 将日志输出到stdout/stderr
  2. 使用Fluentd或Fluent Bit收集日志
  3. 配置适当的日志驱动
FROM node:14

# 创建非root用户
RUN useradd -m appuser
USER appuser

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

CMD ["node", "server.js"]

性能优化技巧

日志记录本身也会影响性能,需要注意:

  1. 避免同步日志写入
  2. 生产环境减少debug日志
  3. 使用批量写入代替频繁IO
// 批量写入示例
const logQueue = [];
let isWriting = false;

async function batchWriteLogs() {
  if (isWriting || logQueue.length === 0) return;
  
  isWriting = true;
  const logsToWrite = [...logQueue];
  logQueue.length = 0;
  
  try {
    await writeToDatabase(logsToWrite);
  } catch (err) {
    console.error('日志写入失败', err);
    // 将失败的日志重新加入队列
    logQueue.unshift(...logsToWrite);
  } finally {
    isWriting = false;
    if (logQueue.length > 0) {
      setImmediate(batchWriteLogs);
    }
  }
}

function logToQueue(message) {
  logQueue.push(message);
  if (logQueue.length >= 100) {
    batchWriteLogs();
  }
}

日志采样策略

高流量应用需要日志采样避免存储爆炸:

function shouldSample(req) {
  // 重要请求总是记录
  if (req.url.startsWith('/api/payment')) return true;
  
  // 错误请求总是记录
  if (res.statusCode >= 500) return true;
  
  // 其他请求按10%采样
  return Math.random() < 0.1;
}

app.use((req, res, next) => {
  if (shouldSample(req)) {
    logger.info(`${req.method} ${req.url}`);
  }
  next();
});

上下文增强日志

为日志添加更多上下文信息有助于调试:

const cls = require('cls-hooked');
const namespace = cls.createNamespace('app');

function contextLogger(req, res, next) {
  namespace.run(() => {
    namespace.set('requestId', req.headers['x-request-id'] || require('crypto').randomBytes(8).toString('hex'));
    namespace.set('userId', req.user?.id || 'anonymous');
    next();
  });
}

function logWithContext(message) {
  const requestId = namespace.get('requestId');
  const userId = namespace.get('userId');
  logger.info(`${requestId} [${userId}] ${message}`);
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌