# 在 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 上發揮的效果,還是自家套件較能完整發揮。
## 運作流程

整個運作流程也很簡單,.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 工具,享受精美的圖表。

實作時遇到什麼問題,或覺得範例寫得不完整,可以去看我 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`