如何在 Node.js 中正確的使用日志對象

點擊上方 程序員成長指北,關注公眾號
回復1,加入高級Node交流群
作者:張挺(作者授權轉載)
地址:https://mp.weixin.qq.com/s/Pb51aYdrxAALM_wR4asDgg
日志,是開發(fā)者排查問題的非常重要的手段,有時候甚至是唯一的,所以如何合理并正確的打印日志,成了開發(fā)時的重中之重。
Node.js 中打日志的方式,一般有幾種:
1、主動展示
2、被動記錄
這兩種方式都可以由不同的模塊來實現(xiàn),我們接下去就來看看怎么選擇。
常見的主動展示
一般來說,主動一般發(fā)生在開發(fā)期,不確定狀態(tài)的時候,我們會打印一些消息,比如常見的。
console.log('hello world');這就是最簡單的主動打印的例子。
但是大多數(shù)場景下,我們都不會使用 console 來進行打印,畢竟除了內置之外,在性能和功能方面沒有特別的優(yōu)勢。
除了大眾都知道的 console 模塊,在 Node.js 領域還有一個較為知名的 debug 模塊。

可以根據(jù)命名空間打印出不同顏色的輸出,但是最最有用的,則是他的環(huán)境變量控制能力。
默認情況下(不包含任何環(huán)境變量),控制臺不會有任何輸出,而當 DEBUG 環(huán)境變量被賦值的時候,對應的命名空間的輸出才會被打印到 stdout。
DEBUG=* node app.js由于 debug 模塊由 TJ 出品,并且在非常早的時候就投入,使用過于廣泛,至今仍有非常多的模塊使用了它。
Node.js 官方一直希望能夠內置一個 debug 模塊。從 v0.11.3 開始,終于加上了一個 util.debuglog 方法。
它的功能和 debug 模塊類似,同時是內置的模塊,所以逐步也有一些模塊開始過渡到它。
const util = require('util');const debuglog = util.debuglog('foo');debuglog('hello from foo [%d]', 123);
它的開關也類似,使用的是 NODE_DEBUG 環(huán)境變量,應該是特意和 debug 模塊做了區(qū)分。
NODE_DEBUG=foo node app.js被動記錄的方式
除了上面提到的類 console 等方式,我們常見的就是各種日志庫默認記錄的日志,由于這些日志平時只是默默的記錄,并不會過多關注,只會在特殊需要的時候(比如差錯,定位,計算時)才會查看,所以我們歸類為 “被動的方式”。
大多的三方庫都有類似的功能,比如 log4j,winston,pino 等等。
這些庫的核心功能一般是:
1、將日志輸出到不同的渠道(比如控制臺、文本文件)
2、日志格式的自定義(文本或者 JSON)
3、日志的輸出等級(warn,debug,error)
4、其他的一些能力,比如切割和文件輪轉,壓縮等等
這些庫用起來一般就比較簡單,獲取實例,調用方法輸出即可。
logger.info('hello world');注意,這里我們會觀察到輸出有一些不一樣的地方。
2021-07-22 14:50:59,388 INFO 7739 [xxx] hello world整個日志是安裝上面類似標準的結構來進行輸出的,計算是 Error,也是相同的類似格式,那么這個結構包含了哪幾部分東西呢?
日志格式
其實整個日志格式追溯,可以到很久以前,不管是 JAVA 默認的 Simple Logger 結構還是類似 nginx 等反向代理服務器的日志,都會包含一些固定的字段,這些固定的字段長久以來形成了一種輸出約定,將這些字段組合起來,形成了當今的日志格式。
當前的日志格式一般會包括幾個部分。
時間戳
日志等級
進程id(node)
日志的標簽(label,from xxx class)
消息體(字符串或者 error stack)
除此之外,可能還有一些自定義的內容,比如執(zhí)行消耗的時間,用戶 id,文本長度等等內容。
在文本結構的輸出中,這些字段將被空格(space)分隔,以換行符作為結尾(\n),這樣可以方便外部的日志采集系統(tǒng)采集,比如阿里云的 SLS 等等。
每個公司會有自己的日志采集和輸出規(guī)范,所以一般常見的庫都會支持自定義的日志格式,但是不管如何變化,基礎的字段(上述)都還會存在。
隨著系統(tǒng)的迭代,先進使用 JSON 格式來記錄日志的方式也逐步出現(xiàn),以 Logstash 為首的一些數(shù)據(jù)(日志)采集分析一體的工具,也逐步的成熟,對結構化的數(shù)據(jù)支持的也很好,所以現(xiàn)在常見的庫也會同步支持 JSON 格式輸出。

正確的打日志
在了解了基本的日志庫和體系之后,我們來具體看一看真正打日志的問題。
比如一個簡單調用遠端服務:
async invokeRemoteAPI() {const result = await remoteService.got();return {result};}
一般,我們會有意識的加上錯誤處理。
async invokeRemoteAPI() {try {const result = await remoteService.got();} catch(err) {logger.error('got a error, err=', err);throw err;}return {result};}
按照上面的標準格式,這個 logger 還需要其他的一些額外信息,比如:
async invokeRemoteAPI() {const pid = process.pid;const startTime = Date.now();try {const result = await remoteService.got();} catch(err) {const endTime = Date.now();logger.error('pid=%s, rt=%s, got a error, err=', pid, Date.now() - startTime, err);throw err;}return {result};}
如果每個代碼都這么寫,就會變得無比冗余,所以,我們會提前將日志的輸出格式定義完畢,這樣,在實際輸出的時候就可以簡化,比如:
const logger = new CustomLogger({format: '${timestamp} ${level} ' + process.pid + ${rt}'});async invokeRemoteAPI() {const startTime = Date.now();try {const result = await remoteService.got();catch(err) {const endTime = Date.now();a error, err=', err, {rt: Date.now() - startTime});throw err;}return {result};}
所以在特定場景下,如果有固定的日志字段,在日志庫允許自定義的情況下,可以先定義好固定的日志格式。
上下文日志
除了最簡單的通用日志輸出之外,還有一種相對復雜的日志,我們稱之為和上下文(請求)綁定的日志,這類日志會輸出上下文相關聯(lián)的數(shù)據(jù),比如之前示例中的響應時間,用戶請求的 ip,請求的路由,甚至是鏈路的唯一 ID 等等。
比如:
2021-07-22 14:50:59,388 INFO 7739 [-/127.0.0.1/-/0ms GET /] hello world這種情況下,再用普通日志的方式加入?yún)?shù)就不合適了。
當然,有些同學會說,我們直接定義一個新的,比如:
class CustomCtxLogger extends CustomLogger {constructor(ctx, format) {this.ctx = ctx;this.format = format;}error(...args) {//xxx}info(...args) {//xxx}}
這樣的做法,每次都會讓基類做初始化,會影響部分性能。我們使用另一種方式來減少性能影響,代理傳統(tǒng)日志。
我們來看看最簡單的實現(xiàn)方式,以 koa 為例。
// 普通日志const logger = new CustomLogger();class CtxLogger {constructor(ctx, logger) {this.ctx = ctx;this.logger = logger;}format() {return '${timestamp} ${level} ' + process.pid + '[${ctx.refer} ${ctx.rt}]'}}app.use(async (ctx, next) => {// 代理原始日志const ctxLogger = new CtxLogger(ctx, logger);ctx.logger = ctxLogger;await next();});
類似這種通過代理原始日志的方式,即減少了每次初始化新日志時的性能問題,又解決了 ctx 上字段透傳的問題。
這也是常見的上下文日志的實踐。
簡單總結一下
我們了解了常用的日志庫以及和日志打印的關系,也簡單的實現(xiàn)了日志庫以及上下文日志的實現(xiàn),是不是現(xiàn)在對日志打印了有了一個基本的了解?
這樣一套下來,相信你對 Node.js 打印日志的方式更加的了解,也在排錯時游刃有余了。
我組建了一個氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對Node.js學習感興趣的話(后續(xù)有計劃也可以),我們可以一起進行Node.js相關的交流、學習、共建。下方加 考拉 好友回復「Node」即可。

“分享、點贊、在看” 支持一波 
