Koa.js 日志系统设计与实现
日志系统的重要性
在生产环境中,完善的日志系统是排查问题、监控系统健康、分析用户行为的重要依据。一个好的日志系统应该满足以下要求:
- 多级别日志:debug、info、warn、error
- 日志格式化:JSON 格式便于解析和搜索
- 日志轮转:按日期或大小切割,避免单文件过大
- 异步写入:不影响请求性能
- 统一管理:集中存储,支持查询分析
日志框架选型
| 框架 | 特点 | 适用场景 |
|---|---|---|
| Winston | 功能最全,生态丰富 | 需要复杂日志处理 |
| Pino | 性能极高,内存占用小 | 高并发生产环境 |
| Log4js | 配置灵活, appender 多 | 需要多种输出方式 |
使用 Winston 实现日志系统
const winston = require('winston');
const path = require('path');
const logDir = path.join(__dirname, 'logs');
// 自定义日志格式
const customFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.printf(({ timestamp, level, message, stack, ...meta }) => {
let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;
if (Object.keys(meta).length > 0) {
log += ` ${JSON.stringify(meta)}`;
}
if (stack) {
log += `\n${stack}`;
}
return log;
})
);
// JSON 格式(用于日志收集)
const jsonFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
);
// 创建 logger
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: jsonFormat,
defaultMeta: { service: 'koa-api' },
transports: [
// 控制台输出
new winston.transports.Console({
format: customFormat
}),
// 错误日志单独文件
new winston.transports.File({
filename: path.join(logDir, 'error.log'),
level: 'error',
maxsize: 10 * 1024 * 1024, // 10MB
maxFiles: 30
}),
// 全部日志
new winston.transports.File({
filename: path.join(logDir, 'combined.log'),
maxsize: 10 * 1024 * 1024,
maxFiles: 7
}),
// HTTP 访问日志
new winston.transports.File({
filename: path.join(logDir, 'access.log'),
level: 'http',
maxsize: 10 * 1024 * 1024,
maxFiles: 7
})
]
});
module.exports = logger;
Koa 中间件集成日志
const logger = require('./logger');
// 请求日志中间件
const requestLogger = async (ctx, next) => {
const start = Date.now();
// 等待请求完成
await next();
const ms = Date.now() - start;
const logData = {
method: ctx.method,
url: ctx.url,
status: ctx.status,
responseTime: ms,
ip: ctx.ip,
userAgent: ctx.get('user-agent'),
requestId: ctx.state.requestId || '-'
};
// 根据状态码选择日志级别
if (ctx.status >= 500) {
logger.error('Request error', logData);
} else if (ctx.status >= 400) {
logger.warn('Request warning', logData);
} else {
logger.info('Request', logData);
}
};
// 统一错误处理中间件
const errorHandler = async (ctx, next) => {
try {
await next();
} catch (err) {
logger.error('Unhandled error', {
error: err.message,
stack: err.stack,
url: ctx.url,
method: ctx.method,
body: ctx.request.body,
query: ctx.query,
requestId: ctx.state.requestId
});
ctx.status = err.status || 500;
ctx.body = {
code: -1,
message: process.env.NODE_ENV === 'production'
? '服务器内部错误'
: err.message
};
}
};
// 请求 ID 中间件
const requestId = async (ctx, next) => {
ctx.state.requestId = ctx.get('X-Request-ID') || generateUUID();
ctx.set('X-Request-ID', ctx.state.requestId);
await next();
};
// 生成 UUID
function generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 使用中间件
app.use(requestId);
app.use(requestLogger);
app.use(errorHandler);
业务日志封装
// 业务日志模块
class BizLogger {
constructor(logger) {
this.logger = logger;
}
// 用户操作日志
logUserAction(userId, action, details = {}) {
this.logger.info('User action', {
type: 'user_action',
userId,
action,
...details
});
}
// 业务操作日志
logBizOperation(operation, result, details = {}) {
this.logger.info('Business operation', {
type: 'biz_operation',
operation,
result,
...details
});
}
// 第三方调用日志
logExternalCall(service, method, duration, success, details = {}) {
const level = success ? 'info' : 'error';
this.logger[level]('External call', {
type: 'external_call',
service,
method,
duration,
success,
...details
});
}
// 数据库操作日志
logDbOperation(operation, table, duration, details = {}) {
this.logger.debug('Database operation', {
type: 'db_operation',
operation,
table,
duration,
...details
});
}
}
const bizLogger = new BizLogger(logger);
// 使用示例
bizLogger.logUserAction('user123', 'login', { ip: '192.168.1.1' });
bizLogger.logExternalCall('payment', 'createOrder', 150, true, { orderId: 'order001' });
日志收集与分析
// 使用 Winston 传输到日志收集系统
const { ElasticsearchTransport } = require('winston-elasticsearch');
const ecsFormat = require('@elastic/ecs-winston-format');
const esTransport = new ElasticsearchTransport({
level: 'info',
clientOpts: {
node: 'http://elasticsearch:9200',
auth: {
username: 'elastic',
password: 'password'
}
},
index: () => {
const date = new Date();
return `koa-logs-${date.getFullYear()}.${date.getMonth() + 1}.${date.getDate()}`;
}
});
// 生产环境的 Logger 配置
const prodLogger = winston.createLogger({
level: 'info',
format: ecsFormat(),
defaultMeta: { service: 'koa-api' },
transports: [
esTransport,
new winston.transports.Console()
]
});
日志级别使用规范
| 级别 | 使用场景 | 示例 |
|---|---|---|
| debug | 开发调试信息 | 变量值、函数调用 |
| info | 正常业务流程 | 用户登录、订单创建 |
| warn | 可恢复的异常 | 参数校验失败、重试成功 |
| error | 需要处理的错误 | 数据库异常、服务调用失败 |
总结
日志系统是生产环境中不可或缺的基础设施。建议在开发阶段就建立完善的日志规范,统一日志格式,便于后续的问题排查和数据分析。对于大规模应用,可以结合 ELK(Elasticsearch + Logstash + Kibana)或 EFK 技术栈实现日志的集中管理和可视化分析。