iT邦幫忙

2024 iThome 鐵人賽

DAY 24
1
DevOps

全端監控技術筆記---從Sentry到Opentelemetry系列 第 24

Day24---手寫一個獲取 log data 的 SDK

  • 分享至 

  • xImage
  •  

前言

在前一篇中,我們展示了如何在一個基於 Node.js + Bunyan 的應用中,使用 OpenTelemetry 收集日誌,並將其發送到 OpenTelemetry Collector,最後將數據傳送至 Loki 服務,並通過 Grafana 進行可視化。

今天,我們將專注在手寫 Opentelemetry 收集應用 log data 的部分。基於前面幾次對 tracing data 和 metric data 手寫邏輯的經驗,我們可以推測,其核心就是要覆蓋 bunyan 的引入,跟自動為 express 增加中間件的方式一樣,利用require-in-the-middle來攔截依賴的引入。

覆蓋 Bunyan

根據 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) {...}

我們可以基於此邏輯,攔截和處理日誌數據,並在日誌輸出之前進行操作。

demo 覆蓋 bunyan,獲取完整log

  • 根據上述的思路,利用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();
  • 接著,在入口文件中引入這段邏輯,讓其無縫集成到 NodeJS 應用中:
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');
});
  • 執行並呼叫後,查看console:

image

可以看到,我們成功攔截並獲取到完整的log記錄。

加入 Exporter,形成完整SDK

在了解了基本邏輯後,我們可以編寫一個簡單的 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;
        });
    }
  • 在SDK中使用這些邏輯
class SelfSdk {
    constructor() {
        this.logExporter = new MockLogExporter();
        this.instrument = new MockBunyanInstrumentation();
    }

    start() {
        this.instrument.init(this.logExporter);
    }
}
  • 最後,我們可以來看看再一次呼叫 create log 的 endpoint,查看 LogExporter 輸出結果:

image

小結

本文中我們完成了對 bunyan 的覆蓋、獲取到該依賴輸出到 log 物件,並且寫了一個簡單的 LogExporter ,模擬了Opentelemetry 對 log data 的採集和輸出。

完整程式碼可以在此 Github repository 中查看。

ref

ChangeLog

  • 20241008--補充內文與圖片
  • 20241001--初稿

上一篇
Day23--簡單demo看看 Opentelemetry log data + Loki + Grafana
下一篇
Day25--關於遙測數據在Opentelemetry Collector的整合
系列文
全端監控技術筆記---從Sentry到Opentelemetry30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
雷N
iT邦研究生 1 級 ‧ 2024-10-08 12:25:04

幫補充剛剛在爬這套件的 ReadMe

為什麼最後的 log 會有 hostnamepid
邊看大大的文章邊學 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"

jamieleee iT邦新手 5 級 ‧ 2024-10-08 14:08:28 檢舉

不過這個demo的log有 pidhostname 是因為 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的時候就會同步

我要留言

立即登入留言