Theme Preview

Hue:

You are using an outdated browser that does not support OKLCH colors. The color setting will not take effect.

Den's Opentelemetry 開源貢獻分享

5.2k words

pr

前言

這篇想跟大家分享自己人生中第一個開源貢獻專案、應該會是 2025 最大的里程碑:@waitingliou/tsc-otel-instrumentation

這是一個面向開發者(特別 for SRE)的 npm 開源套件,他讓你能夠以無侵入式、Zero runtime overhead 的方式,幫助你 tracing 你的 TypeScript 專案中的核心商業邏輯,輕易的提升你的系統 Observability,給予開發維運者更多的 Insights,讓找出系統 Bugs, Bottleneck 的效率大大提升。

目前 @waitingliou/tsc-otel-instrumentation 已經成功被收錄在 Opentelemetry Community Ecosystem 之中!PR 還被社群裡首要貢獻者 Chalin 給了有趣的回饋,開心到原地起飛💥
pr

現在也已經可以在官方 Registry 搜尋的到了:
pr

在介紹更多過程之前,先致謝跟廣告 Pathors,Pathors 是間賊有潛力的 AI agent 新創,最近已經在積極擴展台灣的市場,歡迎有興趣的公司找 Y.J, Brandon 洽談!

我覺得我很幸運能有機會為一個客戶已經跨到美國的產品導入 OpenTelemetry,其中成就感頂峰就是在 demo 這個 package 給 Pathors 公司裡傳奇大神學長姊時他們的反應以及他們給出的肯定,也因為這樣有了些自信,想了想或許可以弄完整些開源出去幫助需要的團隊做到更優雅的 tracing 🚀

Introduction

那 @waitingliou/tsc-otel-instrumentation 的特色是什麼?對你有用處嗎?
arc

如果你的團隊或公司在導入 OpenTelemetry 的過程中,不只是想要 System level Instrumentation(ex: HTTP, DB, network, other third library…etc),
也想要 Instrument 自己內部 TypeScript project 中的某些核心商業邏輯,
比如: Clean Architecture 用於處理核心商業邏輯的 usecase layer 中的 **/*Service.ts, 又或是 Infra layer 中的 **/*Repository.ts 特定檔案

而同時,一方面你不想要你的 codebase 到處都是 tracing code like 下方示意程式碼,一方面也不希望 tracing 造成額外的 runtime overhead。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
async signup(name, email) {
return await tracer.startActiveSpan("Demo.UserService.signup", {
kind: SpanKind.INTERNAL,
attributes: {
"code.function": "signup",
"code.namespace": "UserService",
"otel.library.name": "@waitingliou/tsc-otel-instrumentation",
"otel.library.version": "1.0.0"
}
}, async (span) => {
try {
// ...your business logic
span.setStatus({ code: SpanStatusCode.OK });
return ...;
}
catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
}
finally {
span.end();
}
});
}

@waitingliou/tsc-otel-instrumentation 這個 npm package 就能夠徹底解決你的需求。他的特色在於:

  1. Compile-time instrumentation:你的系統在 instrumentation 的行為上不會有任何的 runtime overhead
  2. Application-level transparency:開發者在 codebase 中完全不會意識、觸摸到 tracing code

Google 在 2010年發布的 papper: Dapper, a Large-Scale Distributed Systems Tracing Infrastructure 之中也有提到在導入任何 Tracing 進到系統時的原則 Best Practice 應該要為:

  • the tracing system should have negligible performance impact on running services.
  • programmers should not need to be aware of the tracing system

而現有的 Instrumentation Library 大多都是針對第三方 library 進行 Instrument,尚未提供一個可以針對特定檔案, 特定 function 做自動化 Instrument 的工具,加上這些 Instrumentation Library 都是透過 runtime 時去介入、Override 第三方 library 的載入再加上 tracing code 的方法(也就是所謂的 Monkey-patching)。

看到這你應該會有個疑問:這個 package 是在 Compile time 做 Instrumentation,那對 Compile time 的 overhead 影響程度多大?

我將 Pathors 內部核心 package 作為實驗目標(其 .ts 總行數是 9,971),經過多次量測取平均得出 Memory 僅增加不到 100MB、Compile time 增加 0.4s。

bm
不過這個變因有點多,僅供參考。

Usage Example

另外,我建立了一個完整的範例專案幫助你能更清楚了解該如何導入 OTel 以及 @waitingliou/tsc-otel-instrumentation,這個範例專案將 Grafana Cloud Loki, Tempo 作為 tracing backend(如開頭的影片),並分別提供 Node.js + Express.js、Bun + Hono.js 兩者常見 Web application 在導入 OTel 的實務場景。

exm

Idea Process

這個 Package 前前後後約一個月誕生,idea 是出自於我在 Pathors 實習的過程,隨著 Pathors 系統越做越大,除錯的過程變得越加複雜,所需要的時間成本越拉越長,某服務變慢也很難找出其中的 bottleneck。原先只靠 Loki log 來除錯變得逐漸雞肋,於是我開始專注在如何幫助 Pathors 提升 System Observability。

一開始的需求其實很簡單:Execution context propagation

  • 想要將 application level 的資訊(ex: request id…etc)帶入到內層如 infra layer 方便進行錯誤 Debug,但同時又不希望從上層一直propagate context 導致 infra 產生耦合

下面這一段是分享為了解決這個需求,一路上的過程,從 AsyncLocalStorage 到 Opentelemetry 最後到 @waitingliou/tsc-otel-instrumentation

AsyncLocalStorage

第一個階段是決定導入 AsyncLocalStorage,但這解法挺糟的,除了要考量到未來是否會有潛在 memory leak 風險外,因為這時的 Pathors 主要應用是用 Next.js 建置的,而 Next.js middleware 運行在不同於 Node.js 的 Edge runtime,導致需要額外寫 Wrapper 將 AsyncLocalStorage 的邏輯包住每個 API route, Server action 而這就導致 duplicated code 賊醜。

下面是當初小研究 AsyncLocalStorage 的紀錄供參:
https://hip-people-3a4.notion.site/AsyncLocalStorage-24ff9b862c4680109379d2268f946cf3

還有一個滿有趣的點是,在實作好 AsyncLocalStorage 後我用 K6 以 constant-vus 模式 20 vus 打下去,雖然 throughput, latency 有大概 2.6% 的 penalty,但其實 memory 根本沒什麼差。

但光是要用 Wrapper 在 codebase 到處包就是個糟糕的解法了,而後來發現 Opentelemetry 其實就能夠完美標準化的解決 execution context propagation 的需求、也可以為後續打好強力的監控基礎。

然而,在引入 Opentelemetry 的過程中發現想要針對專案內部的某些檔案或模組進行 Instrument 時,只能手動將生成 span 的程式碼去包住目標才行,而這就又回到了 AsyncLocalStorage 的最大癥結點 Wrapper -> duplicated code。

於是就出現了下一個解法:

用 Proxy 截斷 function call 在其中 Instrument

他的概念是讓每一個被 Proxy 包住的物件,其任何 function call 都會額外經過一層,這一層做的事情就是 Instrument。這樣做的好處是,往往一個漂亮的專案都會利用 Dependency Injection 管理各種物件初始化,那 Proxy 就只需要在某個固定檔案的某個固定地方加上即可,開發者在開發的過程大部分時間不會碰觸到 tracing code,也算是某種 application-level transparency(但透明度可能算60% xd),不過,缺點也很明顯:runtime overhead

當時,我在向 Pathors 大 Boss(以下簡稱 Y.J大神學長) 提出這個解法後,Y.J 大神學長的回饋是這個做法雖然說不完美但堪用,不過同時,Y.J 大神學長也提出了一個好奇點,而這個好奇點造就了 @waitingliou/tsc-otel-instrumentation 的誕生!(偷偷再次感謝Y.J 大神學長)
:有沒有可能可以像其他 Instrumentation Library 一樣完全不需要有額外的 tracing code 在 codebase 中就能做到 Instrumentation?

@waitingliou/tsc-otel-instrumentation 的誕生

為了達到「完全不需要有額外的 tracing code 在 codebase 中就能做到 Instrumentation」,於是有了:誒 TS 誒,那就從 Compile 階段下手吧的想法。

平常對於 TS Compiler 的知識只停留在它會幫忙把 TS 轉成 JS,那他怎麼轉的?
其實 TS Compiler 會將我們的 TS file 轉換成一顆 AST tree,每一個 TS file 都是由標準化的結構組成的 AST tree(可以參考附圖),你也可以自己到 TypeScript AST Viewer 玩玩看。

ast

看到這裡你應該多少知道 @waitingliou/tsc-otel-instrumentation 做了啥吧!其實邏輯很簡單,就是在編譯的過程針對那些你想要的 TS file 做 traversal,在 traversal 的過程對目標 function call 加上生成 span 的邏輯也就是 instrumentation。

透過這樣就能夠做到真正的 100% application-level transparency,開發人員在開發時完全不會意識到任何 tracing code,因為是在編譯時自動加上在編譯後的 js file!

Conclusion

希望這篇的分享有幫助到也想要嘗試開源貢獻的人,感謝您的閱讀!也歡迎私訊我或留言與我進行交流討論。

如果您覺得這個 package 對您有所幫助,請幫幫在 GitHub 上按下星星,也很歡迎 pay me a coffee 資助我這個有留學夢的窮學生。

Happy tracing 🚀🚀

ref