和前端喜欢用 console.log 一样,开发服务端的时候,如果要排查错误,第一件事就是记录日志。我们一般会给每个接口都加上详细的日志,这样才不至于出问题的时候无从下手。
一个合格的 logger 一般包含以下几个部分:
- 名称:标识日志来源,比如服务名称、模块名等
- 唯一 ID:通常是请求 ID,用于跟踪整个请求链路
- 时间:精确到毫秒的时间戳,便于排查问题发生的时间点
- 等级:如 info、warn、error、debug 等,用于区分日志的重要程度
- 格式化工具:将日志信息格式化为统一的格式,便于阅读和分析
为什么需要良好的日志?
在生产环境中,我们无法直接连接到服务器进行调试。这时,日志成为我们了解系统运行状态的唯一窗口。一个设计良好的日志系统可以:
- 快速定位问题:通过请求 ID 可以追踪完整请求链路
- 监控系统运行状况:记录请求量、响应时间等指标
- 安全审计:记录敏感操作,便于安全审计
- 数据分析:为用户行为分析提供数据支持
实现一个简单但实用的 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开始,是走向全栈开发的第一步。