# 在 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`