# Jaeger搭配Docker來追蹤NET 7 ### 前情提要 先介紹一下Jaeger、OpenTelemetry #### 1. Jaeger Jaeger 是一種可用於監控和解決互連軟體元件 (稱為微型服務) 問題的軟體。多個微型服務可彼此通訊,以完成單一軟體功能。開發人員使用 Jaeger 來視覺化這些微型服務互動中的事件鏈結,以便在出現問題時隔離問題。Jaeger 也稱為 Jaeger Tracing,因為其可透過一系列的微型服務互動來跟蹤或追蹤請求的路徑。 - Jaeger Client: Jaeger client的Library, 有對OpenTelemetry和OpenTracing進行了實現. - Jaeger Agent 也是Sidecar模式的實現, 負責把client透過UDP發出的spans給批量推送到Collector上. 主要是為了屏蔽client對於collector的路由實做細節. - Jaeger Collector 收集Spans, 把Span經過驗證、轉換、索引並且寫入DB內,Colletcor能設定Sampling採樣邏輯, 根據Sampling的設定進行收集和處理 因為這組件是無狀態的, 所以可以建立很多個Collector加速寫入到DB,DB支援了Cassandra、Elasticsearch、Kafka. 官方建議是用Cassandra, 原因有2. Cassanda是一個K-V資料庫, 對於用TracdID來搜尋的場景效率很高. 且寫入吞吐量相當好,但若是為了分析查詢, 還是Elasticsearch較好. - Jaeger Query 接收查詢請求, 然後從DB中檢索, 並透過UI展示, Jaeger Query也是無狀態的, 所以可以啟動多個實例. #### 2. OprnTelemetry OpenTelemetry 為遙測資料建立單一開放式標準,並透過該技術收集及匯出雲端原生應用程式的資料,藉此監控及分析資料。 - Span (跨度):Span 是 OpenTelemetry 中的一個基本概念,代表應用程序中的一個操作或事件。例如,HTTP 請求、資料庫查詢等。Span 包含了一些元數據,如開始時間、結束時間、標籤(tags)、事件(events)等。 - Tracer (追蹤器):Tracer 是 OpenTelemetry 提供的用於創建和管理 Span 的介面。通過 Tracer,你可以在應用程式中創建新的 Span 並將它們連接起來,以形成分散式追蹤的整體視圖。 - Context (上下文):Context 是 OpenTelemetry 中的一個概念,用於將追蹤信息(如 Span)傳遞給應用程序中的不同部分。Context 包含了一些關鍵信息,例如追蹤 ID、span ID 等。在應用程式中,Context 可以透過不同的方式(例如 HTTP headers)傳遞。 - Instrumentation (儀器化):Instrumentation 是 OpenTelemetry 中的一個模組,用於自動捕獲應用程序中的事件並創建相應的 Span。這樣的自動化有助於減少手動儀器化的工作。 - Exporter (導出器):Exporter 是 OpenTelemetry 中的一個組件,用於將收集的追蹤數據輸出到不同的目的地,如後端存儲、分析工具等。常見的 Exporter 包括將數據發送到 Zipkin、Jaeger、Prometheus 等。 - Sampler (取樣器):Sampler 是一個用於確定是否應該捕獲特定 Span 的組件。它可以幫助控制追蹤數據的量,以防止過度膨脹。Sampler 可以按比例抽樣或者根據其他策略進行抽樣。 - Attribute (屬性):Attribute 是 Span 或其他 OpenTelemetry 元件上的一對鍵值對,用於存儲與該元件相關的任意額外信息。這些屬性可以用來細化、標記和描述 Span。 ### 建立基本架構 - Jaeger all-in-one持久化 [參考資料](https://jckling.github.io/2021/05/10/Jaeger/Jaeger%20+%20Elasticsearch%20+%20Kibana/index.html) 1. 先在docker中建立簡易All in one 版本 Jaeger 搭配 Elasticsearch、Kibana 建立docker-compose,執行docker-compose後initial_jaeger在initial完之後就會關閉了 ```yml= version: '3' services: jaeger: image: jaegertracing/all-in-one container_name: jaeger environment: - SPAN_STORAGE_TYPE=elasticsearch - COLLECTOR_ZIPKIN_HOST_PORT=:9411 - ES_SERVER_URLS=http://elasticsearch:9200 - ES_TAGS_AS_FIELDS_ALL=true ports: - "5775:5775/udp" - "6831:6831/udp" - "6832:6832/udp" - "5778:5778" - "16686:16686" - "14268:14268" - "14250:14250" - "9411:9411" - "4317:4317" depends_on: - elasticsearch elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.12.1 container_name: elasticsearch environment: - discovery.type=single-node - xpack.security.enabled=false ports: - "9200:9200" - "9300:9300" kibana: image: docker.elastic.co/kibana/kibana:7.12.1 container_name: kibana environment: - ELASTICSEARCH_HOSTS=http://elasticsearch:9200 ports: - "5601:5601" depends_on: - elasticsearch networks: jaeger-net: ``` ### 針對.Net進行撰寫 1. 請先安裝NuGet套件,利用dotnet cli安裝,注意套件版本 ``` = dotnet add package OpenTelemetry --version 1.7.0 dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol --version 1.7.0 dotnet add package OpenTelemetry.Extensions.Hosting --version 1.7.0 dotnet add package OpenTelemetry.Instrumentation.AspNetCore --version 1.7.0 ``` 2. 在Net7專案中的新增JaegerHelper.cs,建立一個Jaeger連線(JaegerHelper請用Singleton) ```csharp= tracer = Sdk.CreateTracerProviderBuilder() .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Service_Name")) .AddAspNetCoreInstrumentation(o => { o.EnrichWithHttpRequest = (activity, httpRequest) => { activity.SetTag("requestProtocol", httpRequest.Protocol); }; o.EnrichWithHttpResponse = (activity, httpResponse) => { activity.SetTag("responseLength", httpResponse.ContentLength); }; o.EnrichWithException = (activity, exception) => { activity.SetTag("exceptionType", exception.GetType().ToString()); }; }) .AddSource("*") .AddOtlpExporter(opt => { opt.Endpoint = new Uri(JaegerUri); opt.Protocol = OtlpExportProtocol.Grpc; }) .Build(); ``` 3. 在需要追蹤的地方呼叫Singleton JaegerHelper ```csharp= using (var childSpan = JaegerHelper.Instance.GetInstance().tracer.GetTracer("Service_Name").StartActiveSpan("MethodName")) { childSpan.SetAttribute("Result", "OK"); //可放需要新增的Attribute } ``` 4. 用DispatchProxy來攔截需要被追蹤的程式 ```csharp= public class InterceptorProxy<T> : DispatchProxy { public T Target { get; set; } JsonSerializerOptions options = new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve }; protected override object Invoke(MethodInfo targetMethod, object[] args) { try { if (Array.IndexOf(MethodFilter, targetMethod.Name) < 0) { using (var childSpan = JaegerHelper.Instance.GetInstance().tracer.GetTracer("Service_Name").StartActiveSpan(targetMethod.Name)) { object result = targetMethod.Invoke(Target, args); childSpan.SetAttribute("Programs", Target.ToString()); childSpan.SetAttribute("Parameter", JsonSerializer.Serialize(args, options)); if (result is Task resultTask) { resultTask.Wait(); // 等待非泛型 Task 的完成 if (resultTask.GetType().GetProperty("Result") is PropertyInfo resultProperty && resultProperty.PropertyType != typeof(void)) { object taskResult = resultProperty.GetValue(resultTask); childSpan.SetAttribute("Result", JsonSerializer.Serialize(resultTask, options)); return result; } } else { return result; } } } return targetMethod.Invoke(Target, args); } catch (Exception ex) { return ex; } finally { } } } ``` 5. 隊要被追蹤的程式進行Proxy,呼叫Proxy後的程式才會被DispatchProxy攔截到 ```csharp= var ProxyName = DispatchProxy.Create<Interface, InterceptorProxy<Interface>>(); ((InterceptorProxy<Interface>)ProxyName).Target = class; //說明 第一行是在建立一個由Interface去Proxy的變數,注意只有Interface才能被Proxy所以要追蹤class的話請讓他繼承一個interface。 //第二行是將Proxy後的變數設定一個指向目標的class當執行到這個變數時他會去找它的Target的方法,此處Target就是你的class。 ``` ### 最後結果 1. 完成上述步驟後應該有個雛型了,這邊的追蹤方法偏向對現有專案進行追蹤,網路上有許多簡單的追蹤方法這邊就不多做說明。 ![螢幕擷取畫面 2024-01-12 150456](https://hackmd.io/_uploads/Byhj4PR_6.png) 2. 各追蹤內容,可以看到相較簡單的設置這邊多了幾層的span讓他可以追蹤到更下層。 ![螢幕擷取畫面 2024-01-12 150624](https://hackmd.io/_uploads/S1m-HDCOa.png)