做好全栈,从打一个合格的日志开始

7 阅读3分钟

和前端喜欢用 console.log 一样,开发服务端的时候,如果要排查错误,第一件事就是记录日志。我们一般会给每个接口都加上详细的日志,这样才不至于出问题的时候无从下手。

一个合格的 logger 一般包含以下几个部分:

  • 名称:标识日志来源,比如服务名称、模块名等
  • 唯一 ID:通常是请求 ID,用于跟踪整个请求链路
  • 时间:精确到毫秒的时间戳,便于排查问题发生的时间点
  • 等级:如 info、warn、error、debug 等,用于区分日志的重要程度
  • 格式化工具:将日志信息格式化为统一的格式,便于阅读和分析

为什么需要良好的日志?

在生产环境中,我们无法直接连接到服务器进行调试。这时,日志成为我们了解系统运行状态的唯一窗口。一个设计良好的日志系统可以:

  1. 快速定位问题:通过请求 ID 可以追踪完整请求链路
  2. 监控系统运行状况:记录请求量、响应时间等指标
  3. 安全审计:记录敏感操作,便于安全审计
  4. 数据分析:为用户行为分析提供数据支持

实现一个简单但实用的 Logger

下面是一个简单但实用的 Logger 实现,它具备了基本的日志功能,并且可以方便地集成到各种服务中。 在实际项目中,你可能还需要考虑日志的持久化存储、日志轮转、日志聚合等更复杂的需求。


type LogLevel = 'info' | 'warn' | 'error' | 'debug';

interface LogOptions {
method?: string;
path?: string;
requestId?: string;
body?: any;
params?: any;
query?: any;
response?: any;
responseTime?: number;
error?: Error | unknown;
additionalInfo?: Record<string, any>;
}

/**
* 格式化日志输出
* @param level 日志级别
* @param message 日志消息
* @param options 日志选项
*/
function formatLog(
level: LogLevel,
message: string,
options?: LogOptions
): string {
const timestamp = new Date().toISOString();
const requestId = options?.requestId || generateRequestId();

let logParts = [
  `[${timestamp}]`,
  `[${level.toUpperCase()}]`,
  `[RequestID: ${requestId}]`
];

if (options?.method && options?.path) {
  logParts.push(`[${options.method} ${options.path}]`);
}

logParts.push(message);

return logParts.join(' ');
}

/**
* 生成请求ID
*/
function generateRequestId(): string {
return Math.random().toString(36).substring(2, 15);
}

/**
* 记录日志
* @param level 日志级别
* @param message 日志消息
* @param options 日志选项
*/
function log(level: LogLevel, message: string, options?: LogOptions): void {
const formattedLog = formatLog(level, message, options);

switch (level) {
  case 'info':
    console.info(formattedLog);
    if (options) {
      if (options.body) console.info('Request Body:', options.body);
      if (options.params) console.info('Request Params:', options.params);
      if (options.query) console.info('Request Query:', options.query);
      if (options.response) console.info('Response:', options.response);
      if (options.additionalInfo)
        console.info('Additional Info:', options.additionalInfo);
    }
    break;
  case 'warn':
    console.warn(formattedLog);
    break;
  case 'error':
    console.error(formattedLog);
    if (options?.error) {
      if (options.error instanceof Error) {
        console.error('Error:', options.error.message);
        console.error('Stack:', options.error.stack);
      } else {
        console.error('Error:', options.error);
      }
    }
    break;
  case 'debug':
    console.debug(formattedLog);
    if (options) {
      if (options.body) console.debug('Request Body:', options.body);
      if (options.params) console.debug('Request Params:', options.params);
      if (options.query) console.debug('Request Query:', options.query);
      if (options.response) console.debug('Response:', options.response);
      if (options.additionalInfo)
        console.debug('Additional Info:', options.additionalInfo);
    }
    break;
}
}

export const logger = {
info: (message: string, options?: LogOptions) =>
  log('info', message, options),
warn: (message: string, options?: LogOptions) =>
  log('warn', message, options),
error: (message: string, options?: LogOptions) =>
  log('error', message, options),
debug: (message: string, options?: LogOptions) =>
  log('debug', message, options)
};

使用示例

下面展示如何在项目中使用这个日志工具:

// 在API请求处理中使用
import { logger } from '../utils/logger';

async function handleUserLogin(req, res) {
  // 请求开始时记录信息
  logger.info('用户登录请求开始', {
    method: req.method,
    path: req.path,
    body: { username: req.body.username }, // 不记录密码等敏感信息
    requestId: req.headers['x-request-id']
  });

  try {
    // 业务逻辑处理...
    const startTime = Date.now();
    const user = await userService.login(req.body);
    const responseTime = Date.now() - startTime;

    // 请求成功,记录响应信息
    logger.info('用户登录成功', {
      method: req.method,
      path: req.path,
      requestId: req.headers['x-request-id'],
      responseTime,
      additionalInfo: { userId: user.id }
    });

    return res.json({ success: true, data: user });
  } catch (error) {
    // 请求失败,记录错误信息
    logger.error('用户登录失败', {
      method: req.method,
      path: req.path,
      requestId: req.headers['x-request-id'],
      error
    });

    return res.status(400).json({ success: false, message: error.message });
  }
}

总结

一个好的日志系统是服务器端开发中不可或缺的部分。它可以帮助我们快速定位问题,提高开发效率。

在实际项目中,你可能还需要考虑更多的情况,如:

  • 日志切割与归档:避免日志文件过大
  • 多环境配置:开发环境可能需要更详细的日志,而生产环境则更关注性能和存储空间
  • 日志收集与分析:如ELK(Elasticsearch, Logstash, Kibana)等工具的集成
  • 敏感信息过滤:确保不会记录用户密码等敏感信息

但无论如何,从一个基础但功能完善的logger开始,是走向全栈开发的第一步。