只有累積,沒有奇蹟

2022年1月26日 星期三

[OpenTelemetry] 現代化監控使用 OpenTelemetry 實現 : 在 .NET 如何使用 OpenTelemetry

前言
最近在 suvery 監控相關議題時接觸到 OpenTelemetry 蒐集遙測數據的開源框架,覺得這議題挺有趣的因此整理變成系列文,這篇是研究 OpenTelemetry 的系列文第三篇, 這系列主要會分為四篇分別是 若對於上述內容有問題或是不清楚的地方,歡迎提出來一起討論

OpenTelemetry in .NET
在前面分別提到了甚麼是可觀測性 (Observability) 以及 OpenTelemetry 蒐集遙測數據的規範標準,在 OpenTelemetry 在多種主流的程式語言都有實作,這邊就來分享在 .NET 中要如何使用 OpenTelemetry。在 OTel (OpenTelemetry簡稱)中 .NET Core 及 .NET Framework 都有支援(後者版本需高於 4.6.1),在 Github 位置是 opentelemetry-dotnet,與開發者最直接相關的為下列部分
  • API
  • SDK
  • Instrumentation libraries
  • Exporter libraries
以下分別針對個別項目做簡單介紹

API
Github : OpenTelemetry .NET API
可觀測性 (Observability) 中的三個重要元素 Tracing、Metrics、Logging 在 OTel dotnet 分別皆有實作,在支援的程度上除了 Tracing 是 1.0 之外,其他兩項分別還在 Alpha 與 Beta 階段。在 OTel dotnet 上提供 Tracing API, Logging API, Metrics API, Context and Propagation API 等 API 讓開發者依據各自需求作使用。

SDK
Github : OpenTelemetry .NET SDK
SDK 是 OTel API 的實現,目前 SDK 中實現了 Tracing API、Metrics API 和 Context API 等功能。當開發者使用與安裝配置的 SDK 後,所使用到的 API 方法將會開始生成、蒐集、發送遙測數據內容,並提供開發者自訂義 process pipleline 與導出到特定後端功能。目前 SDK 有與 ILogging 進行整合。

Instrumentation libraries
Github : OpenTelemetry .NET Instrumentation Library
提供常用的 Library 讓開發者在使用 OpenTelemetry 上更方便,像是 ASP.NET、ASP.NET Core、HttpClient、SQL Client 或是 SQL Client 等。你的應用程式代碼中如果有用到 httpclient 就可以引用其 OTel httpclient library,使用後就會自動蒐集生成遙測資料,開發者不用寫很多代碼即可達到其目的。目前在 dotnet 支援的 library 可以參考官方網站 OpenTelemetry registry 說明頁。

Exporter libraries
Github : OpenTelemetry .NET Exporter Library
蒐集遙測數據之後這些數據內容呈現的工具,像是在 Demo code 常使用的 console 或是 memory 中,亦或可以選擇 jaeger、zipkin、prometheus 等常見的工具,如果是在 Cloud 上在三大雲端服務廠商 AWS、Azure、GCP 也開始支援 OpenTelemetry 的輸出資料呈現。舉例來說在 Azure 上就可以透過 Azure Application Insights 以及 Azure Monitor 等服務查看蒐集到的資料呈現。

Package
Metrics
這裡列出在 .NET 中與 OpenTelemetry 會有相關的 namespace,其中 Instrumentation 與 Exporter 前面章節已經有提到過不在重複。在 .NET 6 之後的版本開始添加 OpenTelemetry 的支援。System.Diagnostics.Metrics 是 OpenTelemetry Metrics API 規範在 .NET 的實現,Meter 類別是在使用時須要先建立的物件與對象,在依據場景來使用所需要 Metrics 的指標與方法,在目前 Metrics API 支援以下幾個儀器類型
  • Counter : 計數器,可以使用 Add 方法來計算變化率或是總和
  • ObservableCounter : 與 Counter 類似,用於可觀察的儀器值 例如 CPU 時間(針對不同 Process、Task or 用戶模式)
  • ObservableGauge : 非相加值得可觀察儀器,例如當前室溫
  • Histogram : 繪製直方圖或計算百分位數的工具
或許對於上述描述的說明還有些模糊或是不清楚,在微軟 MSDN 文件有提到使用 Metrics 儀器的使用時機
  • 如果是計算內容是隨著時間增加的值 : 建議使用 Counter 與 ObservableCounter
  • 如果是計算時事物 : 建議使用 Histogram
  • 其他類像是業務指標、Cache 命中率、緩存大小 : 建議使用 ObservableGauge
詳細可以參考 MSDN Best practices when selecting an instrument type 文件說明

Tracing
在 Trace 部分可以透過 .NET 內建類別 Activity 或是使用 OpenTelemetry Tracing API 都可以達到其目的,兩者使用後都會定義到 Span 概念。.NET 5 開始提供 Activity 的類別作為 tracing 追蹤的目的,建立實例後可以透過 StartActivity 啟動 Activity 並定義其追蹤名稱;在 OpenTelemetry API 也可以透過 Tracer 達到一樣的目的,透過 TracerProvider 建立實體後使用 StartActiveSpan 來定義追蹤物件。因此你可以發現在微軟官方的部落格或是 sample code 上都是使用內建的 Activity API 做完範例, 兩者的 API 比較差異可以參考上圖

今晚來點 Demo
以上講很多概念與 OpenTelemetry API 的介紹,接著我們就來針對些好玩的部分做 Demo 讓大家對於 OpenTelemetry 在 .NET 的應用更了解,每個 Demo Code 範例代碼皆已上傳到 Github 上,有興趣的朋友可以將 clone 下來自己執行試玩看看

Demo#1 : Console
source code : OpenTelemetrySample
第一個 Demo 開始透過簡單的 Console 作為起手式,在建立專案後請先引用 OpenTelemerey.NET SDK 及 OpenTelemerey.NET Exporter Console 相關 Package,相關指令如下
dotnet add package OpenTelemetry --version 1.1.0
dotnet add package OpenTelemetry.Exporter.Console --version 1.1.0
Program.cs 中進入點 main 程式碼如下
class Program
{
	private static readonly ActivitySource MyActivitySource = new ActivitySource("Company.Product.Library");

	public static void Main()
	{
		using var tracerProvider = Sdk.CreateTracerProviderBuilder()
			.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("SampleService"))
			.SetSampler(new AlwaysOnSampler())
			.AddSource("Company.Product.Library")
			.AddConsoleExporter()
			.Build();

		using (var activity = MyActivitySource.StartActivity("SayHello"))
		{
			activity?.SetTag("foo", 1);
			activity?.SetTag("bar", "NET Conf 2021, Hej !");
			activity?.SetTag("baz", new int[] { 1, 2, 3 });
		}
	}
}
程式碼說明
  • Line 3 : 建立 ActivitySource 物件,並定義名稱為 Company.Product.Library
  • Line 7 : 透過 SDK 建立 TracerProvider 物件並設定 Trace Service 的名稱為 SampleService
  • Line 9 : 設定 Sampler 採樣器配置設定為 alwaysOn
  • Line 10 : 定義 TraceProvider source 名稱為 Company.Product.Library (需要與 line 3 相同)
  • Line 11 : 設定 Exporter,這裡使用的是在 Console 輸出其結果
  • Line 14~19 : 設定 Actitity 物件並定義其 tag 的 key 與 value, ex : foo 值為 1
執行結果如下
除了上述程式碼的說明之外可以看到的是其中 activity 會自動蒐集其起始的時間 (startTime) 與花費時間 (Duration) 等在 trace 中重要的資訊,tagObject 也可以依據需求定義客製化的 attribute 或是屬性資訊,以上是簡單的範例一部分

Demo#2 : API
source code : OpenTelemetrySample.API
接著的範例是使用 API 的部分,在將範例的內容結果顯示在 Zipkin 工具上,因此在建立專案後請先引用 OpenTelemerey.NET SDK 及 OpenTelemerey.NET Exporter Console 及 OpenTelemetry.Exporter.Zipkin Package,相關指令如下
dotnet add package OpenTelemetry --version 1.1.0
dotnet add package OpenTelemetry.Exporter.Console --version 1.1.0
dotnet add package OpenTelemetry.Exporter.Zipkin --version 1.1.0
在這範例中會將蒐集到的遙測數據資料輸出到 Zipkin 中,Zipkin 是分散式追蹤的工具,可以幫助開發者在工具中以視覺化的方式了解到請求中哪一段緩慢的原因與百分比資訊,相關介紹可以透過 官網說明,快速使用方式可以透過 docker 在本機執行下列語法
docker run -d -p 9411:9411 openzipkin/zipkin
Program.cs 中進入點 main 程式碼如下
class Program
{
	private static readonly ActivitySource MyActivitySource = new ActivitySource("Company.Product.Library", "1.0");
	static void Main(string[] args)
	{
		using var tracerProvider = Sdk.CreateTracerProviderBuilder()
		   .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MyService"))
		   .AddSource("Company.Product.Library")
		   .AddZipkinExporter(zipkinOptions =>
		   {
			   zipkinOptions.Endpoint = new Uri("http://localhost:9411/api/v2/spans");
		   })               
		   .AddConsoleExporter()
		   .Build();

		DoSomeWork();

		Console.WriteLine("Example work done"); 
	}

	static void DoSomeWork()
	{
		using (var a = MyActivitySource.StartActivity("SomeWork"))
		{
			StepOne();
			StepTwo();
		}

		using (var activity = MyActivitySource.StartActivity("StepThree", ActivityKind.Server))
		{
			activity?.SetTag("http.method", "GET");
			if (activity != null && activity.IsAllDataRequested == true)
			{
				activity.SetTag("http.url", "http://www.google.com");
				activity.SetTag("body", "This is a book");
			}
		}
	}

	static void StepOne()
	{
		using (var a = MyActivitySource.StartActivity("StepOne"))
		{
			Task.Delay(500);
		}
	}

	static void StepTwo()
	{
		using (var a = MyActivitySource.StartActivity("StepTwo"))
		{
			Task.Delay(1000);
		}
	}
}  
程式碼說明,其中有些與 Demo#1 相同的就不在重複說明唷
  • Line 7 : 設定 Service 的名稱為 MyService
  • Line 9 : 將遙測數據在 zipkin 上顯示,並定義輸出的 endpoint 為 http://localhost:9411/api/v2/spans
  • Line 23 : 定義 activity 物件名稱叫 Somework,其中執行 StepOne 與 StepTwo 兩個方法
  • Line 29 : 定義新的 activity 物件名稱為 StepThree,類型為 Server,內容模擬向第三方發出請求,因此會有定義 httpMethod、呼叫 endpoint 方法與送出 body 內容。
  • Line 40 : 定義 StepOne activity 物件,內容為 delay 500 毫秒
  • Line 48 : 定義 StepTwo activity 物件,內容為 delay 1 秒
執行結果如下
整理一下幾個重點
  • 順序為 StepOne > StepTwo > SomeWork > StepThree
  • StepOne 的 Id 為 StepTwo、SomeWork、StepThree 的 ParentId,上一篇提到的 Context Propagation 上下文關聯是透過此來定義
  • 每個 activity 會自動取得執行的 instance Id,這在雲端環境有這資訊很方便
  • 每個 Activity 皆有紀錄花費時間與起始時間
接著我們可以透過 zipkin 來看一下上面數據的視覺化呈現為和,開啟瀏覽器後輸入 http://localhost:9411/zipkin/ ,在按下 run query 按鈕

可以透過視覺化的方式來看到整個的關聯圖,已這個例子來說,Total Spans 一共三個,總共花費的時間為 60.288 ms,其中 stepOne 與 stepTwo 各自花費的時間,以及這兩個 span 中所各自定義的屬性是甚麼(右邊區塊),可以發現的是透過 zipkin 分散式追蹤工具在理解上更為清晰,如果在執行過程中有某個 span 是緩慢的可以更清楚的知道緩慢的原因與時間,那麼回到真實的世界會是甚麼樣子呢 ? 我們可以透過範例三知道。

Demo#3 : Automatic instrumentation
source code :
OpenTelemetrySample.WeatherForecast.Client
OpenTelemetrySample.WeatherForecast.API
在這範例中會透過較真實案例來說明,前端 Client 專案會透過 httpClient 呼叫後端 API 來取得 WeatherForecast 資訊,後端 API 會將資訊紀錄在 Redis 資料庫中,在亂數回 weather 資訊。我們會在這專案中引用自動蒐集遙測數據的 Library 像是 AddHttpClientInstrumentation,在發出 Httpclient 請求時自動蒐集相關遙測數據,並將其關聯結果呈現在 zipkin 工具上。
在 Client 專案的 Startup.cs 程式碼如下
public class Startup
{	
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllersWithViews();

		services.AddOpenTelemetryTracing(Configuration);
	}

	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
		//...省略
	}
	
	static class ServiceCollectionExtensions
    {

        public static IServiceCollection AddOpenTelemetryTracing(this IServiceCollection services, IConfiguration configuration)
        {
            var zipkinServiceName = configuration.GetValue("Zipkin:ServiceName");
            var zipkinEndpoint = configuration.GetValue("Zipkin:Endpoint");
            
            services.AddOpenTelemetryTracing((builder) => builder
                    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(zipkinServiceName))
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddZipkinExporter(zipkinOptions =>
                    {
                        zipkinOptions.Endpoint = new Uri($"{zipkinEndpoint}");
                    })
                    .AddConsoleExporter());

            return services;
        }
    }
}
程式碼說明
  • Line 7 : 在 ConfigureService 方法中加上 AddOpenTelemetryTracing 擴充方法
  • Line 20~21 : 取得 Config 設定檔中定義 zipkin 的 serviceName 與 endpoint
  • Line 25 : 將 OpenTelemerey 新增到 ASP.NET Core 應用程式中
  • Line 26 : 針對 httpclient 進行遙測數據的蒐集
  • Line 27 : 輸出到 zipkin 工具

另外,在 API 專案的 Startup.cs 程式碼如下
public class Startup
{	
	public void ConfigureServices(IServiceCollection services)
	{
		services.AddControllers();

		services.AddOpenTelemetryTracing(Configuration);

		services.AddStackExchangeRedisCache(options =>
		{
			options.InstanceName = "Redis Cache";
			options.Configuration = "localhost:6379";
		});
	}

	public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
   {
		// 省略
	}	
	static class ServiceCollectionExtensions
    {

        public static IServiceCollection AddOpenTelemetryTracing(this IServiceCollection services, IConfiguration configuration)
        {
            var zipkinServiceName = configuration.GetValue("Zipkin:ServiceName");
            var zipkinEndpoint = configuration.GetValue("Zipkin:Endpoint");
            var redisEndpoint = configuration.GetValue("Redis:Endpoint");

            var connection = ConnectionMultiplexer.Connect(redisEndpoint);
            services.AddSingleton(connection);
            
            services.AddOpenTelemetryTracing((builder) => builder
                    .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(zipkinServiceName))
                    .AddAspNetCoreInstrumentation()
                    .AddHttpClientInstrumentation()
                    .AddRedisInstrumentation(
                        connection,
                        option=>
                        {
                            option.FlushInterval = TimeSpan.FromSeconds(5);
                        }
                    )
                    .AddZipkinExporter(zipkinOptions =>
                    {
                        zipkinOptions.Endpoint = new Uri($"{zipkinEndpoint}");
                    })
                    .AddConsoleExporter());

            return services;
        }
    }	
}
  
API 的 Startup.cs 程式碼說明
  • Line 9 : 新增 Redis instance
  • Line 30 : 設定 redis life cycle
  • Line 36 : 設定 redis 自動蒐集 Library

撰寫完代瑪之後,開啟瀏覽器輸入 https://localhost:44321/weatherforecast 進行測試,可以發現有正常的回傳 weather 的資訊

接著在開啟 zipkin 看這次的範例會如何呈現

在 zipkin 中可以看到請求的相關連資訊,整個請求可以分為四個 span 每個 span 所花費的時間都可以透過 zipkin 顯示出來,從 Demo Website 到呼叫 Demo API 再到後面的 Redis 緩存 cache 都一目了然呈現,其中像是點選 localhost:6379 redis 的話,右邊區塊會呈現蒐集 redis 相關的遙測數據資料,舉例來說 redis 的 servicename, endpoint、db.system 都有提供,是透過哪個 library 搜集器的資訊與版本也可以看到。

結論
在過去如果要達到相同目蒐集請求資訊的話,可能在代碼上會加上很多不同的類別,像是要蒐集時間可能會使用 stopwatch 來計算執行時間、定義要蒐集的資訊加上logging,logging 會在加上 tag 或是自定義 attribute 蒐集想要的內容、上述部分如果沒有一個好的規範可能在代碼上就會相對雜亂一些,如果希望規模或一致性會定義蒐集資訊的共用規範或是 library 則需要很大的前置作業與功夫來規範。透過 OpenTelemerey 則可以大大省下這些時間,就像上面範例三的 Demo 內容透過 OTel 提供的 Library 就可蒐集請求的遙測數據資料,並 exporter 到指定的工具或是平台上,在透過自己習慣使用的平台來查詢異常問題的 root cause 或是異常的 service 是哪個,三大雲平台廠商在 OpenTelemetry 規範孵蛋成功後,也開始在自己的平台上陸續支援這套蒐集遙測數據的規範,像是在 Azure 就有在 MSDN 與技術部落格說明 Azure Monitor 與 Application insign 如何整合 OpenTelemetry。

這篇文章的範例與程式碼在 Github 上都可以看的到,位置是 OpenTelemetrySample with .NET Core,各位有興趣的朋友都可以 Clone 專案下來在自己的本機環境試試看,hope it help :D

參考
https://opentelemetry.io/

0 意見:

張貼留言

Copyright © m@rcus 學習筆記 | Powered by Blogger

Design by Anders Noren | Blogger Theme by NewBloggerThemes.com