当前位置: 首页 > 后端技术 > Node.js

基于Egg框架的日志链接跟踪实践分享

时间:2023-04-03 13:53:55 Node.js

快速导航【Logger-Custom】需求后台【Logger-Custom】自定义日志插件开发【Logger-Custom】项目扩展【Logger-Custom】项目应用【ContextFormatter】contextFormatter自定义日志格式[Logrotator]日志切割需求后台实现全链路日志跟踪,方便日志监控、故障排除、接口响应耗时数据统计等。首先,API接口服务接收到调用者的请求,并根据给调用方传递的traceId,在这条调用链处理业务时,如果需要打印日志,则按照约定的规范打印日志信息,并记录traceId,实现日志链路跟踪。日志路径约定/var/logs/${projectName}/bizLog/${projectName}-yyyyMMdd.log日志格式约定日志时间[]traceId[]serverIP[]clientIP[]loglevel[]log内容采用Egg.js框架的egg-logger中间件,在实现过程中发现按照上面的日志格式打印是不能满足要求的(至少我还没有找到实现的方法),如果你想要自己实现它,您可能必须发明自己的轮子。好在egg-logger官方中间件提供了自定义日志扩展功能。参考高级自定义日志,也提供了日志切分、多进程日志处理等功能。egg-logger提供多种传输渠道。我们的需求主要是将请求的业务日志以自定义格式存储。我们主要使用fileTransport和consoleTransport两个通道,分别将日志打印到文件和终端。自定义日志插件开发开发一个基于egg-logger自定义的插件项目。参考插件开发。下面以egg-logger-custom为工程展示核心代码编写logger.jsegg-logger-custom/lib/logger.jsconstmoment=require('moment');constFileTransport=require('egg-logger').FileTransport;constutils=require('./utils');constutil=require('util');/***继承FileTransport*/classAppTransportextendsFileTransport{constructor(options,ctx){super(options);这个.ctx=ctx;//获取每个请求的上下文}log(level,args,meta){//获取自定义格式消息constcustomMsg=this.messageFormat({level,});//打印错误消息的错误堆栈if(args[0]instanceofError){consterr=args[0]||{};args[0]=实用程序。format('%s:%s\n%s\npid:%s\n',err.name,err.message,err.stack,process.pid);}else{args[0]=util.format(customMsg,args[0]);}//这个是必须的,否则日志文件不会写入super.log(level,args,meta);}/***自定义消息格式*根据自己的业务需要自己来定义*@param{String}level*/messageFormat({level}){const{ctx}=this;constparams=JSON.stringify(Object.assign({},ctx.request.query,ctx.body));返回[moment().format('YYYY/MM/DDHH:mm:ss'),ctx.request.get('traceId'),utils.serviceIPAddress,utils.clientIPAddress(ctx.req),level,]。加入(utils.loggerDelimiter)+utils.loggerDelimiter;}}module.exports=AppTransport;工具egg-logger-custom/lib/utils.jsconstinterfaces=require('os').networkInterfaces();module.exports={/***日志分隔符*/loggerDelimiter:'[]',/***获取当前服务器IP*/serviceIPAddress:(()=>{for(constdevNameininterfaces){constiface=interfaces[devName];for(leti=0;i{constaddress=req.headers['x-forwarded-for']||//判断是否有反向代理IPreq.connection.remoteAddress||//判断连接的远程IPreq.socket.remoteAddress||//判断后端socket的IPreq.connection.socket.remoteAddress;返回地址.replace(/::ffff:/ig,'');},clientIPAddress:ctx=>{returnctx.ip;},}注意:以上获取当前请求客户端IP的方法,如需限流防止刷用户IP,请不要使用以上方法,见科普文章:如何伪造和获取用户的真实IP?,在Egg.js中,也可以通过ctx.ip获取,参考FrontProxyMode初始化Loggeregg-logger-custom/app.jsconstLogger=require('egg-logger').Logger;constConsoleTransport=require('egg-logger').ConsoleTransport;constAppTransport=require('./app/logger');module.exports=(ctx,options)=>{constlogger=newLogger();logger.set('file',newAppTransport({level:options.fileLoggerLevel||'INFO',file:`/var/logs/${options.appName}/bizLog/${options.appName}.log`,},ctx));logger.set('console',newConsoleTransport({level:options.consoleLevel||'INFO',}));returnlogger;}以上是为日志自定义格式的开发准备的。如果有实际业务需求,可以根据自己团队的需要,打包成团队内部的npm中间件。项目扩展自定义日志中间件打包后,我们在实际项目应用中还需要多一步。Egg提供了框架扩展功能,包括五项:Application、Context、Request、Response、Helper,可以自定义定义扩展。对于日志,我们需要记录当前请求携带的traceId,对每条日志记录做链接跟踪,需要用到Context(Koa的请求上下文)扩展项。创建一个新的app/extend/context.js文件constAppLogger=require('egg-logger-custom');//上面定义的中间件module.exports={getlogger(){//自定义名称也可以是customLoggerreturnAppLogger(this,{appName:'test',//项目名称consoleLevel:'DEBUG',//终端日志级别fileLoggerLevel:'DEBUG',//文件日志级别});}}建议:对于日志级别,可以使用Consul等配置中心进行配置,上线时将日志级别设置为INFO。当需要检查生产问题时,可以动态启用DEBUG模式。关于Consul,大家可以关注一下我之前写的服务注册,发现Consul系列项目应用了错误日志记录,会直接记录错误日志的完整堆栈信息,并输出到errorLog中。为了保证异常的可追溯性,必须保证所有抛出的异常都是Error类型,因为只有Error类型才会带上堆栈信息来定位问题。constController=require('egg').Controller;classExampleControllerextendsController{asynclist(){const{ctx}=this;ctx.logger.error(newError('程序异常!'));ctx.记录器。调试('测试');ctx.logger.info('测试');}}最终日志打印格式如下:2019/05/3001:50:21[]d373c38a-344b-4b36-b931-1e8981aef14f[]192.168.1.20[]221.69.245.153[]INFO[]TestcontextFormattercustom日志格式Egg-Logger最新版本支持通过contextFormatter函数自定义日志格式,见之前的PR:支持contextFormatter#51应用也很简单,通过配置contextFormatter下面是一个简单的应用config.logger={contextFormatter:function(meta){console.log(meta);返回[meta.date,meta.message].join('[]')},...};同样,在你的业务中,需要打印日志的地方,和之前一样ctx.logger.info('Thisisatestdata');输出结果如下:2019-06-0412:20:10,421[]这是一个提供egg-logrotator中间件的测试数据日志切割框架。默认切割是按天切割。其他方式可以参考官网进行配置。框架默认日志路径egg-logger模块lib/egg/config/config.default.jsconfig.logger={dir:path.join(appInfo.root,'logs',appInfo.name),...};自定义日志目录很简单。根据我们的需要,在项目配置文件中重新定义logger的dir路径config.logger={dir:/var/logs/test/bizLog/}这样可以吗?按照我们自定义的日志文件名格式(${projectName}-yyyyMMdd.log),好像是不行的。日志切分过程中默认的文件名格式为.log.YYYY-MM-DD,参考源码https://github.com/eggjs/egg-logrotator/blob/master/app/lib/day_rotator。js_setFile(srcPath,files){//不要在filesRotateBySize中旋转logPathif(this.filesRotateBySize.indexOf(srcPath)>-1){return;}//不要在filesRotateByHour中轮换logPathif(this.filesRotateByHour.indexOf(srcPath)>-1){return;}if(!files.has(srcPath)){//允许2分钟偏差consttargetPath=srcPath+moment().subtract(23,'hours').subtract(58,'minutes').format('.YYYY-MM-DD');//日志格式定义debug('setfile%s=>%s',srcPath,targetPath);文件.set(srcPath,{srcPath,targetPath});}}日志切分扩展中间件egg-logrotator预留了一个扩展接口。自定义日志文件名,可以使用框架提供的app.LogRotator自定义app/schedule/custom.jsconstmoment=require('moment');module.exports=app=>{constrotator=getRotator(app);return{schedule:{type:'worker',//只有一个worker运行这个任务cron:'100***',//每天00:00运行},asynctask(){awaitrotator.rotate();}};};functiongetRotator(app){classCustomRotatorextendsapp.LogRotator{asyncgetRotateFiles(){constfiles=newMap();constsrcPath=`/var/logs/test/bizLog/test.log`;consttargetPath=`/var/logs/test/bizLog/test-${moment().subtract(1,'days').format('YYYY-MM-DD')}.log`;files.set(srcPath,{srcPath,targetPath});返回文件;}}returnnewCustomRotator({app});}经过分割之后文件显示如下:$ls-lh/var/logs/test/bizLog/total188K-rw-r--r--1rootroot135KJun111:00test-2019-06-01.log-rw-r--r--1rootroot912Jun209:44test-2019-06-02.log-rw-r--r--1rootroot40KJun311:49test.logextension:基于以上日志格式,可以使用ELK进行日志收集、分析、分析检索作者:五悦君链接:https://www.imooc.com/article...来源:MOOC阅读推荐重点关注Nodejs服务端技术栈:https://www.nodejs.red公众号:Nodejs技术堆