在前一篇中,能夠在NodeJS express的專案中使用宣告的方式來自動增加中間件、攔截請求,並且獲取traceparent
header。
下一步,就是要處理「發起請求時,附上traceparent
header」,也就是對請求的覆蓋。
而在NodeJS中,網路HTTP請求幾乎都是由http
和https
這兩個庫來實現,所以我們就專注在對這兩個庫的請求覆蓋。
const originalHttpRequest = http.request
http.request = (options, callback) => {
console.log('---intercept http');
return originalHttpRequest(options, callback)
};
demo.js
中:const http = require('http');
const demoStart = () => {
const originalHttp = http.request;
http.request = (option, callback) => {
console.log('---intercept HTTP request');
return originalHttp(option, callback);
};
};
const { demoStart } = require('./utils/demo');
demoStart();
const express = require('express');
const cors = require('cors');
....
app.get('/demo', async (req, res) => {
const data = await axios.get(`${PYTHON_SERVICE_URL}/api/demo-01`);
res.send('demo success');
});
根據上述,我們了解到了覆蓋Http請求的方法。接下來是要在請求中增加traceparent
header,然後再發出請求給其他service。但,要如何獲取到當前的traceId和spanId、把其當作parentSpanId來傳給其他service?
在上一篇「攔截請求」中,我們是先簡單在請求過來的時候,檢查請求的headers;但這個作法沒辦法讓其他feature(如現在的覆蓋請求)獲取到當時的上下文。此時,需要獲取到當前這個請求的trace 上下文,在Opentelemetry中就是 Context Propagation。
Context 就是包含每個請求或操作的 traceId 和 spanID 等請求鏈路上下文,在 OpenTelemetry 中,Context 負責攜帶這些追蹤資料,它的作用是確保在你的應用程式中,不論是同一個請求的不同部分還是跨服務,這些追蹤資料都能夠正確傳遞下去。
它是在應用程式內部保持 tracing data 的一種機制,例如在處理一個請求的時候,Context就會確保在這個應用程式內部都可以共享同一個 tracing 上下文。(不過目前的demo,我們先直接用應用內的全局變量來存儲 context。)
然後當需要對其他服務發送請求的時候,Context需要透過 HTTP headers(例如 traceparent
)來把該上下文傳給下一個服務,這個機制就是 Propagation---在請求header中、對context的解析以及注入
簡單程式碼如下:
class Propagation {
inject(context, headers) {
const traceparent = `00-${context.traceId}-${context.spanId}-01`;
headers['traceparent'] = traceparent;
}
extract(headers) {
const traceparent = headers['traceparent'];
if (traceparent) {
const parts = traceparent.split('-');
return {
traceId: parts[1],
spanId: parts[2],
};
}
return null;
}
}
完整的對http攔截的demo的程式碼如下:
_interceptHttp() {
this.originalHttpsRequest = http.request;
// 攔截發起的 HTTP 請求,附上 traceparent header
http.request = (options, callback) => {
// 記錄請求
const currSpan = this._tracingHandler(options);
// 注入trace context
propagation.inject(
{ traceId: currSpan.traceId, spanId: currSpan.spanId },
options.headers,
);
const req = this.originalHttpsRequest(options, (response) => {
// 當請求完成時,結束 span
currSpan.end();
if (callback) callback(response);
});
req.on('error', (error) => {
console.error(`Request error: ${error.message}`);
currSpan.end(); // 當請求失敗時也結束 span
});
return req;
};
}
接著我們嘗試呼叫另一個 server 的 api,在該 api 的 controller 中打印 headers ,可以看到有包含 traceparent
,代表我們注入的 trace context 成功傳遞到另一個service中
在今天和昨天的兩篇文章中,我們完成了在opentelemetry協議中的trace簡單demo,能夠攔截請求、獲取trace id、新增span id、然後再傳遞給下一個service。
完整程式碼可以在此 Github repository查看。
在之後的研究中,才發現也可以直接使用 http.Server.prototype
來攔截接收到的請求;而使用 express middleware 來攔截主要是可以更精確追蹤到請求經過哪些middleware、route等。