來玩 OTel Go Metric Exemplar

Nathan.LuNathan.Lu
6 min read

在今年寫書時,第 6.4 小節關於 Metric Exemplar 的演示,當時我使用的是 Prometheus Exempar 來演示,因為當時這項功能還處於提案階段,但最近幾次的 Release(v1.31.0v1.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 的用途

  1. 快速定位問題

    • 當某個 metric(例如高延遲或高錯誤率)異常時,可以快速通過 Exemplar 跳轉到分散式追蹤查看具體原因。畢竟 Alert 與值班人員看的都是 metric,不會一直盯著 Trace 與 Log 在那邊看。大家看股票也是看最新價格跟掛單簿,不會細到在那邊一直看每一筆交易的細節。
  2. 補充上下文資訊

    • 讓 metric 數據更有意義,與系統行為建立更緊密的關聯。因為 metric 其實是個非常粗顆粒度的遙測資料,因為一筆 metric data 就只能說明一個事情表達的數值,如果要說明足夠廣泛的事情,那就要多種類的 metric,且不是任何事情都能轉成數值。但是 metric 很直觀便於決策,但卻難以做細部的展開與分析。
  3. 高效率問題分析

    • 適合於高併發場景下的性能診斷,例如 HTTP 請求的異常分析。

假設:

  • 您有一個 HTTP 服務,記錄了延遲(Latency)和請求數量(Request Count)。

  • 在某個桶(如延遲為 100ms 至 200ms)中,系統發現了異常增長。

解決:

  • 通過 Exemplar,您可以快速定位該延遲範圍內的具體請求。

  • 通過 Exemplar 提供的 TraceIDSpanID,查看該請求的完整分布式追蹤,找出導致延遲的根本原因(例如下游服務超時)。

Show Examplar on Grafana

來到 Grafana 選到 metric 尾字是bucket 的,然後中間的 Exemplars 功能要打開,就能看見一堆小點點在儀表板上,那就是 Exemplar。

接著,隨便選一個,我們就能看見上面的資訊,其中最特別的就是 TraceId與SpanId了。

要能夠從 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" 的指標,但它們的描述和數據類型不同。

  1. dice.rolls - Counter

     {
         "Name": "dice.rolls",
         "Description": "The number of rolls by roll value",
         "Unit": "{roll}",
         "Data": { ... }
     }
    
    • Namedice.rolls,指標名稱。

    • DescriptionThe number of rolls by roll value,描述該 metric 表示每個擲骰子結果的次數。

    • Unit{roll},單位為「擲骰子次數」。

    • Data:包含具體的DataPoints

Data 詳細解析
  • DataPointsDataPoint 的列表,每個 DataPoint 代表一個擲骰子結果的統計。

  • TemporalityCumulativeTemporality,表示數據是累積的。書裡還有講到 Delta

  • IsMonotonictrue,表示計數器的值只會遞增,不會減少。

DataPoints 內容

每個DataPoints包含以下信息:

  1. Attributes:屬性,包含 roll.value,即擲骰子的結果(點數)。

  2. StartTime:該DataPoint開始收集的時間。

  3. Time:該DataPoint的記錄時間。

  4. Value:計數器的累積值,表示該點數的擲骰子次數。

  5. Exemplars:示例數據,包含了與該DataPoint相關的詳細上下文。

Exemplars
  • FilteredAttributes:過濾的屬性,這裡為 null

  • Time:示例的時間戳。

  • Value:示例的值,與DataPointValue 相同。

  • SpanIDTraceID:與該數據點相關聯的追蹤信息,可用於鏈接到具體的 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 的結果。
  1. dice.rolls - Histogram

     {
         "Name": "dice.rolls",
         "Description": "The histogram of rolls by roll value",
         "Unit": "{roll}",
         "Data": { ... }
     }
    
  • Name:同樣為 dice.rolls

  • DescriptionThe histogram of rolls by roll value,表示擲骰子結果的直方圖分布。

  • Unit{roll},單位為「擲骰子點數」。

  • Data:包含直方圖數據。

Data 詳細解析
  • DataPoints:直方圖數據點的列表。

  • TemporalityCumulativeTemporality,累積的數據。

DataPoints 內容

每個數據點包含:

  1. Attributes:與上述相同,包含 roll.value

  2. StartTimeTime:數據點的時間範圍。

  3. Count:樣本總數,表示該點數出現的次數。

  4. Bounds:直方圖的桶邊界,定義了直方圖的分布範圍。

  5. BucketCounts:每個桶中的計數,表示落在該範圍內的樣本數量。

  6. MinMax:該數據點的最小值和最大值。

  7. Sum:所有樣本值的總和。

  8. 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:包含了與該數據點相關的追蹤信息。


  • 這些資料反映了在某個時間範圍內(從 StartTimeTime),擲骰子遊戲的結果統計。

  • 計數器指標(第一個 dice.rolls):

    • 記錄了每個點數出現的累積次數。

    • 每個點數(1、3、4)都有一個數據點,表示該點數出現的總次數為 1。

  • 直方圖指標(第二個 dice.rolls):

    • 記錄了擲骰子結果的分布情況。

    • 將結果按照定義的桶(Bounds)進行分類,計算每個桶內的樣本數(BucketCounts)。

    • 由於骰子點數為 1-6,樣本都落在第二個桶(0-5)內。

  • Exemplars

    • 為每個數據點提供了具體的示例,包含了時間、值、以及相關的 SpanIDTraceID

    • 可以用於追蹤該DataPoint在分散是系統中的上下文,方便進一步的性能分析和問題排查。

  • 性能監控

    • 通過監控 dice.rolls 指標,可以了解不同點數出現的頻率,檢查是否存在偏差(例如,某個點數出現的次數異常高或低)。
  • 分布分析

    • 使用直方圖可以分析擲骰子結果的分布情況,確認結果是否符合預期的均勻分布。
  • 問題排查

    • 利用 Exemplar 中的 TraceIDSpanID,可以追蹤到具體的操作或請求,幫助定位可能的問題。

Event Model

以上這些都還是在應用程式中,你們覺得這些資料屬於下圖的哪個呢?

Events → Data Stream → Timeseries Diagram

答案那一大坨 Data Points 都是屬於 In Transit︰OTLP Stream Model。而骰骰子,API的請求阿等等的,那些 Data Points 發生被捕或的地方,才是屬於 Event Model

Event Model 的特徵

  1. 原始觀測事件:

    • 事件模型的基礎是記錄實時或按需的數據點(如單次 HTTP 請求大小)。

    • 記錄的是原始數據,尚未聚合或壓縮。

  2. 數據轉換的重要性:

    • 將原始事件數據轉換成適合後端存儲和分析的指標流(Metric Streams)。

    • 避免直接報告大量事件數據至後端,減少網路和處理資源的壓力。

  3. 多重輸出能力:

    • 同一事件數據可映射成多種指標格式,靈活滿足不同的監控需求。

    • 例如,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 支持,你就能在圖表中看到帶有 TraceIDSpanID 的小點點,這些點點就是 Exemplar。通過點擊它們,你可以直接進入 Trace 的詳細視圖。

這樣一來,從高延遲指標到具體的請求上下文,你只需點擊幾下即可完成,這種無縫跳轉的體驗,極大提升了性能分析和問題診斷的效率。

總結來說,Exemplar 將 OpenTelemetry 的指標數據從單純的趨勢觀察,拓展到問題追蹤的深度分析。透過它,工程師能在指標與事件間無縫切換,顯著提升系統可觀測性與效能優化能力。

有興趣能購買OpenTelemetry 入門指南:建立全面可觀測性架構(iThome鐵人賽系列書) 電子書來閱讀。

今天的範例程式碼都在[OpenTelemetry 入門指南︰建立全面可觀測性架構 Ch 6當中](https://github.com/tedmax100/OpenTelemetryEntryBeook/tree/main/ch6/exemplar)。

0
Subscribe to my newsletter

Read articles from Nathan.Lu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Nathan.Lu
Nathan.Lu