一、为什么要自己写Koa日志中间件?
很多刚接触Koa的开发者,可能会先去找现成的中间件,比如koa-logger,它能简单输出请求的基本信息,比如方法、路径、状态码、耗时,但用了之后会发现很多问题:比如输出的是乱乱的纯文本,想要分析具体某个接口的耗时,得自己去解析字符串,不方便;如果想加业务相关的字段,比如这次请求的用户ID,或者追踪ID,现成中间件可能不支持;还有,日志文件会越写越大,几个月后可能占几个G的空间,找之前的日志要翻很久,还容易卡住。所以自己手写一个中间件,就能把这些问题都解决,完全贴合项目的需求。
1.1 适配项目的特殊需求
比如我之前做的一个电商项目,线上需要统计每个商品页面的请求耗时,还要关联用户的登录状态,现成的中间件加不了这些字段,只能自己写,把用户ID、商品ID都放到日志里,排查性能问题的时候,一眼就能看出是哪个用户、哪个商品的接口慢。
二、核心功能拆解
手写日志中间件,其实就是把我们需要的功能拆成几个部分,逐个实现,不用想复杂,一步步来就行。
2.1 请求耗时统计
这个功能很简单,就是“掐表”:请求刚进来的时候,记一个时间,等请求处理完(不管成功还是失败),再拿当前时间减去之前记的,就是这次请求的耗时,单位可以用毫秒,方便看。这里为啥要用try-finally?因为后面的中间件如果出错了,比如路由找不到,或者数据库连接崩了,原来的流程会中断,但是finally里的代码一定会执行,这样就算请求出错,也能把日志记下来,不会丢记录。
2.2 结构化日志格式
啥是结构化?就是不用那种“GET /api/order 200 15ms”的纯文本,而是用像JSON这样的格式,比如{"method":"GET","path":"/api/order","user_id":123,"cost_time":15,"status":200},这种格式的好处是,不管加多少字段,都整齐,后续可以用工具(比如ELK、Datadog)直接分析,不用自己手动解析文本。比如要找所有耗时超过100ms的GET请求,直接搜“cost_time>100 AND method=GET”就行,比找文本快多了。
2.3 日志切割与轮转
这个功能是解决日志文件太大的问题,轮转的意思就是,不用一个日志文件写到底,而是按规则切成多个文件,比如每天一个,或者每个文件最大20M,超过了就新建一个,然后把旧的文件保留一段时间(比如30天),自动删掉更旧的,这样磁盘不会被撑爆,找日志也方便,比如想找10月5号的日志,直接打开对应日期的文件就行。实现这个功能不用自己写复杂的文件操作,用现成的日志扩展库就可以,比如winston-daily-rotate-file,专门处理这个。
三、完整中间件实现
下面的示例用的技术栈是Koa 2 + winston(Node.js常用的日志库) + winston-daily-rotate-file(处理日志轮转),全程可运行,带详细注释。
首先要安装依赖,打开终端执行:
npm install koa winston winston-daily-rotate-file
然后创建index.js,写中间件代码:
// 技术栈:Koa 2 + winston日志库 + 日志轮转扩展
const Koa = require('koa');
const { createLogger, format, transports } = require('winston');
// 引入日志轮转的扩展库
const DailyRotateFile = require('winston-daily-rotate-file');
// 1. 配置日志轮转的规则,这是核心的日志管理部分
const rotateTransport = new DailyRotateFile({
filename: 'app-%DATE%.log', // 日志文件名的格式,%DATE%会自动替换成当天的日期
datePattern: 'YYYY-MM-DD', // 日期的格式,每天生成一个新文件
maxSize: '20m', // 单个日志文件最大20MB,超过就自动新建
maxFiles: '30d' // 只保留最近30天的日志,自动删除30天前的旧文件
});
// 2. 创建结构化的日志记录器,配置输出格式和级别
const logger = createLogger({
level: 'info', // 日志级别,只记录info及以上级别的(比如warn、error也会记录)
format: format.combine(
// 给每条日志加上时间戳,格式是年-月-日 时:分:秒
format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
// 输出JSON格式,方便后续工具分析
format.json()
),
transports: [
// 同时把日志输出到控制台,开发的时候方便看
new transports.Console(),
// 把日志输出到按规则轮转的文件里
rotateTransport
]
});
// 3. Koa日志中间件的核心逻辑
async function koaLoggerMiddleware(ctx, next) {
// 请求刚进来,记录开始时间(单位毫秒)
const startTime = Date.now();
try {
// 把请求交给后面的中间件或路由处理,这一步是必须的,不然请求不会继续
await next();
} finally {
// 不管请求成功还是失败,都会执行这里的代码,计算耗时并记录日志
const costTime = Date.now() - startTime;
// 准备要记录的结构化日志数据,都是需要的关键信息
const logInfo = {
method: ctx.method, // 请求方法,比如GET、POST、PUT
path: ctx.path, // 请求的路径,比如/api/user/order
status: ctx.status, // 响应的状态码,比如200、404、500
cost_time_ms: costTime, // 请求耗时,单位毫秒
client_ip: ctx.ip, // 客户端的IP地址
user_agent: ctx.get('User-Agent') // 客户端的信息,比如浏览器类型、手机型号
};
// 输出日志,info级别,这里也可以根据状态码调整级别,比如4xx用warn,5xx用error
logger.info('请求处理完成', logInfo);
}
}
// 4. 初始化Koa应用,挂载中间件
const app = new Koa();
// 注意:中间件必须放在最前面,这样才能拿到完整的响应状态码
app.use(koaLoggerMiddleware);
// 写个测试用的路由,返回字符串,方便测试日志
app.use(async ctx => {
ctx.body = '这是测试接口,用来验证日志中间件是否正常工作';
});
// 启动服务,监听3000端口
app.listen(3000, () => {
console.log('服务启动成功,访问地址:http://localhost:3000');
});
这个代码跑起来之后,访问http://localhost:3000,就会在控制台看到JSON格式的日志,同时在项目的logs目录下(winston默认会创建)生成按日期命名的日志文件,每天一个,自动删除30天前的。
四、应用场景、技术优缺点、注意事项
4.1 核心应用场景
这种手写的日志中间件,最适合这些情况:一是项目有专属的日志规范,比如要求记录特定的业务字段(比如用户ID、部门ID),现成中间件改不了;二是需要对日志进行二次分析,比如统计每个接口的平均耗时,或者排查线上用户的问题,结构化的日志更方便;三是需要严格的日志管理,比如自动归档、删除旧日志,避免磁盘浪费。
4.2 技术优缺点
优点很明显:第一是灵活,想加什么字段就加什么,想改格式就改,完全符合项目需求;第二是轻量,不需要引入复杂的工具,只用winston这个常用库;第三是可控,不会被第三方库的默认行为限制,比如不想输出某些字段,直接去掉就行。缺点的话,就是需要自己维护,比如如果winston更新了,要兼容;还要自己处理异常,比如日志记录失败了,不能影响主流程,需要加try-catch,不然日志出错可能导致服务挂掉。
4.3 注意事项
有几个点一定要注意,不然会出问题:第一是中间件的顺序,必须放在所有路由中间件的前面,因为只有这样,才能拿到完整的ctx.status,比如如果路由在中间件前面,中间件执行的时候还没拿到状态码,就会记录错误的状态;第二是不要输出敏感信息,比如用户的密码、身份证号、手机号,要是日志里漏了这些,会有安全风险,所以如果要记录用户信息,只记非敏感的ID;第三是日志级别的设置,开发的时候可以用debug级别,线上用info级别,错误才用error,避免输出太多没用的日志;第四是日志目录的权限,要确保Node进程对日志目录有读写权限,不然会出现权限错误,日志写不进去。
五、总结
手写Koa日志中间件,其实就是把需求拆解,用合适的工具实现,不用觉得复杂,每个部分都很简单。从请求耗时的掐表,到结构化日志,再到日志轮转,一步步下来,就能做出符合自己项目的日志系统,比用现成的中间件更贴合需求,排查问题的时候也更方便,适合所有需要自定义日志的Node.js项目。
评论
围绕“手写Koa日志中间件的完整经验:实现请求耗时统计、结构化格式化输出并集成日志切割与轮转”参与讨论