---
# System prepended metadata

title: 在 .NET 中使用 OpenTelemetry ，為應用程式埋下觀測點
tags: [Grafana Tempo, Observability, OpenTelemetry, Grafana, APM]

---

# 在 ASP .NET Core 中使用 OpenTelemetry，為應用程式埋下觀測點
這篇文章主要會說明為什麼選擇使用 OpenTelemetry 作為處理追蹤資料的套件，還有如何利用 .NET `System.Diagnostics` 函式庫來產生追蹤資料，並交由 OpenTelemetry 來接手處理後續流程的方式。

## 為什麼選擇 OpenTelemetry
OpenTelemetry 是一個可以採集 trace, metric, log 三樣遙測資料的整合工具，支援多種程式語言，同時又是 CNCF 裡的項目。以往想要導入 APM 的工具時，基本上就只能使用那家工具所開發的套件，日後想要換工具，就會遇到服務內部的程式碼都要改用相對應套件的情況，亦增加了更換工具的成本。現在的話，只要裝 OpenTelemetry 這一套，就可以隨意更換後面的 APM 工具，不會因為更換套件的成本很高而被特定的工具綁住。OpenTelemetry Protocol 目前已經有很多 APM 工具支援它的格式，例如 Grafana、Elastic APM。

當然，OpenTelemetry 也不是說有其他 APM 工具支援就完美無缺的，要看支援程度高不高，有機率出現搭配自家套件才能讓整套工具更好用、有一好沒兩好的情況，這點就要自行評估。Grafana Tempo 對於 OpenTelemetry Protocol 格式的支援度很高，欄位的對應也一致，但是對於用追蹤的資料來製作統計資料，這點就難度很高，甚至沒辦法製作，當然跟我對 Grafana 製作圖表很不熟也有關係。後續也有嘗試使用 Elastic APM 搭配 OpenTelemetry（送到 OTel Collector 再轉到 Elastic Agent），也試過搭配 Elastic 自家的套件 Elastic.Apm（直送 Elastic Agent），結果兩者的資料在 Elastic APM 上發揮的效果，還是自家套件較能完整發揮。
    
## 運作流程
![運作流程](https://hackmd.io/_uploads/ryQ5cJ_K3.png)

整個運作流程也很簡單，.NET 的應用程式裡使用 `System.Diagnostics` 函式庫產生追蹤資料（手動埋點），然後交由 OpenTelemetry 套件接手處理自動側錄、輸出的目的地等設定。處理完成後，再透過 OpenTelemetry Protocol 來傳到採集器（OpenTelemetry Collector 或是其他採集器），之後採集器再送到 APM 工具內，交由我們查看。

## OpenTelemetry 套件
這裡提供一些我常用的套件，GitHub 裡也有很多[官方](https://github.com/open-telemetry/opentelemetry-dotnet)和[協作者（contribute）](https://github.com/open-telemetry/opentelemetry-dotnet-contrib)所建立的套件，可依需求選用（裡面地雷還不少，有的會互相衝突）。各套件的使用說明都在 GitHub 裡，沒有額外的網站說明。另外，有一些套件目前還在預覽版，記得要先允許使用預覽版的套件，這樣才能看到。

> 套件版本可能較舊，若使用新版本，可能用法會不同，建議搭配官方文件。

```
OpenTelemetry 1.4.0
OpenTelemetry.Exporter.Console 1.4.0
OpenTelemetry.Exporter.OpenTelemetryProtocol 1.4.0
OpenTelemetry.Extensions.Hosting 1.4.0
OpenTelemetry.Instrumentation.AspNetCore 1.0.0-rc9.14
OpenTelemetry.Instrumentation.Http 1.0.0-rc9.14
OpenTelemetry.Instrumentation.SqlClient 1.0.0-rc9.14
```

接下來用函式庫的類型來分類，簡單說明一下各套件的用途：
### 核心
沒了它，接下來的內容可以不用看了。

| 套件 | 說明 |
|-----|-----|
| [OpenTelemetry](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry) | OpenTelemetry 的核心套件 |
| [OpenTelemetry.Extensions.Hosting](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Extensions.Hosting) | 提供 tracing 和 metrics 自動開始和停止側錄的擴充方法，簡化 OpenTelemetry SDK 的生命週期 |

### Exporter
將資料輸出到指定位置，有很多輸出的位置，也可以同時使用多個，例如：Console, InMemory, Jaeger, OpenTelemetry Protocol 等。

| 套件 | 說明 |
|-----|-----|
| [OpenTelemetry.Exporter.Console](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Exporter.Console) | 將 `System.Diagnostics.Activity` 產生的資料輸出到 Console，方便在開發的時候「人工」觀測和解析 |
| [OpenTelemetry.Exporter.OpenTelemetryProtocol](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Exporter.OpenTelemetryProtocol) | 將 `System.Diagnostics.Activity` 產生的資料轉成 OpenTelemetry Protocol 格式並輸出到指定位置，通常是送到 collector 或是 agent。提供 gRPC 和 HTTP 的方式傳送 |

### Instrumentation
自動側錄的工具，不同的工具會自動側錄它注重的資料，例如 HTTP 就會注重 HTTP 動詞、status code、url、query string 等，這個就交由各位自行在 Console 或是追蹤資料中觀察。需要注意的是，這些套件和 [OpenTelemetry .NET Automatic Instrumentation](https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation) 不同，這個需要安裝在主機上，然後就能自動側錄主機上的所有服務，基本上不需要修改服務的程式碼，有興趣的可以自行研究（這東東也是個坑）。

| 套件 | 說明 |
|-----|-----|
| [OpenTelemetry.Instrumentation.AspNetCore](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.AspNetCore) | 自動側錄 ASP .NET Core 專案的資料，需要注意，它只會側錄這個專案，其他的一概不理 |
| [OpenTelemetry.Instrumentation.Http](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.Http) | 自動側錄 HttpClient 這個物件的資料 |
| [OpenTelemetry.Instrumentation.SqlClient](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/src/OpenTelemetry.Instrumentation.SqlClient) | 自動側錄 SqlClient 這個物件的資料，同時支援 `Microsoft.Data.SqlClient` 和 `System.Data.SqlClient`，可以側錄 SQL 語句 |

## 實作
OpenTelemetry 會搭配 .NET 提供的 `System.Diagnostics` 函式庫來取得追蹤資料，之後 OpenTelemetry 就會依照設定開始介入處理。在實作的時候可以選擇埋點或不埋點，不埋點只要在`Program.cs`中調整好配置就能直接使用，可以直接看`設定 OpenTelemetry`的章節。想要埋點，看比較詳細的流程，可以看完`埋點`。關於 .NET OpenTelemetry 的詳細使用說明就不贅述了，可以參考下面的文件：
- [.NET | OpenTelemetry](https://opentelemetry.io/docs/instrumentation/net/)
- [新增分散式追蹤檢測 - .NET | Microsoft Learn](https://learn.microsoft.com/zh-tw/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs)
- [System.Diagnostics 命名空間 | Microsoft Learn](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics)
- [[OpenTelemetry] 現代化監控使用 OpenTelemetry 實現 : 在 .NET 如何使用 OpenTelemetry ~ m@rcus 學習筆記](https://marcus116.blogspot.com/2022/01/opentelemetry-in-asp-net.html.html)

### 埋點
一開始只要埋得廣就可以了，讓各個有相依到的服務都能串聯起來，之後有必要時再來針對性地埋點，而且埋得愈多資料量愈大，後續可能就會衍生出空間和查詢效能的問題。

提供名詞的對照，這樣比較好理解在 .NET 裡的物件對應到什麼：
| OpenTelemetry |      .NET      |
|---------------|----------------|
|    Tracer     | ActivitySource |
|     Span      |    Activity    |

注意事項：
- ActivitySource：建議只建立一次，儲存在靜態變數中，同一個元件中都使用同一個 ActivitySource。如果想要獨立控制的話，請再建立一個新的 ActivitySource。ActivitySource 是一個用來建立和啟動 Activity 的物件。
- Activity：在開始和停止的範圍內，操作要記錄的內容。

下面提供兩種記錄的方式，任君選擇。

#### 方法一：手動為每個方法埋點
比較繁雜，但是操作空間大，能夠輕鬆微調自定義的標籤。

1. 建立 ActivitySource

Instrumentation.cs
```csharp
namespace OtelSample.Service;

public static class Instrumentation()
{
    // 通常一個元件建立一個專用的 ActivitySource。
    public static readonly ActivitySource ServiceActivitySource = new ActivitySource("OtelSample.Service");
}
```

2. Activity 開始記錄

TestService.cs
```csharp
namespace OtelSample.Service;

public class TestService()
{
    public void GetData()
    {
        // 記錄 using 範圍內的資料。進入 using 就開始記錄，離開就停止。
        using (var activity = Instrumentation.ServiceActivitySource.Start("OtelSample.Service.TestService.GetData"))
        {
            // do something
        }
    }
}
```

#### 方法二：使用 AOP 套件
這種方式在類別或是方法上掛上自己寫好的 attribute 後，就能自動為目標方法在執行前或執行後做一些事，概念和 MVC 中的 Filter 一樣。AOP 的概念網路上的文章很多，搜尋一下就有。有興趣想看怎麼實作一個 AOP 框架和原理的話，可以看看蔣金楠的[全新升级的AOP框架Dora.Interception[6]: 框架设计和实现原理](https://www.cnblogs.com/artech/p/dora-aop-6x.html)。

.NET 有很多 AOP 套件可以選用，例如：
- Castle DynamicProxy（[官方文件](http://www.castleproject.org/projects/dynamicproxy/)，[GitHub](https://github.com/castleproject/Core)）
- Aspect Injector（無官方文件，[GitHub](https://github.com/pamidur/aspect-injector)）
- Dora.Interception（[官方文件](https://www.cnblogs.com/artech/p/dora-aop.html)，[GitHub](https://github.com/jiangjinnan/dora)）

範例中將使用 Aspect Injector，詳細用法可以看 GitHub，或者看 [[料理佳餚] C# 一個 Open Source 的 Compile-time AOP 框架 - AspectInjector - 軟體廚房](https://dotblogs.com.tw/supershowwei/2020/07/20/101750)，就不在這介紹用法。同時，也會將這個 attribute 設為共用元件，讓多個元件都能使用，所以會使用反射的方式來取得元件、類別、方法等名稱。

1. 安裝 Aspect Injector 套件
```
AspectInjector 2.8.1
```

2. 建立 ActivitySource

將這個類別作為取得各元件 ActivitySource 的集中處。先用元件名稱查詢有無這個名稱的 AcitvitySource，有就繼續沿用，沒有則新增一個並存下來。

Instrumentation.cs
```csharp
namespace OtelSample.Common;

public static class Instrumentation
{
    private static readonly List<ActivitySource> ActivitySources = new List<ActivitySource>();

    public static Activity StartActivity(string componentName, string activityName)
    {
        var activitySource = GetActivitySource(componentName);

        var activity = activitySource.StartActivity(activityName);
        
        return activity;
    }

    private static ActivitySource GetActivitySource(string activityName)
    {
        var activitySource = ActivitySources.FirstOrDefault(q => q.Name.Contains(activityName));

        if (activitySource is null)
        {
            activitySource = new ActivitySource(activityName);
            ActivitySources.Add(activitySource);
        }

        return activitySource;
    }
}
```

3. 定義攔截器和 Advice

我們只需要記錄 public 的方法，並使用 Around 類型來包覆整個方法。

TracingAspect.cs
```csharp
namespace OtelSample.Common;

[Aspect(Scope.PerInstance)]
public class TracingAspect
{
    [Advice(Kind.Around, Targets = Target.Method | Target.Public)]
    public object Around(
        [Argument(Source.Type)] Type type, 
        [Argument(Source.Name)] string name, 
        [Argument(Source.Arguments)] object[] arguments, 
        [Argument(Source.Target)] Func<object[], object> target)
    {
        var componentName = Assembly.GetCallingAssembly().GetName().Name;
        var className = type.Name;
        
        using var activity = Instrumentation.StartActivity(componentName, $"{componentName}.{className}.{name}");
        
        return target(arguments);
    }
}
```

4. 建立 attribute

TracingAttribute.cs
```csharp
namespace OtelSample.Common;

[Injection(typeof(TracingAspect))]
public class TracingAttribute : Attribute
{
    
}
```

5. 掛載 attribute

TestService.cs
```csharp
namespace OtelSample.Service;

[Tracing]
public class TestService()
{
    public void GetData()
    {
        // do something
    }
}
```

### 設定 OpenTelemetry
程式都埋好點之後（或不埋點），接下來就可以交由 OpenTelemetry 來處理了。

Program.cs
```csharp
// 反射取得服務相關的類別庫名稱
var serviceName = Assembly.GetEntryAssembly()?.GetName().Name;
var serviceVersion = Assembly.GetEntryAssembly()?.GetName().Version?.ToString();

var componentPrefix = "OtelSample";

var assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(q => q.GetName().Name.StartsWith(componentPrefix));
var sources = assemblies.Select(q => q.GetName().Name);

// tracing
builder.Services.AddOpenTelemetry()
    .WithTracing(tracerProviderBuilder =>
        tracerProviderBuilder
            .SetResourceBuilder(
                ResourceBuilder.CreateDefault()
                    .AddService(serviceName, serviceVersion: serviceVersion))
            .AddSource(sources.ToArray())
            .AddAspNetCoreInstrumentation(options =>
            {
                options.RecordException = true;
            })
            .AddHttpClientInstrumentation(options =>
            {
                options.RecordException = true;
            })
            .AddSqlClientInstrumentation(options =>
            {
                options.RecordException = true;
                options.SetDbStatementForText = true; // 記錄 SQL 語法，EF 或 Dapper 都可以被記錄
            })
            .AddOtlpExporter(cfg =>
            {
                cfg.Endpoint = new Uri("http://localhost:4317");
                cfg.Protocol = OtlpExportProtocol.Grpc;
            })
            .AddConsoleExporter());
```

## 最後
呼叫幾次 API，再看看你的 APM 工具，享受精美的圖表。
![Tracing 甘特圖](https://hackmd.io/_uploads/rkDsSCvFn.png)

實作時遇到什麼問題，或覺得範例寫得不完整，可以去看我 GitHub 的 Repository：[zamhsu/Otel-WebApiSample](https://github.com/zamhsu/Otel-WebApiSample)

## 參考
- [.NET | OpenTelemetry](https://opentelemetry.io/docs/instrumentation/net/)
- [新增分散式追蹤檢測 - .NET | Microsoft Learn](https://learn.microsoft.com/zh-tw/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs)
- [System.Diagnostics 命名空間 | Microsoft Learn](https://learn.microsoft.com/zh-tw/dotnet/api/system.diagnostics)
- [opentelemetry-dotnet - GitHub](https://github.com/open-telemetry/opentelemetry-dotnet)
- [opentelemetry-dotnet-contrib - GitHub](https://github.com/open-telemetry/opentelemetry-dotnet-contrib)
- [[OpenTelemetry] 現代化監控使用 OpenTelemetry 實現 : 在 .NET 如何使用 OpenTelemetry ~ m@rcus 學習筆記](https://marcus116.blogspot.com/2022/01/opentelemetry-in-asp-net.html.html)
- [aspect-injector - GitHub](https://github.com/pamidur/aspect-injector)

## 延伸閱讀
- [[Free] 電子書 : OpenTelemetry 可觀測性的未來 ~ m@rcus 學習筆記](https://marcus116.blogspot.com/2023/03/free-opentelemetry.html)

###### tags: `Grafana Tempo` `APM` `Grafana` `OpenTelemetry`