來玩 OTel Go Metric Exemplar
在今年寫書時,第 6.4 小節關於 Metric Exemplar 的演示,當時我使用的是 Prometheus Exempar 來演示,因為當時這項功能還處於提案階段,但最近幾次的 Release(v1.31.0 和 v1.32.0) ,已經 OTel Go Metric Exemplar 功能已經稍稍完整了,就讓我們來玩玩看。
Exemplar 是用來讓 metric 與 trace 能相互關聯重要資料。之前的版本之所以不夠完整的原因是缺少了 ExemplarFilter 機制的完整支援,內建的機制有 AlwaysOn、AlwaysOff 和 TraceBased。以及 ExamplarReservoir 機制,以及 View。這些書上會解釋介紹,這裡就先不多做介紹了。TraceBased 主要是看 Span 的 Sampled Flag而已,這在書上的第6-1小節有介紹。
為什麼一定要有這些機制才算基本完整?因為連 Trace 都有 sampling 機制,Metric Exemplar 沒有的話會很怪,Trace 如果都不被紀錄了,Exemplar 卻被記錄下來也沒作用。
// 建立 meter provider 時,也選定 Exemplar Filter。
meterProvider := metric.NewMeterProvider(
metric.WithExemplarFilter(exemplar.TraceBasedFilter),
metric.WithReader(metric.NewPeriodicReader(metricExporter,
// Default is 1m. Set to 3s for demonstrative purposes.
metric.WithInterval(3*time.Second))),
)
Exemplar 的用途
快速定位問題:
- 當某個 metric(例如高延遲或高錯誤率)異常時,可以快速通過 Exemplar 跳轉到分散式追蹤查看具體原因。畢竟 Alert 與值班人員看的都是 metric,不會一直盯著 Trace 與 Log 在那邊看。大家看股票也是看最新價格跟掛單簿,不會細到在那邊一直看每一筆交易的細節。
補充上下文資訊:
- 讓 metric 數據更有意義,與系統行為建立更緊密的關聯。因為 metric 其實是個非常粗顆粒度的遙測資料,因為一筆 metric data 就只能說明一個事情表達的數值,如果要說明足夠廣泛的事情,那就要多種類的 metric,且不是任何事情都能轉成數值。但是 metric 很直觀便於決策,但卻難以做細部的展開與分析。
高效率問題分析:
- 適合於高併發場景下的性能診斷,例如 HTTP 請求的異常分析。
假設:
您有一個 HTTP 服務,記錄了延遲(Latency)和請求數量(Request Count)。
在某個桶(如延遲為 100ms 至 200ms)中,系統發現了異常增長。
解決:
通過 Exemplar,您可以快速定位該延遲範圍內的具體請求。
通過 Exemplar 提供的
TraceID
和SpanID
,查看該請求的完整分布式追蹤,找出導致延遲的根本原因(例如下游服務超時)。
Show Examplar on Grafana
來到 Grafana 選到 metric 尾字是bucket
的,然後中間的 Exemplars 功能要打開,就能看見一堆小點點在儀表板上,那就是 Exemplar。
接著,隨便選一個,我們就能看見上面的資訊,其中最特別的就是 TraceId與SpanId了。
Link from Metric to Trace
要能夠從 metric 的 trace_id 連結至 trace 資料頁面,我們需要去 Grfana Data Source → Prometheus 設定中的 Exemplars 設定完成能了。
再回來 Explore 頁面上就會看見 trace_id 旁邊有個
Query with TraceDemo
的按鈕了。
然後我們就能在一個畫面中,跟具有異常的指標區間,找到想了解的 Exemplar data,然後展開對應的 tracing 來了解此時的系統之間的行為。
進行探索的過程中,metric 是很好的直觀展示數據的方式。也許大家只在資源用了多少,但其實系統表現與性能更為重要。靠 log 其實有點太細了,雖然能夠快速 filter,但你會發現分析時,你還是會將 log 做成圖表在看,因為我們要找到的是時間連續中的異常表現區間。
Metric Instrument 種類
在書上的第5-3小節中,有提到幾種計量類型 Counter、UpDownCounter、Gauge 與 Histogram 。我們在演示程式中也是在程式中宣告這些類型在測量這些事件。
rollCnt, err = meter.Int64Counter("dice.rolls",
metric.WithDescription("The number of rolls by roll value"),
metric.WithUnit("{roll}"))
rollCntHistogram, err = meter.Int64Histogram("dice.rolls",
metric.WithDescription("The number of rolls by roll value"),
metric.WithUnit("{roll}"),
)
// 增加 Span Counter 並附加 Exemplar
rollCnt.Add(ctx, 1, metric.WithAttributes(rollValueAttr))
// 記錄到 Histogram 並附加 Exemplar
rollCntHistogram.Record(ctx, int64(roll), metric.WithAttributes(rollValueAttr))
Data Point
{
"Resource": [...],
"ScopeMetrics": [
{
"Scope": {
"Name": "go.opentelemetry.io/otel/example/dice",
"Version": "",
"SchemaURL": "",
"Attributes": null
},
"Metrics": [
{
"Name": "dice.rolls",
"Description": "The number of rolls by roll value",
"Unit": "{roll}",
"Data": {
"DataPoints": [
{
"Attributes": [
{
"Key": "roll.value",
"Value": {
"Type": "INT64",
"Value": 1
}
}
],
"StartTime": "2024-11-21T01:52:45.284089623+08:00",
"Time": "2024-11-21T01:52:51.285312908+08:00",
"Value": 1,
"Exemplars": [
{
"FilteredAttributes": null,
"Time": "2024-11-21T01:52:47.929584861+08:00",
"Value": 1,
"SpanID": "/nfde45R6CM=",
"TraceID": "J+uh/Jv7OzD0v6aF6ID+Xw=="
}
]
},
{
"Attributes": [
{
"Key": "roll.value",
"Value": {
"Type": "INT64",
"Value": 4
}
}
],
"StartTime": "2024-11-21T01:52:45.284089623+08:00",
"Time": "2024-11-21T01:52:51.285312908+08:00",
"Value": 1,
"Exemplars": [
{
"FilteredAttributes": null,
"Time": "2024-11-21T01:52:49.185654994+08:00",
"Value": 1,
"SpanID": "ZnMwibK/1Cs=",
"TraceID": "vmjwm8lDTqfTCGF9iKShHA=="
}
]
},
{
"Attributes": [
{
"Key": "roll.value",
"Value": {
"Type": "INT64",
"Value": 3
}
}
],
"StartTime": "2024-11-21T01:52:45.284089623+08:00",
"Time": "2024-11-21T01:52:51.285312908+08:00",
"Value": 1,
"Exemplars": [
{
"FilteredAttributes": null,
"Time": "2024-11-21T01:52:46.471871714+08:00",
"Value": 1,
"SpanID": "MvoQs46zXbY=",
"TraceID": "884mmUbC1WruNPrWeyzMhw=="
}
]
}
],
"Temporality": "CumulativeTemporality",
"IsMonotonic": true
}
},
{
"Name": "dice.rolls",
"Description": "The histogram of rolls by roll value",
"Unit": "{roll}",
"Data": {
"DataPoints": [
{
"Attributes": [
{
"Key": "roll.value",
"Value": {
"Type": "INT64",
"Value": 3
}
}
],
"StartTime": "2024-11-21T01:52:45.284125019+08:00",
"Time": "2024-11-21T01:52:51.285330742+08:00",
"Count": 1,
"Bounds": [
0,
5,
10,
25,
50,
75,
100,
250,
500,
750,
1000,
2500,
5000,
7500,
10000
],
"BucketCounts": [
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"Min": 3,
"Max": 3,
"Sum": 3,
"Exemplars": [
{
"FilteredAttributes": null,
"Time": "2024-11-21T01:52:46.471880671+08:00",
"Value": 3,
"SpanID": "MvoQs46zXbY=",
"TraceID": "884mmUbC1WruNPrWeyzMhw=="
}
]
},
{
"Attributes": [
{
"Key": "roll.value",
"Value": {
"Type": "INT64",
"Value": 1
}
}
],
"StartTime": "2024-11-21T01:52:45.284125019+08:00",
"Time": "2024-11-21T01:52:51.285330742+08:00",
"Count": 1,
"Bounds": [
0,
5,
10,
25,
50,
75,
100,
250,
500,
750,
1000,
2500,
5000,
7500,
10000
],
"BucketCounts": [
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"Min": 1,
"Max": 1,
"Sum": 1,
"Exemplars": [
{
"FilteredAttributes": null,
"Time": "2024-11-21T01:52:47.929600621+08:00",
"Value": 1,
"SpanID": "/nfde45R6CM=",
"TraceID": "J+uh/Jv7OzD0v6aF6ID+Xw=="
}
]
},
{
"Attributes": [
{
"Key": "roll.value",
"Value": {
"Type": "INT64",
"Value": 4
}
}
],
"StartTime": "2024-11-21T01:52:45.284125019+08:00",
"Time": "2024-11-21T01:52:51.285330742+08:00",
"Count": 1,
"Bounds": [
0,
5,
10,
25,
50,
75,
100,
250,
500,
750,
1000,
2500,
5000,
7500,
10000
],
"BucketCounts": [
0,
1,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0
],
"Min": 4,
"Max": 4,
"Sum": 4,
"Exemplars": [
{
"FilteredAttributes": null,
"Time": "2024-11-21T01:52:49.185663229+08:00",
"Value": 4,
"SpanID": "ZnMwibK/1Cs=",
"TraceID": "vmjwm8lDTqfTCGF9iKShHA=="
}
]
}
],
"Temporality": "CumulativeTemporality"
}
}
]
}
]
}
這段資料是一個 OpenTelemetry SDK 輸出的 Metrics 資料,包含了有關擲骰子遊戲的 metric data,特別是每次擲骰子的結果。以下是對該資料的詳細說明:
"Metrics"
是一個包含多個 metric 的數組。在此資料中,有兩個名為 "dice.rolls"
的指標,但它們的描述和數據類型不同。
dice.rolls - Counter
{ "Name": "dice.rolls", "Description": "The number of rolls by roll value", "Unit": "{roll}", "Data": { ... } }
Name:
dice.rolls
,指標名稱。Description:
The number of rolls by roll value
,描述該 metric 表示每個擲骰子結果的次數。Unit:
{roll}
,單位為「擲骰子次數」。Data:包含具體的DataPoints。
Data 詳細解析
DataPoints:DataPoint 的列表,每個 DataPoint 代表一個擲骰子結果的統計。
Temporality:
CumulativeTemporality
,表示數據是累積的。書裡還有講到Delta
。IsMonotonic:
true
,表示計數器的值只會遞增,不會減少。
DataPoints 內容
每個DataPoints包含以下信息:
Attributes:屬性,包含
roll.value
,即擲骰子的結果(點數)。StartTime:該DataPoint開始收集的時間。
Time:該DataPoint的記錄時間。
Value:計數器的累積值,表示該點數的擲骰子次數。
Exemplars:示例數據,包含了與該DataPoint相關的詳細上下文。
Exemplars
FilteredAttributes:過濾的屬性,這裡為
null
。Time:示例的時間戳。
Value:示例的值,與DataPoint的
Value
相同。SpanID 和 TraceID:與該數據點相關聯的追蹤信息,可用於鏈接到具體的 Tracing中,便於進一步分析。
範例
擲出點數 1 的數據點:
jsonCopy code{ "Attributes": [ { "Key": "roll.value", "Value": { "Type": "INT64", "Value": 1 } } ], "Value": 1, "Exemplars": [ { "Time": "2024-11-21T01:52:47.929584861+08:00", "Value": 1, "SpanID": "/nfde45R6CM=", "TraceID": "J+uh/Jv7OzD0v6aF6ID+Xw==" } ] }
- 表示從開始時間到目前,共擲出了 1 次點數為 1 的結果。
dice.rolls - Histogram
{ "Name": "dice.rolls", "Description": "The histogram of rolls by roll value", "Unit": "{roll}", "Data": { ... } }
Name:同樣為
dice.rolls
。Description:
The histogram of rolls by roll value
,表示擲骰子結果的直方圖分布。Unit:
{roll}
,單位為「擲骰子點數」。Data:包含直方圖數據。
Data 詳細解析
DataPoints:直方圖數據點的列表。
Temporality:
CumulativeTemporality
,累積的數據。
DataPoints 內容
每個數據點包含:
Attributes:與上述相同,包含
roll.value
。StartTime 和 Time:數據點的時間範圍。
Count:樣本總數,表示該點數出現的次數。
Bounds:直方圖的桶邊界,定義了直方圖的分布範圍。
BucketCounts:每個桶中的計數,表示落在該範圍內的樣本數量。
Min 和 Max:該數據點的最小值和最大值。
Sum:所有樣本值的總和。
Exemplars:與上述相同的示例數據。
範例
擲出點數 3 的直方圖數據點:
{ "Attributes": [ { "Key": "roll.value", "Value": { "Type": "INT64", "Value": 3 } } ], "Count": 1, "Bounds": [0, 5, 10, 25, ...], "BucketCounts": [0, 1, 0, 0, ...], "Min": 3, "Max": 3, "Sum": 3, "Exemplars": [ { "Time": "2024-11-21T01:52:46.471880671+08:00", "Value": 3, "SpanID": "MvoQs46zXbY=", "TraceID": "884mmUbC1WruNPrWeyzMhw==" } ] }
Count:1,表示總共擲出 1 次點數為 3。
BucketCounts:表示在第二個桶(範圍為 0-5)內有 1 個樣本。
Min/Max/Sum:因為只有一個樣本,所以最小值、最大值和總和都為 3。
Exemplars:包含了與該數據點相關的追蹤信息。
這些資料反映了在某個時間範圍內(從
StartTime
到Time
),擲骰子遊戲的結果統計。計數器指標(第一個
dice.rolls
):記錄了每個點數出現的累積次數。
每個點數(1、3、4)都有一個數據點,表示該點數出現的總次數為 1。
直方圖指標(第二個
dice.rolls
):記錄了擲骰子結果的分布情況。
將結果按照定義的桶(
Bounds
)進行分類,計算每個桶內的樣本數(BucketCounts
)。由於骰子點數為 1-6,樣本都落在第二個桶(0-5)內。
Exemplars:
為每個數據點提供了具體的示例,包含了時間、值、以及相關的
SpanID
和TraceID
。可以用於追蹤該DataPoint在分散是系統中的上下文,方便進一步的性能分析和問題排查。
性能監控:
- 通過監控
dice.rolls
指標,可以了解不同點數出現的頻率,檢查是否存在偏差(例如,某個點數出現的次數異常高或低)。
- 通過監控
分布分析:
- 使用直方圖可以分析擲骰子結果的分布情況,確認結果是否符合預期的均勻分布。
問題排查:
- 利用 Exemplar 中的
TraceID
和SpanID
,可以追蹤到具體的操作或請求,幫助定位可能的問題。
- 利用 Exemplar 中的
Event Model
以上這些都還是在應用程式中,你們覺得這些資料屬於下圖的哪個呢?
答案那一大坨 Data Points 都是屬於 In Transit︰OTLP Stream Model。而骰骰子,API的請求阿等等的,那些 Data Points 發生被捕或的地方,才是屬於 Event Model。
Event Model 的特徵
原始觀測事件:
事件模型的基礎是記錄實時或按需的數據點(如單次 HTTP 請求大小)。
記錄的是原始數據,尚未聚合或壓縮。
數據轉換的重要性:
將原始事件數據轉換成適合後端存儲和分析的指標流(Metric Streams)。
避免直接報告大量事件數據至後端,減少網路和處理資源的壓力。
多重輸出能力:
同一事件數據可映射成多種指標格式,靈活滿足不同的監控需求。
例如,
ValueRecorder
可生成 Gauge(即時量規)、Histogram(直方圖)、Sum(總和)等多種 OTLP Stream Model。
OpenTelemetry 中的事件模型關鍵點
檢測工具的彈性:
- OpenTelemetry 的檢測工具設計允許靈活的 Stream Model 生成,並提供合理的默認映射。
應用場景:
- 適用於需要記錄和壓縮高頻率數據的場景,例如記錄 HTTP 請求、記錄數據分佈等。
高效傳輸:
- 通過壓縮和聚合減少資料量,提升傳輸和儲存效率。不管是應用程式傳輸到 OTel Collector 還是 OTel Collector 之間相互傳輸,傳遞的都是 OTLP stream model。包括你在 OTel Collector 用 debug 輸出在 terminal 上看到的也是。
Metric Instrument 轉成 OTLP stream model 的程式碼在exporter.go的Export()。
// Export transforms and transmits metric data to an OTLP receiver.
//
// This method returns an error if called after Shutdown.
// This method returns an error if the method is canceled by the passed context.
func (e *Exporter) Export(ctx context.Context, rm *metricdata.ResourceMetrics) error {
...
otlpRm, err := transform.ResourceMetrics(rm)
...
}
而 OTLP stream model 的定義則在otlp/metrics/v1/metrics.pb.go。這種換過程中的設計有用到 Go gerneric type 一些技巧來減少程式碼的重複,有興趣之後再寫一篇。
OTLP Metrics Model
上一段提到的是 Go exporter 的檔案定義在哪,那這裡我們接著看看 OTel collector 的檔案定義又在哪?程式碼從 OTLP receiver 爬著爬著來到這隻pdata/internal/data/protogen/metrics/v1/metrics.pb.go。看檔名好像跟前面題的不同,不是說都是傳遞OTLP stream model嘛?
別急,讓我們看看它編譯的來源檔案。來源都是這隻// source: opentelemetry/proto/metrics/v1/metrics.proto
,所以其實都一樣的。
Time Seriese Model
但這些遙測資料最終還是要落地的。因為 OTel 框架並沒提供一個儲存方案來儲存,所以最後還是要儲存像 Prometheus、ClickHouse這類的服務中。
所以 TimeSeries Model 是指 OpenTelemetry 收集的觀測數據,經過轉換與處理後,存儲在後端系統中的模型形式。它通常用於長期分析和查詢。這個模型將指標數據組織成時間序列(Time Series),每條時間序列由唯一的標籤組合和時間戳數據點構成。
它是指標數據的「靜止狀態」,也是可觀測系統的核心之一,負責儲存並提供支持多維度分析的能力。這種模型為長期查詢與性能優化提供了堅實的數據基礎,也是各家可觀測性平台(如 Prometheus 與 Grafana)的核心設計理念之一。
總結
當我們談到 Exemplar 的應用時,它帶來了 OpenTelemetry 指標世界的一大進步。過去,指標數據只能提供高度彙總的視角,例如「延遲分布」或「錯誤總數」,但它卻無法直接讓你跳進一個具體的問題事件。而 Exemplar 解決了這個核心問題:它讓你可以從聚合數據直接鏈接到具體的 Trace 和上下文,從而更快定位問題根源。
在 Grafana 的儀表板中,Exemplar 的價值進一步放大。只要你正確配置了 Prometheus 的 Exemplar 支持,你就能在圖表中看到帶有 TraceID
和 SpanID
的小點點,這些點點就是 Exemplar。通過點擊它們,你可以直接進入 Trace 的詳細視圖。
這樣一來,從高延遲指標到具體的請求上下文,你只需點擊幾下即可完成,這種無縫跳轉的體驗,極大提升了性能分析和問題診斷的效率。
總結來說,Exemplar 將 OpenTelemetry 的指標數據從單純的趨勢觀察,拓展到問題追蹤的深度分析。透過它,工程師能在指標與事件間無縫切換,顯著提升系統可觀測性與效能優化能力。
有興趣能購買OpenTelemetry 入門指南:建立全面可觀測性架構(iThome鐵人賽系列書)
電子書來閱讀。
今天的範例程式碼都在[OpenTelemetry 入門指南︰建立全面可觀測性架構 Ch 6當中](https://github.com/tedmax100/OpenTelemetryEntryBeook/tree/main/ch6/exemplar)。
Subscribe to my newsletter
Read articles from Nathan.Lu directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by