您现在的位置是:网站首页 > 错误处理与日志记录策略文章详情

错误处理与日志记录策略

在Express框架中,错误处理和日志记录是保证应用稳定性和可维护性的关键。合理的策略能快速定位问题、优化用户体验,并为后续调试提供完整上下文。以下从错误捕获、中间件设计、日志分类等方面展开具体实践方案。

错误处理中间件设计

Express通过四参数中间件(err, req, res, next)捕获错误。典型结构应包含错误分类和响应格式化:

app.use((err, req, res, next) => {
  // 识别错误类型
  const isOperational = err instanceof BaseError;
  const statusCode = isOperational ? err.statusCode : 500;

  // 生产环境隐藏技术细节
  const response = {
    message: isOperational ? err.message : 'Internal Server Error',
    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
  };

  res.status(statusCode).json(response);
  
  // 非预期错误触发告警
  if (!isOperational) {
    process.emit('uncaughtException', err);
  }
});

自定义错误类可增强错误识别能力:

class APIError extends Error {
  constructor(message, statusCode = 500) {
    super(message);
    this.statusCode = statusCode;
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

// 使用示例
throw new APIError('用户不存在', 404);

异步错误捕获方案

Express默认不捕获异步错误,需要结合Promise链或async/await处理:

// 包装器方案
const asyncHandler = fn => (req, res, next) => 
  Promise.resolve(fn(req, res, next)).catch(next);

// 路由中使用
router.get('/data', asyncHandler(async (req, res) => {
  const data = await fetchData();
  if (!data) throw new APIError('数据获取失败', 503);
  res.json(data);
}));

对于未处理的Promise拒绝,应在进程级别监听:

process.on('unhandledRejection', (reason, promise) => {
  logger.error('未处理的拒绝:', reason);
  // 可在此处触发重启策略
});

多层级日志记录策略

日志系统应区分不同严重级别并结构化输出。使用winston的典型配置:

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

const logger = createLogger({
  level: 'debug',
  format: combine(
    timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
    json()
  ),
  transports: [
    new transports.File({ 
      filename: 'logs/error.log',
      level: 'error',
      maxsize: 5 * 1024 * 1024 // 5MB
    }),
    new transports.Console({
      format: format.combine(
        format.colorize(),
        format.printf(info => 
          `${info.timestamp} [${info.level}]: ${info.message}`
        )
      )
    })
  ]
});

// 中间件集成示例
app.use((req, res, next) => {
  logger.info(`${req.method} ${req.path}`);
  next();
});

敏感信息过滤机制

日志记录需避免泄露敏感数据,可采用数据清洗中间件:

const maskFields = ['password', 'token', 'creditCard'];

function sanitizeBody(body) {
  return Object.keys(body).reduce((acc, key) => {
    acc[key] = maskFields.includes(key) ? '***' : body[key];
    return acc;
  }, {});
}

app.use((req, res, next) => {
  logger.debug('请求体:', sanitizeBody(req.body));
  next();
});

性能日志与追踪ID

分布式系统中需关联请求链路,推荐使用CLS(Continuation Local Storage):

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

// 中间件生成唯一ID
app.use((req, res, next) => {
  namespace.run(() => {
    const traceID = crypto.randomUUID();
    namespace.set('traceID', traceID);
    res.setHeader('X-Trace-ID', traceID);
    next();
  });
});

// 日志中自动附加ID
logger.format = format.printf(info => {
  const traceID = namespace.get('traceID') || 'null';
  return `${info.timestamp} [${traceID}] ${info.level}: ${info.message}`;
});

错误聚合与监控集成

将关键错误上报至Sentry等监控平台:

const Sentry = require('@sentry/node');

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.5,
  attachStacktrace: true
});

// 错误中间件追加
app.use((err, req, res, next) => {
  Sentry.captureException(err);
  next(err);
});

日志轮转与归档策略

使用logrotate或专业工具管理日志文件:

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

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

开发与生产环境差异化配置

通过环境变量切换日志行为:

const isProduction = process.env.NODE_ENV === 'production';

logger.configure({
  level: isProduction ? 'info' : 'debug',
  silent: process.env.DISABLE_LOGGING === 'true'
});

// 错误响应差异处理
app.use((err, req, res, next) => {
  if (isProduction && err.isAxiosError) {
    err.message = '上游服务不可用';
  }
  next(err);
});

HTTP请求上下文增强

扩展错误对象包含请求上下文:

class ContextualError extends Error {
  constructor(req, message) {
    super(message);
    this.request = {
      method: req.method,
      path: req.path,
      params: req.params,
      query: req.query
    };
  }
}

// 使用示例
app.post('/users', (req, res, next) => {
  if (!req.body.name) {
    return next(new ContextualError(req, '缺少必要字段'));
  }
});

客户端错误上报接口

为前端提供专用错误上报端点:

app.post('/client-error', (req, res) => {
  const { message, stack, userAgent } = req.body;
  logger.clientError({
    message,
    stack,
    deviceInfo: {
      userAgent,
      ip: req.ip
    }
  });
  res.sendStatus(200);
});

数据库操作错误处理

Mongoose操作的特殊处理示例:

router.get('/users/:id', asyncHandler(async (req, res) => {
  const user = await User.findById(req.params.id)
    .orFail(new APIError('用户不存在', 404));
  
  if (user.isLocked) {
    throw new APIError('账户已锁定', 403, {
      unlockTime: user.unlockTime
    });
  }
}));

测试环境Mock日志

单元测试中替换真实日志:

// test/setup.js
const mockLogger = {
  info: jest.fn(),
  error: jest.fn()
};

jest.mock('../lib/logger', () => mockLogger);

// 测试用例示例
test('should log API errors', async () => {
  await request(app).get('/error-route');
  expect(mockLogger.error).toHaveBeenCalledWith(
    expect.objectContaining({ message: '模拟错误' })
  );
});

我的名片

网名:~川~

岗位:console.log 调试员

坐标:重庆市-九龙坡区

邮箱:cc@qdcc.cn

沙漏人生

站点信息

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