# 來玩 OTel Go Metric Exemplar

在今年寫書時，第 6.4 小節關於 Metric Exemplar 的演示，當時我使用的是 Prometheus Exempar 來演示，因為當時這項功能還處於*提案階段*，但最近幾次的 Release（[v1.31.0](https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.31.0) 和 [v1.32.0](https://github.com/open-telemetry/opentelemetry-go/releases/tag/v1.32.0)） ，已經 OTel Go Metric Exemplar 功能已經稍稍完整了，就讓我們來玩玩看。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732036303471/5e630d9c-fd13-441f-b498-045be6c988d3.png align="center")

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732036257866/14486107-1404-47fc-92c4-854dfffb5ef4.png align="center")

**Exemplar** 是用來讓 metric 與 trace 能相互關聯重要資料。之前的版本之所以不夠完整的原因是缺少了 **ExemplarFilter** 機制的完整支援，內建的機制有 AlwaysOn、AlwaysOff 和 TraceBased。以及 **ExamplarReservoir** 機制，以及 **View**。這些書上會解釋介紹，這裡就先不多做介紹了。**TraceBased** 主要是看 Span 的 Sampled Flag而已，這在書上的第6-1小節有介紹。

為什麼一定要有這些機制才算基本完整？因為連 Trace 都有 sampling 機制，Metric Exemplar 沒有的話會很怪，Trace 如果都不被紀錄了，Exemplar 卻被記錄下來也沒作用。

```go
// 建立 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 提供的 `TraceID` 和 `SpanID`，查看該請求的完整分布式追蹤，找出導致延遲的根本原因（例如下游服務超時）。
    

## Show Examplar on Grafana

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732122942365/58ce0cf5-3537-4724-8ba1-70b4a71f9bce.png align="center")

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

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732123520580/239bf687-163b-4cf4-ad70-0632e256c840.png align="center")

### Link from Metric to Trace

要能夠從 metric 的 trace\_id 連結至 trace 資料頁面，我們需要去 Grfana Data Source → Prometheus 設定中的 Exemplars 設定完成能了。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732124133196/598f6659-ccd3-4624-89fc-89bcb6aca664.png align="center")

再回來 Explore 頁面上就會看見 `trace_id 旁邊有個` `Query with TraceDemo` 的按鈕了。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732123942209/9c7514a2-b142-4918-a094-2715e6020730.png align="center")

然後我們就能在一個畫面中，跟具有異常的指標區間，找到想了解的 Exemplar data，然後展開對應的 tracing 來了解此時的系統之間的行為。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732124082907/6bc7d015-150a-495c-adf2-722617087c16.png align="center")

進行探索的過程中，metric 是很好的直觀展示數據的方式。也許大家只在資源用了多少，但其實系統表現與性能更為重要。靠 log 其實有點太細了，雖然能夠快速 filter，但你會發現分析時，你還是會將 log 做成圖表在看，因為我們要找到的是時間連續中的異常表現區間。

## Metric Instrument 種類

在書上的第5-3小節中，有提到幾種計量類型 Counter、UpDownCounter、Gauge 與 Histogram 。我們在演示程式中也是在程式中宣告這些類型在測量這些事件。

```go
	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

```json
{
	"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**
    
    ```json
    {
        "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**包含以下信息：
    
    1. **Attributes**：屬性，包含 `roll.value`，即擲骰子的結果（點數）。
        
    2. **StartTime**：該**DataPoint**開始收集的時間。
        
    3. **Time**：該**DataPoint**的記錄時間。
        
    4. **Value**：計數器的累積值，表示該點數的擲骰子次數。
        
    5. **Exemplars**：示例數據，包含了與該**DataPoint**相關的詳細上下文。
        
    
    ###### **Exemplars**
    
    * **FilteredAttributes**：過濾的屬性，這裡為 `null`。
        
    * **Time**：示例的時間戳。
        
    * **Value**：示例的值，與**DataPoint**的 `Value` 相同。
        
    * **SpanID** 和 **TraceID**：與該數據點相關聯的追蹤信息，可用於鏈接到具體的 Tracing中，便於進一步分析。
        
    
    ##### **範例**
    
    * **擲出點數 1 的數據點**：
        
        ```json
        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 的結果。
            
    
2. dice.rolls - Histogram
    
    ```json
    {
        "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 內容**

每個數據點包含：

1. **Attributes**：與上述相同，包含 `roll.value`。
    
2. **StartTime** 和 **Time**：數據點的時間範圍。
    
3. **Count**：樣本總數，表示該點數出現的次數。
    
4. **Bounds**：直方圖的桶邊界，定義了直方圖的分布範圍。
    
5. **BucketCounts**：每個桶中的計數，表示落在該範圍內的樣本數量。
    
6. **Min** 和 **Max**：該數據點的最小值和最大值。
    
7. **Sum**：所有樣本值的總和。
    
8. **Exemplars**：與上述相同的示例數據。
    

##### **範例**

* **擲出點數 3 的直方圖數據點**：
    
    ```json
    {
        "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`，可以追蹤到具體的操作或請求，幫助定位可能的問題。
        

## Event Model

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

![Events → Data Stream → Timeseries Diagram](https://opentelemetry.io/docs/specs/otel/metrics/img/model-layers.png align="left")

答案那一大坨 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()](https://github.com/open-telemetry/opentelemetry-go/blob/main/exporters/otlp/otlpmetric/otlpmetricgrpc/exporter.go#L73C27-L73C42)。

```go
// 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](https://github.com/open-telemetry/opentelemetry-proto-go/blob/main/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](https://github.com/open-telemetry/opentelemetry-collector/blob/main/pdata/internal/data/protogen/metrics/v1/metrics.pb.go)。看檔名好像跟前面題的不同，不是說都是傳遞OTLP stream model嘛？

別急，讓我們看看它編譯的來源檔案。來源都是這隻[`// source: opentelemetry/proto/metrics/v1/metrics.proto`](https://github.com/open-telemetry/opentelemetry-proto/blob/main/opentelemetry/proto/metrics/v1/metrics.proto)，所以其實都一樣的。

## Time Seriese Model

但這些遙測資料最終還是要落地的。因為 OTel 框架並沒提供一個儲存方案來儲存，所以最後還是要儲存像 Prometheus、ClickHouse這類的服務中。

所以 **TimeSeries Model** 是指 OpenTelemetry 收集的觀測數據，經過轉換與處理後，存儲在後端系統中的模型形式。它通常用於長期分析和查詢。這個模型將指標數據組織成時間序列（Time Series），每條時間序列由**唯一的標籤組合**和**時間戳數據點**構成。

它是指標數據的「靜止狀態」，也是可觀測系統的核心之一，負責儲存並提供支持多維度分析的能力。這種模型為長期查詢與性能優化提供了堅實的數據基礎，也是各家可觀測性平台（如 Prometheus 與 Grafana）的核心設計理念之一。

![](https://cdn.hashnode.com/res/hashnode/image/upload/v1732036628454/4ad31d45-2a54-4f5e-a3fb-e72a6f706dd5.png align="center")

# 總結

當我們談到 **Exemplar** 的應用時，它帶來了 OpenTelemetry 指標世界的一大進步。過去，指標數據只能提供高度彙總的視角，例如「延遲分布」或「錯誤總數」，但它卻無法直接讓你跳進一個具體的問題事件。而 Exemplar 解決了這個核心問題：**它讓你可以從聚合數據直接鏈接到具體的 Trace 和上下文，從而更快定位問題根源。**

在 Grafana 的儀表板中，Exemplar 的價值進一步放大。只要你正確配置了 Prometheus 的 Exemplar 支持，你就能在圖表中看到帶有 `TraceID` 和 `SpanID` 的小點點，這些點點就是 Exemplar。通過點擊它們，你可以直接進入 Trace 的詳細視圖。

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

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

有興趣能購買[`OpenTelemetry 入門指南：建立全面可觀測性架構（iThome鐵人賽系列書）`](https://www.books.com.tw/products/E050230487?srsltid=AfmBOoqGbaY9XPyzh201UxqG6Msl8M_EzcwRVRTKnK72YqR1EnFlC_RJ) 電子書來閱讀。

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