首頁  >  文章  >  web前端  >  手把手教你在node中怎麼優雅列印全鏈路日誌

手把手教你在node中怎麼優雅列印全鏈路日誌

青灯夜游
青灯夜游轉載
2022-03-07 20:05:102505瀏覽

node如何優雅地列印全鏈路日誌?以下這篇文章就來跟大家介紹一下在node中優雅列印全連結日誌的方法,希望對大家有幫助!

手把手教你在node中怎麼優雅列印全鏈路日誌

當使用者報問題:線上某個功能使用報錯時,如何快速且準確地定位?當某個請求介面返回資料緩慢時,如何有效追蹤優化?

一、原理和實踐

眾所周知,當一個請求到來時,大概會有以下日誌產生:

1、AceesLog:使用者訪問日誌

2、Exception:程式碼異常日誌

3、SQL:sql查詢日誌

4、ThirdParty:第三方服務日誌

該如何追蹤一條請求產生的所有日誌?

一般做法是使用一個requestId來做唯一標識,

然後寫一個中間件,把requestId注入到context上下文中,當需要打日誌時,再從context中取出打印,

在第三方服務和SQL日誌中,還需要把requestId傳入到對應的函數裡面列印,這樣層層傳遞,實在太麻煩,程式碼侵入性也比較強。

我們的目標是降低程式碼侵入性,一次注入,自動追蹤。

經過調查,async_hooks可以追蹤異步行為的生命週期,在每個非同步資源(每個請求都是一個非同步資源)中,它都有2個ID,

分別是asyncId(非同步資源目前生命週期ID),trigerAsyncId(父級非同步資源ID)。

async_hooks提供了以下生命週期鉤子來監聽非同步資源:

asyncHook = async_hook.createHook({
  // 监听异步资源的创建
  init(asyncId,type,triggerAsyncId,resource){},
  // 异步资源回调函数开始执行之前
  before(asyncId){},
  // 异步资源回调函数开始执行后
  after(asyncId){},
  // 监听异步资源的销毁
  destroy(asyncId){}
})

那如果我們做一個映射,每個asyncId映射一個storage,storage裡面再儲存對應的requestId,那requestId就可以很容易取得了。

正好cls-hooked這個函式庫已經基於async_hooks做好了封裝,在同一份非同步資源維護一份數據,以鍵值對的形式儲存。 (注意:async_hooked需要在高版本node>=8.2.1使用)當然社群中還有其他的實現,例如cls-session,node-continuation-local-storage等。

下面講下我把cls-hooked運用在我專案中的實例:

/session.js 建立命名儲存空間

const createNamespace = require('cls-hooked').createNamespace 
const session = createNamespace('requestId-store') 
module.exports = session

/logger.js 列印日誌

const session = require('./session') 
module.exports = { 
info: (message) => 
{ 
const requestId = session.get('requestId')
console.log(`requestId:${requestId}`, message) 
}, 
error: (message) => 
{ 
const requestId = session.get('requestId') 
console.error(`requestId:${requestId}`, message) 
} 
}

/sequelize.js sql呼叫logger列印日誌

const logger = require("./logger") 
new Sequelize( 
logging: function (sql, costtime) { 
logger.error( `sql exe : ${sql} | costtime ${costtime} ms` ); 
} )

/app.js 設定requestId、設定requestId回傳回應頭、列印存取日誌

const session = require('./session') 
const logger = require('./logger') 
async function accessHandler(ctx, next) 
{ 
const requestId = ctx.header['x-request-id'] || uuid()
const params = ctx.request.body ? JSON.stringify(ctx.request.body) : JSON.stringify(ctx.request.query)
// 设置requestId session.run(() => { session.set('requestId', requestId)
logger.info(`url:${ctx.request.path};params:${params}`) next()
// 设置返回响应头
ctx.res.setHeader('X-Request-Id',requestId)
}) }

我們看下當一條請求路徑是/home?a=1到來時的日誌:

访问日志:
requestId:79f422a6-6151-4bfd-93ca-3c6f892fb9ac url:/home;params:{"a":"1"}

Sql日志:
requestId:79f422a6-6151-4bfd-93ca-3c6f892fb9ac sql exe :
Executed (default): SELECT `id` FROM t_user

可以看到同一條請求整個連結的日誌requestId是一樣的。如果後面有告警發到告警平台,那麼我們根據requestId就可以找到這條請求執行的整個連結了。

細心的同學可能會觀察到我在介面回傳的回應頭裡面也設定了requestId,目的就是為了後續如果發現某條請求回應緩慢或有問題,那直接從瀏覽器就可以知道requestId ,就可以做分析了。

二、效能開銷

我本地做了一下壓測,

這是記憶體的佔用比較:

手把手教你在node中怎麼優雅列印全鏈路日誌

#比未使用async_hook多了約10%。

對於我們qps是百級的系統還好,但是如果是高並發的服務,可能要慎重考慮下了。

ps:有錯誤歡迎指出,不喜勿噴

更多node相關知識,請造訪:nodejs 教學

以上是手把手教你在node中怎麼優雅列印全鏈路日誌的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.cn。如有侵權,請聯絡admin@php.cn刪除