先前我們介紹了 Logging 與 Metrics 兩個服務監控機制,今天要來探討 Moleculer 內建的 Tracing 模組,它用於收集 Moleculer 內部應用的追蹤資訊,你可以使用 Tracing 工具來建立服務間的相互關係圖[Fig. 1.] 。內建有 Zipkin
、 Jaeger
、 Datadog
可以直接使用。
注意,此功能可能會在 v0.15 版本進行大幅改動,你可能需要考慮是否要使用目前內建的模組,詳情請參考上一篇文章。
Fig. 1. Grafana Tempo 的 Node Graph[2]
範例:快速使用
moleculer.config.js
// moleculer.config.js
module.exports = {
tracing: true
};
範例:選項設定方式
名稱 | 類型 | 預設值 | 說明 |
---|---|---|---|
enabled |
<Boolean> | false |
啟動 tracing 功能 |
exporter |
<Object> | <Object[]> | null |
Tracing 輸出配置 |
sampling |
<Object> | 取樣設定 | |
actions |
<Boolean> | true |
在 Actions 啟用 Tracing |
events |
<Boolean> | false |
在事件啟用 Tracing |
errorFields |
<String[]> | ["name", "message", "code", "type", "data"] |
要加到 Span 標籤的錯誤物件欄位 |
stackTrace |
<Boolean> | false |
是否要將錯誤案件的堆疊追蹤資訊加到 Span 標籤 |
tags |
<Object> | null |
加入客製化 Span 標籤到 Actions 與事件 |
defaultTags |
<Object> | null |
預設標籤,它會被加進所有的 spans 。 |
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
exporter: "Console",
events: true,
stackTrace: true
}
};
Moleculer 有多種 Tracing 的取樣方法。它會由 Root Span 開始確認是否有正常取樣且傳播到所有的 Child Spans 。如此一來,可以確保無論使用哪種取樣方法或速率,都會輸出完整追蹤路線。
此取樣方法使用 0 ~ 1 的固定取樣率,設為 1
表示所有的 Spans 都將被取樣,設為 0
時則都不取樣。
範例:取樣所有 Spans
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
sampling: {
rate: 1.0
}
}
};
範例:取一半的 Spans
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
sampling: {
rate: 0.5
}
}
};
此取樣方法採用速率限制,你可以設置一秒鐘內可以取樣幾個 Spans 。
範例:每秒取樣 2 個 Spans
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
sampling: {
tracesPerSecond: 2
}
}
};
範例:每 10 秒取樣 1 個 Span
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
sampling: {
tracesPerSecond: 0.1
}
}
};
Tracing 支援多個輸出器、客製化 tracing spans 及整合儀表板套件 (例如: dd-trace[3] )。
Console
是一種除錯用輸出器,可以將完整的本地 trace 列印到主控台。但是只能本地使用,不能遠端追蹤遠端呼叫。
範例:
module.exports = {
tracing: {
enabled: true,
exporter: {
type: "Console",
options: {
// 客製化 logger
logger: null,
// 啟用顏色
colors: true,
// 寬度
width: 100,
// 測量寬度
gaugeWidth: 40
}
}
}
};
Datadog
輸出器會透過 dd-trace[3] 將 tracing 資料發送到 Datadog
伺服器。
使用前請安裝 dd-trace 套件
npm install dd-trace --save
。
Fig. 2. Datadog
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
exporter: {
type: "Datadog",
options: {
// Datadog 服務器 URL
agentUrl: process.env.DD_AGENT_URL || "http://localhost:8126",
// 環境變數
env: process.env.DD_ENVIRONMENT || null,
// 取樣優先度,更多請參閱[4]
samplingPriority: "AUTO_KEEP",
// 預設標籤,它會被加進所有的 spans
defaultTags: null,
// 客製化 Datadog Tracer 選項,更多請參閱[5]
tracerOptions: null,
}
}
}
};
moleculer.config.js
Event
輸出器會將 tracing 資料發送到 Moleculer 事件 ( $tracing.spans
) 。
module.exports = {
tracing: {
enabled: true,
exporter: {
type: "Event",
options: {
// 事件名稱
eventName: "$tracing.spans",
// 當 span 開始時發送事件
sendStartSpan: false,
// 當 span 結束後發送事件
sendFinishSpan: true,
// 是否廣播
broadcast: false,
// 事件群組
groups: null,
// 發送時間間隔 (秒)
interval: 5,
// 發送前的客製化 Span 物件轉換器
spanConverter: null,
// 預設標籤,它會被加進所有的 spans
defaultTags: null
}
}
}
};
為了避免與舊版混淆,本文不特別解說舊版的事件輸出器,會來看本系列的人應該也不會是舊版使用者,欲知詳情可以查看官方手冊說明:
https://moleculer.services/docs/0.14/tracing.html#Event-legacy
Jaeger
輸出器會發送 tracing spans 資訊到 Jaeger 伺服器。
使用前請安裝 jaeger-client 套件
npm install jaeger-client --save
。
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
exporter: {
type: "Jaeger",
options: {
// HTTP 報表產生器端點
endpoint: null,
// UDP 發送器 host
host: "127.0.0.1",
// UDP 發送器連接埠
port: 6832,
// Jaeger 取樣配置
sampler: {
// 取樣類型[6]
type: "Const",
// 取樣配置
options: {}
},
// `Jaeger.Tracer` 的附加選項
tracerOptions: {},
// 預設標籤,它會被加進所有的 spans
defaultTags: null
}
}
}
};
Zipkin
輸出器會發送 tracing spans 資訊到 Zipkin 伺服器。
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
exporter: {
type: "Zipkin",
options: {
// Zipkin 伺服器的 Base URL
baseURL: "http://localhost:9411",
// 發送時間間隔 (秒)
interval: 5,
// 附加資料選項
payloadOptions: {
// 設定啟用 `debug` 屬性
debug: false,
// 設定啟用 `shared` 屬性
shared: false
},
// 預設標籤,它會被加進所有的 spans
defaultTags: null
}
}
}
};
NewRelic
輸出器會以 Zipkin v2 格式發送 tracing spans 資訊到 NewRelic 伺服器。
moleculer.config.js
{
tracing: {
enabled: true,
events: true,
exporter: [
{
type: 'NewRelic',
options: {
// NewRelic 伺服器的 Base URL
baseURL: 'https://trace-api.newrelic.com',
// NewRelic Insert Key
insertKey: 'my-secret-key',
// 發送時間間隔 (秒)
interval: 5,
// 附加資料選項
payloadOptions: {
// 設定啟用 `debug` 屬性
debug: false,
// 設定啟用 `shared` 屬性
shared: false,
},
// 預設標籤,它會被加進所有的 spans
defaultTags: null,
},
},
],
},
}
你也可以建立客製化的輸出器,官方建議可以參考 Console Exporter[9] 的原始碼來修改,再實作 init
、 stop
、 spanStarted
、 spanFinished
方法。
範例:建立客製化 Tracing
my-tracing-exporter.js
const TracerBase = require("moleculer").TracerExporters.Base;
class MyTracingExporters extends TracerBase {
init() { /*...*/ }
stop() { /*...*/ }
spanStarted() { /*...*/ }
spanFinished() { /*...*/ }
}
module.exports = MyTracingExporters;
範例:使用客製化 Tracing
moleculer.config.js
const MyTracingExporters = require("./my-tracing-exporter");
module.exports = {
tracing: {
enabled: true,
exporter: [
new MyTracingExporters(),
]
}
};
你可以在 exporter
以陣列來定義多個輸出器。
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
exporter: [
"Console",
{
type: "Zipkin",
options: {
baseURL: "http://localhost:9411",
}
},
{
type: "Jaeger",
options: {
host: "127.0.0.1",
}
}
]
}
};
你可以在 Actions 或事件處理器裡面使用 ctx.startSpan
及 ctx.finishSpan
方法來加入新的 spans 。
posts.service.js
module.exports = {
name: "posts",
actions: {
async find(ctx) {
const span1 = ctx.startSpan("get data from DB", {
tags: {
...ctx.params
}
});
const data = await this.getDataFromDB(ctx.params);
ctx.finishSpan(span1);
const span2 = ctx.startSpan("populating");
const res = await this.populate(data);
ctx.finishSpan(span2);
return res;
}
}
};
範例:當 Context 不可用時,你也可以使用 broker.tracer
來建立 Span 。
posts.service.js
module.exports = {
name: "posts",
started() {
// 建立一個 span 來初始化資料庫
const span = this.broker.tracer.startSpan("initializing db", {
tags: {
dbHost: this.settings.dbHost
}
});
await this.db.connect(this.settings.dbHost);
// 建立 sub-span 來建立資料表
const span2 = span.startSpan("create tables");
await this.createDatabaseTables();
// 結束 sub-span
span2.finish();
// 結束 span
span.finish();
}
};
當你使用外部隊列通訊時也可以連接到 Spans (例如: moleculer-channels[10] )。你只需要將 parentID
及 requestID
丟給處理程序,然後使用這些 ID 來啟動客製化 span。
範例:連接 Spans
module.exports = {
name: "trace",
actions: {
async extractTraces(ctx) {
// 從 Context 取得 `parentID` 及 `requestID`
const { parentID, requestID: traceID } = ctx;
// 將 `parentID` 及 `traceID` 作為參數發送到遠端隊列
await this.broker.sendToChannel("trace.setSpanID", {
// 發送 IDs 參數
parentID,
traceID,
});
},
},
channels: {
"trace.setSpanID"(payload) {
// 將參數帶進客製化 span
const span = this.broker.tracer.startSpan("my.span", payload);
// ... 撰寫邏輯
span.finish(); // 結束客製化 span
},
},
};
你可以客製化 trace span 的名稱。這種情況下,必須將 spanName
設為一個 String
或是 Function
。
範例:建立客製化名稱函數
posts.service.js
module.exports = {
name: "posts",
actions: {
get: {
tracing: {
spanName: ctx => `Get a post by ID: ${ctx.params.id}`
},
async handler(ctx) {
// ...
}
}
}
};
你可以設定要將那些 Context 的 params
或 meta
加進 span 標籤。
範例:預設只會加入 ctx.params
的所有屬性
posts.service.js
module.exports = {
name: "posts",
actions: {
get: {
tracing: {
// 加入 `params` ,但不加入 `meta`
tags: {
params: true,
meta: false,
}
},
async handler(ctx) {
// ...
}
}
}
};
範例:客製化 params
posts.service.js
module.exports = {
name: "posts",
actions: {
get: {
tracing: {
tags: {
// 加入 `ctx.params.id`
params: ["id"],
// 加入 `ctx.meta.loggedIn.username`
meta: ["loggedIn.username"],
// 在 action 響應中加入標籤
response: ["id", "title"]
}
},
async handler(ctx) {
// ...
}
}
}
};
範例:使用客製化函數來產生 span 標籤
posts.service.js
module.exports = {
name: "posts",
actions: {
get: {
tracing: {
// 可由引數取得 Context 資料
tags(ctx, response) {
return {
params: ctx.params,
meta: ctx.meta,
custom: {
a: 5
},
response
};
}
},
async handler(ctx) {
// ...
}
}
}
};
注意,當你在 Action 中使用這個方法時,執行成功的話函數將會被呼叫兩次。第一次只會響應
ctx
資訊,第二次則會包含ctx
與response
資訊。
你可以在配置的 tracing.tags
中設定全域的 Action 與事件的 span 標籤,除非你在服務的 Action 與事件內去覆蓋設定,否則它將會套用到所有的 Action 與事件。所有前述的客製化標籤類型都是有效的。雖然在服務中 Action 與事件的標籤都是優先套用的,但 params
、 meta
與 response
標籤的定義卻不是,意思就是你仍然可以在每個服務中,去定義全域的 meta
標籤或本地 response 標籤。
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
tags: {
action: {
// 永不加入 params
params: false,
// 由 `ctx.meta` 新增 `loggedIn.username` 值
meta: ["loggedIn.username"],
// 總是加入 response
response: true,
},
event(ctx) {
return {
params: ctx.params,
meta: ctx.meta,
// 加入呼叫功能
caller: ctx.caller,
custom: {
a: 5
},
};
},
}
}
};
使用配置的 tags 屬性所定義的客製化標籤可以使用
ctx
引數,如果是在 action 使用還可以拿到response
。若是在defaultTags
選項中定義的標籤則必須是靜態的物件,或是寫成一個函數並在接收 tracer 實例後返回一個物件。它也可以透過 tracer 實例請求 broker 實例,但無法透過ctx
請求。
範例:事件 tracing 只需在設置中加上 events: true
。
moleculer.config.js
module.exports = {
tracing: {
enabled: true,
events: true
}
};
通常不建議在 ctx.params
或 ctx.meta
發送不可序列化的參數(例如: http request 、 socket 實例及串流實例等)。如果啟用了 tracing ,則 tracer 輸出器將試著遞迴展平參數(使用 flattenTags method[11] ),這將會導致最大呼叫堆疊錯誤。
為了避免發生問題,可以在輸出器設定使用 safetyTags
,設為 true
的時候,輸出器將不會進入循環展平,此選項可以在任何的內建輸出器使用。
注意,當你啟動這個功能,有可能導致系統的效率下降。
範例:全域使用 safetyTags
moleculer.config.js
{
tracing: {
exporter: [{
type: "Zipkin",
options: {
safetyTags: true,
baseURL: "http://127.0.0.1:9411"
}
}]
}
}
為避免影響所有的 Action ,你可以只在 Action 啟用 safetyTags
,以避免其它 Action 受到影響。
broker.createService({
name: "greeter",
actions: {
hello: {
tracing: {
safetyTags: true
},
handler(ctx) {
return `Hello!`;
}
}
}
});
[1] Tracing, https://moleculer.services/docs/0.14/tracing.html
[2] 淺談OpenTelemetry Specification - Trace, https://ithelp.ithome.com.tw/articles/10288979
[3] dd-trace, https://github.com/DataDog/dd-trace-js
[4] Sampling rules, https://docs.datadoghq.com/tracing/faq/trace_sampling_and_storage/?tab=java#sampling-rules
[5] Tracer settings, https://datadoghq.dev/dd-trace-js/#tracer-settings
[6] Client Sampling Configuration, https://www.jaegertracing.io/docs/1.14/sampling/#client-sampling-configuration
[7] Zipkin, https://zipkin.io/
[8] new relic, https://newrelic.com/
[9] Moleculer Console Exporter, https://github.com/moleculerjs/moleculer/blob/master/src/tracing/exporters/console.js
[10] moleculer-channels, https://github.com/moleculerjs/moleculer-channels
[11] flattenTags method, https://github.com/moleculerjs/moleculer/blob/c48d5a05a4f4a1656075faaabc64085ccccf7ef9/src/tracing/exporters/base.js#L87-L101