在前一篇中,我們展示了如何在一個基於 Node.js + Bunyan 的應用中,使用 OpenTelemetry 收集日誌,並將其發送到 OpenTelemetry Collector,最後將數據傳送至 Loki 服務,並通過 Grafana 進行可視化。
今天,我們將專注在手寫 Opentelemetry 收集應用 log data 的部分。基於前面幾次對 tracing data 和 metric data 手寫邏輯的經驗,我們可以推測,其核心就是要覆蓋 bunyan
的引入,跟自動為 express
增加中間件的方式一樣,利用require-in-the-middle
來攔截依賴的引入。
根據 opentelemetry-instrumentation-bunyan
的原始碼,我們可以看到:
...
this._wrap(
Logger.prototype,
'_emit',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._getPatchedEmit() as any
);
...
this._wrap(
patchedExports,
'createLogger',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this._getPatchedCreateLogger() as any
);
...
這裡主要是覆蓋 bunyan
中的createLogger
和 _emit
屬性。createLogger
是創建一個 Logger 物件實例,而 _emit
方法可以獲取完整的 log
我們可以先來查看一下 bunyan
的相關原始碼:
createLogger
。我們可以看到這是創建日誌實例的主要方法。我們需要覆蓋這個方法來改變內部的_emit
行為module.exports.createLogger = function createLogger(options) {
return new Logger(options);
};
_emit
。此方法的參數rec
就是我們要獲取的 log data,我們要通過覆蓋這個方法來攔截 log data。/**
* Emit a log record.
*
* @param rec {log record}
* @param noemit {Boolean} Optional. Set to true to skip emission
* and just return the JSON string.
*/
Logger.prototype._emit = function (rec, noemit) {...}
我們可以基於此邏輯,攔截和處理日誌數據,並在日誌輸出之前進行操作。
require-in-the-middle
攔截 bunyan
的引入,覆蓋createLogger
和 _emit
,獲取完整log 物件---rec
:const hook = require('require-in-the-middle');
const demoOverwriteBanyan = () => {
hook(['bunyan'], (exports) => {
const originalCreateLogger = exports.createLogger;
exports.createLogger = (...args) => {
const logger = originalCreateLogger.apply(this, args);
// 攔截 _emit 方法來獲取完整log
const originalEmit = logger._emit;
logger._emit = function (rec, noemit) {
// `rec` 是完整的log 物件,包含所有日誌字段
console.log('---Intercepted full log:', rec);
// 繼續調用原始的 _emit 方法,確保log正常輸出
return originalEmit.call(this, rec, noemit);
};
return logger;
};
return exports;
});
};
demoOverwriteBanyan();
require('./utils/demo');
const express = require('express');
const cors = require('cors');
const { PORT } = require('./utils/config');
const { logger } = require('./utils/log');
...
app.get('/', (req, res) => {
logger.info('hihi log');
res.send('hello server');
});
可以看到,我們成功攔截並獲取到完整的log記錄。
在了解了基本邏輯後,我們可以編寫一個簡單的 SDK 來處理 log data 的收集和導出。
MockLogExporter
class MockLogExporter {
export(log) {
console.log('Exporting log:', log);
}
}
bunyan
的邏輯寫在 MockBunyanIntrument
的初始化方法中,並傳入 log exporter init(logExporter) {
// 攔截 bunyan requiring,然後覆寫 createLoger 方法
hook(['bunyan'], (exports) => {
const originalCreateLogger = exports.createLogger;
exports.createLogger = (...args) => {
const logger = originalCreateLogger.apply(this, args);
// 攔截 _emit 方法來獲取完整log
const originalEmit = logger._emit;
logger._emit = function (rec, noemit) {
// TODO: 整理成otel log 格式
logExporter.export(rec);
return originalEmit.call(this, rec, noemit);
};
return logger;
};
return exports;
});
}
class SelfSdk {
constructor() {
this.logExporter = new MockLogExporter();
this.instrument = new MockBunyanInstrumentation();
}
start() {
this.instrument.init(this.logExporter);
}
}
本文中我們完成了對 bunyan
的覆蓋、獲取到該依賴輸出到 log 物件,並且寫了一個簡單的 LogExporter ,模擬了Opentelemetry 對 log data 的採集和輸出。
完整程式碼可以在此 Github repository 中查看。
幫補充剛剛在爬這套件的 ReadMe
為什麼最後的 log 會有 hostname
與pid
邊看大大的文章邊學 XD
https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node
Node 的自動檢測機制, 以下那些屬性稱為 Resouce context, OpenTelemetry 會把這些 context 注入到 log 內容中. 更好的知道這些log來自哪裡, 什麼版本什麼時間發生這件事情.
By default, all SDK resource detectors are used, but you can use the environment variable OTEL_NODE_RESOURCE_DETECTORS to enable only certain detectors, or completely disable them:
env
host
os
process
serviceinstance
container
alibaba
aws
azure
gcp
all - enable all resource detectors
none - disable resource detection
For example, to enable only the env, host detectors:
export OTEL_NODE_RESOURCE_DETECTORS="env,host"
不過這個demo的log有 pid
和 hostname
是因為 bunyan
本身在創建log 物件的時候,就會獲取process.pid
和 os.hostname()
;
在 otel 下,應該是在exporter的時候有對log做改造,像雷N大你提到的 context 注入。(但在 otel 裡,context好像不是管理這個,而是tracing data的上下文、哪個context是activate的)
我剛剛再查看了一下原始碼,這個 OTEL_NODE_RESOURCE_DETECTORS
會在SDK創建的時候就會偵測,然後合併到 Resource
物件中,提供給 TraceProvider、
LogProvider、MeterProvider,在創建trace、log、metric data的時候就會同步