iT邦幫忙

2024 iThome 鐵人賽

DAY 18
1
DevOps

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

Day18--寫一個有關trace的sdk(上)--攔截接收的請求

  • 分享至 

  • xImage
  •  

前言

在前一篇中,我們知道了基於opentelemetry協議的trace機制,是在請求headers中新增、修改traceparent屬性,得到這個請求trace的上下文。接下來我們將簡單實作看看。

如何攔截接收到的請求

使用中間件

「一個服務在收到請求後,要查看請求headers是否有traceparent」,這一段,很明顯可以用 middleware 的方式來達成:

  • 先試試在一個 middleware 中,當每個請求進來後,打印 headers
const app = express()

app.use((req, res, next) => {
    console.log('---req headers: ', req.headers);
    next();
});
  • 來修改header,增加traceparent 屬性
app.use((req, res, next) => {
    if (!req.headers['traceparent']) {
        req.headers['traceparent'] = 'hello traceparent';
    }
    console.log('---req headers: ', req.headers);
    next();
});

可以在console中看到,request的headers中已經有了traceparent

無侵入增加中間件

然而,在使用@opentelemetry/sdk-node的時候,我們發現它並沒有需要再寫一個中間件並且執行,而是在 sdk 初始化的時候、就自動生成了中間件並且自動 apply,這是如何做到的?

我們從opentelemetry-js的原始碼中可以看到,它是使用require-in-the-middle這個依賴---它可以捕捉 NodeJS應用在引入依賴庫的時候,讓開發者可以在這個階段做修改。

我們可以先demo看看,在引入express的時候,做一個console---

  • demo.js
const hook = require('require-in-the-middle');

const demoStart = () => {
    // 攔截 exporess
    hook(['express'], (exports) => {
        const original = exports;
        return (...args) => {
            const app = original(...args);

            console.log('---while during requiring express');
            return app;
        };
    });
};

module.exports = {
    demoStart,
};

然後在原本的express app 根文件中的最上層引入:

  • main.js
const { demoStart } = require('./demo');
demoStart();

const express = require('express');
const cors = require('cors');

const app = express();
const PORT = 3030;
...

運行之後,可以看到 terminal中的console--
image

因此,我們可以透過require-in-the-middle的使用來無侵入地將express直接apply我們的攔截請求中間件。

定義Tracer和Span相關物件

回到Opentelemetry中的tracing流程,我們需要在一個tracing span中完成

  • 開始span
  • 記錄上下文如traceID、parentSpanID。
  • 解析 span,將上下文傳給存儲服務(exporter)

所以,在我們的簡單demo中,需要一個Span物件、以及處理span的Processor物件、以及生成span的Tracer

span

Span物件,用來存放上下文,以及有一個end方法來呼叫processer處理自己

class Span {
    constructor(traceId, spanId, parentSpanId, name, processor) {
        ...存放上下文

        // recording start date time
        this.startTime = Date.now();
    }

    end() {
        this.processor.onEnd(this);
        ...
    }
}

Processor

存放exporter,有一個onEnd方法來讓exporter把span給export到存儲服務

class SimpleSpanProcessor {
    constructor(exporter) {
        this.exporter = exporter;
    }
    onEnd(span) {
        this.exporter.export([span]); // 傳送 span 到 Exporter
    }
}

tracer

Tracer物件,存放這個服務的name、以及一個startSpan方法來生成新的span,同時將該請求的traceId和paretSpanId(如果有的話)給傳到新的span中。

const crypto = require('crypto');

const Span = require('./span');

// 生成隨機的 traceId 和 spanId
function generateId(bytes) {
    return crypto.randomBytes(bytes).toString('hex');
}

class Tracer {
    constructor(name, processor) {
        this.name = name;
        this.processor = processor;
    }

    startSpan(name, options) {
        ... //獲取traceId、或者生成新的traceid;以及parentSpanId
        return new Span(traceId, spanId, parentSpanId, name, this.processor);
    }
}

整合進 express 中間件

以上我們知道了如何無侵入的使用express middleware,也定義好了 trace process 所需要的物件,接下來我們就來定義 MyTracingSDK,裡面有個start方法來啟動sdk、同時進行攔截 express 引入來apply middleware;而在這個 middleware 中,我們要獲取請求、生成 span:

const hook = require('require-in-the-middle');

const Tracer = require('./tracer');
const { MockExporter, SimpleSpanProcessor } = require('./processor');

class MyTracingSDK {
    constructor(serviceName) {
        ...
    }
    start() {
        ...
        // 攔截 express requiring
        hook(['express'], (exports) => {
            const original = exports;
            return (...args) => {
                const app = original(...args);
                app.use((req, res, next) => {

                    // 攔截請求,生成span
                    const currSpan = this._tracingHandler(req); // 記錄請求

                    // 當請求結束時,結束 span
                    res.on('finish', () => {
                        currSpan.end();
                        this.traceContext = null;
                    });
                    next();
                });
                return app;
            };
        });
    }

    _tracingHandler(req) {
        ... 獲取req headers中的traceparent
      
        // 理論上這個span name是一個 operation,但目前就直接mock http
        const newSpan = this.tracer.startSpan('mock-http', {
            traceId,
            parentSpanId: parentSpan,
            version,
            traceFlag,
        });

        return newSpan;
    }
}

然後就可以在應用的根文件中的最上層引入sdk,並且完成初始化和start方法:

const MyTracingSDK = require('./utils/self-otel/sdk');
const sdk = new MyTracingSDK();
sdk.start();

const express = require('express');
const cors = require('cors');

const app = express();
const PORT = 3030;

運行過後,請求一個api,可以看到terminal中已經有我們這個請求的span記錄:

image

小結

在本文中,我們使用中間件攔截並修改請求、增加或更新 traceparent header,並且使用require-in-the-middle 來完成無侵入式的宣告使用該 middleware,完成了對 Express 請求的攔截!

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

ref

ChangeLog

  • 20241002--補內文與圖片
  • 20240924--初稿

上一篇
Day17--在 Opentelemetry 中,是如何發起trace/span
下一篇
Day19-- 寫一個有關trace的sdk(下)--攔截發起的請求
系列文
全端監控技術筆記---從Sentry到Opentelemetry30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言