log是开发者排查问题的一个非常重要的手段,有时甚至是唯一的手段,所以如何合理正确的打印日志就成为了开发重重的重点。Node.js的登录方式一般有几种:主动展示和被动记录。这两种方法可以由不同的模块来实现。下面我们就来看看如何选择吧。**常见主动展示**一般来说,主动通常发生在开发期。当状态不确定时,我们会打印一些信息,比如普通的。console.log('helloworld');这是活动打印的最简单示例。但是在大多数场景下,我们不会使用控制台进行打印。毕竟除了内置之外,在性能和功能上并没有什么特别的优势。除了大家熟知的console模块,在Node.js领域还有一个大家熟知的debug模块。它可以根据命名空间以不同的颜色打印输出,但最有用的是它能够控制环境变量。默认情况下(没有任何环境变量),控制台不会有任何输出,当DEBUG环境变量赋值时,相应命名空间的输出会打印到stdout。$DEBUG=*nodeapp.js由于debug模块是TJ出品的,很早就投入使用,所以被广泛使用,现在很多模块还在使用。Node.js官方一直希望有一个内置的调试模块。从v0.11.3开始,终于增加了一个util.debuglog方法。它的功能类似于debug模块,是内置模块,所以逐渐有一些模块开始向它过渡。constutil=require('util');constdebuglog=util.debuglog('foo');debuglog('hellofromfoo[%d]',123);它的开关也类似,使用NODE_DEBUG环境变量,应该是专门针对debug模块做区分的。$NODE_DEBUG=foonodeapp.js被动记录方式除了上面提到的类似控制台的方式,我们经常看到的还有各种日志库默认记录的日志。由于这些日志通常都是默默记录的,所以我们不会太在意。只有在有特殊需要(比如错误、定位、计算)的时候才会检查,所以我们把它归类为“被动方法”。大多数三方库都有类似的功能,比如log4j、winston、pino等。这些库的核心功能一般是:将日志输出到不同的通道(例如控制台、文本文件)自定义日志格式(文本或JSON)日志输出级别(warn、debug、error)其他能力,例如Cutting和filerotation,压缩等。这些库一般使用都比较简单,获取实例调用方法输出即可。logger.info('helloworld');请注意,在这里我们将观察到输出中的一些差异。2021-07-2214:50:59,388INFO7739[xxx]安装类似上述标准的结构输出helloworld的全部日志,计算是Error,也是同样类似的格式。那么这个结构包含哪些部分呢?日志格式其实整个日志格式可以追溯到很久以前。无论是JAVA默认的SimpleLogger结构,还是nginx等反向代理服务器的日志,都会包含一些固定的字段。这些固定的字段长期以来形成了一种输出约定,结合这些字段形成今天的日志格式。目前的日志格式一般由几部分组成。Timestamp日志级别processid(node)loglabel(label,fromxxxclass)messagebody(stringorerrorstack)此外,可能还有一些自定义的内容,比如执行时间、用户id、文本长度等。在文本结构的输出中,这些字段会以空格分隔,并以换行符(\n)结束,方便外部的日志采集系统,如阿里云的SLS等。每个公司都会有自己的日志采集和输出规范,所以一般常见的库都会支持自定义日志格式,但是不管怎么变,基本的字段(上面)还是会存在的。随着系统的迭代,逐渐出现了使用JSON格式记录日志的先进方式。以Logstash为首的一些数据(日志)收集和分析工具已经逐渐成熟,对结构化数据的支持非常好。所以现在常用的库也会支持JSON格式的同步输出。正确的日志记录在了解了基本的日志记录库和系统之后,让我们仔细看看实际的日志记录问题。比如简单调用远程服务:asyncinvokeRemoteAPI(){constresult=awaitremoteService.got();return{result};}一般我们都会有意识地加入错误处理。asyncinvokeRemoteAPI(){try{constresult=awaitremoteService.got();}catch(err){logger.error('gotaerror,err=',err);throwerr;}return{result};}按照上面的标准格式,这个logger还需要一些其他的额外信息,比如:asyncinvokeRemoteAPI(){constpid=process.pid;conststartTime=Date.now();try{constresult=awaitremoteService.got();}catch(err){constendTime=Date.now();logger.error('pid=%s,rt=%s,gotaerror,err=',pid,Date.now()-startTime,err);throwerr;}return{result};}如果每个代码这样写的话会变得极其冗余,所以我们会提前定义好日志的输出格??式,这样在实际输出的时候可以简化,例如:constlogger=newCustomLogger({format:'${timestamp}${level}'+process.pid+${rt}'});asyncinvokeRemoteAPI(){conststartTime=Date.now();try{constresult=awaitremoteService.got();}catch(err){constendTime=Date.now();logger.error('gotaerror,err=',err,{rt:Date.now()-startTime});throwerr;}return{result};}所以在一个具体场景,如果有固定的日志字段,在日志库允许自定义的情况下,可以先定义固定的日志格式。上下文日志除了最简单的通用日志输出,还有一个相对复杂的日志,我们称之为绑定到上下文(request)的日志,它会输出与上下文关联的数据,比如前面例子中的响应时间,用户请求的ip,请求的路由,甚至链接的唯一ID等。例如:2021-07-2214:50:59,388INFO7739[-/127.0.0.1/-/0msGET/]helloworld这种情况下,用普通日志的方式加参数就不合适了。当然有同学会说我们直接定义一个新的,比如:classCustomCtxLoggerextendsCustomLogger{constructor(ctx,format){this.ctx=ctx;this.format=format;}error(...args){//xxx}Info(...args){//xxx}}每次都会初始化基类,会影响一些性能。我们使用另一种方法来减少性能影响,即代理传统日志。我们来看一个最简单的实现,以koa为例。//普通日志constlogger=newCustomLogger();classCtxLogger{constructor(ctx,logger){this.ctx=ctx;this.logger=logger;}format(){return'${timestamp}${level}'+process.pid+'[${ctx.refer}${ctx.rt}]'}}app.use(async(ctx,next)=>{//代理原始日志constctxLogger=newCtxLogger(ctx,logger);ctx.logger=ctxLogger;awaitnext();});类似这种代理原始日志的方式,减少了每次初始化新日志时的性能问题,解决了ctx上的字段透传问题。这也是上下文日志记录的常见做法。简单总结一下,了解了常用的日志库以及与日志打印的关系,也简单实现了日志库和上下文日志的实现。你现在对日志打印有基本的了解了吗?这样一套下来,相信大家对Node.js打印日志的方式有了更深入的了解,排错的时候也更加得心应手。
