在前一篇中,我們知道了基於opentelemetry協議的trace機制,是在請求headers中新增、修改traceparent
屬性,得到這個請求trace的上下文。接下來我們將簡單實作看看。
「一個服務在收到請求後,要查看請求headers是否有traceparent
」,這一段,很明顯可以用 middleware 的方式來達成:
const app = express()
app.use((req, res, next) => {
console.log('---req headers: ', req.headers);
next();
});
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--
因此,我們可以透過require-in-the-middle
的使用來無侵入地將express直接apply我們的攔截請求中間件。
回到Opentelemetry中的tracing流程,我們需要在一個tracing span中完成
所以,在我們的簡單demo中,需要一個Span
物件、以及處理span的Processor
物件、以及生成span的Tracer
Span物件,用來存放上下文,以及有一個end
方法來呼叫processer
處理自己
class Span {
constructor(traceId, spanId, parentSpanId, name, processor) {
...存放上下文
// recording start date time
this.startTime = Date.now();
}
end() {
this.processor.onEnd(this);
...
}
}
存放exporter
,有一個onEnd
方法來讓exporter把span給export到存儲服務
class SimpleSpanProcessor {
constructor(exporter) {
this.exporter = exporter;
}
onEnd(span) {
this.exporter.export([span]); // 傳送 span 到 Exporter
}
}
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 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記錄:
在本文中,我們使用中間件攔截並修改請求、增加或更新 traceparent
header,並且使用require-in-the-middle
來完成無侵入式的宣告使用該 middleware,完成了對 Express 請求的攔截!
以上程式碼可以在此 Github repository 中查看。