新增分散式追蹤檢測

本文適用於:✔️.NET Core 2.1 和更新版本 ✔️ .NET Framework 4.5 和更新版本

您可以使用 System.Diagnostics.Activity API 來檢測 .NET 應用程式,以產生分散式追蹤遙測。 某些檢測內建於標準 .NET 程式庫,但您可能想要新增更多檢測,讓您的程式碼更容易診斷。 在本教學課程中,您將會新增新的自訂分散式追蹤檢測。 若要深入了解記錄此檢測所產生的遙測,請參閱集合教學課程

必要條件

建立初始應用程式

首先,您將會建立使用 OpenTelemetry 收集遙測的範例應用程式,但還沒有任何檢測。

dotnet new console

目標為 .NET 5 和更新版本的應用程式已經包含必要的分散式追蹤 API。 針對以舊 .NET 版本為目標的應用程式,請新增 System.Diagnostics.DiagnosticSource NuGet 套件 第 5 版或更新版本。

dotnet add package System.Diagnostics.DiagnosticSource

新增 OpenTelemetryOpenTelemetry.Exporter.Console NuGet 套件,此套件將會用來收集遙測。

dotnet add package OpenTelemetry
dotnet add package OpenTelemetry.Exporter.Console

以此範例來源取代產生之 program.cs 的內容:

using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System;
using System.Threading.Tasks;

namespace Sample.DistributedTracing
{
    class Program
    {
        static async Task Main(string[] args)
        {
            using var tracerProvider = Sdk.CreateTracerProviderBuilder()
                .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("MySample"))
                .AddSource("Sample.DistributedTracing")
                .AddConsoleExporter()
                .Build();

            await DoSomeWork("banana", 8);
            Console.WriteLine("Example work done");
        }

        // All the functions below simulate doing some arbitrary work
        static async Task DoSomeWork(string foo, int bar)
        {
            await StepOne();
            await StepTwo();
        }

        static async Task StepOne()
        {
            await Task.Delay(500);
        }

        static async Task StepTwo()
        {
            await Task.Delay(1000);
        }
    }
}

應用程式還沒有任何檢測,因此沒有可顯示的追蹤資訊:

> dotnet run
Example work done

最佳作法

只有應用程式開發人員需要參考選擇性的第三方程式庫來收集分散式追蹤遙測,例如此範例中的 OpenTelemetry。 .NET 程式庫作者可以獨佔地依賴 System.Diagnostics.DiagnosticSource 中的 API,這是 .NET 執行階段的一部分。 這可確保程式庫會在各種 .NET 應用程式中執行,而不論應用程式開發人員對於要用來收集遙測的程式庫或廠商喜好設定為何。

新增基本檢測

應用程式和程式庫會使用 System.Diagnostics.ActivitySourceSystem.Diagnostics.Activity 類別新增分散式追蹤檢測。

ActivitySource

首先建立 ActivitySource 的執行個體。 ActivitySource 提供 API 來建立和啟動 Activity 物件。 將靜態 ActivitySource 變數新增至 Main() 上方,並且將 using System.Diagnostics; 新增至 using 陳述式。

using OpenTelemetry;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
using System;
using System.Diagnostics;
using System.Threading.Tasks;

namespace Sample.DistributedTracing
{
    class Program
    {
        private static ActivitySource source = new ActivitySource("Sample.DistributedTracing", "1.0.0");

        static async Task Main(string[] args)
        {
            ...

最佳作法

  • 先建立一次 ActivitySource,儲存在靜態變數中,並視需求使用該執行個體。 每個程式庫或程式庫子元件都能夠 (而且通常最好) 建立自己的來源。 如果您預期應用程式開發人員會希望能夠獨立地啟用和停用來源中的 Activity 遙測,請考慮建立新的來源,而不是重複使用現有的來源。

  • 傳遞至建構函式的來源名稱必須是唯一的,避免與任何其他來源發生衝突。 如果相同組件內有多個來源,請使用包含組件名稱和選擇性包含元件名稱的階層式名稱,例如,Microsoft.AspNetCore.Hosting。 如果組件是在第二個獨立組件中新增程式碼的檢測,則名稱應該以定義 ActivitySource 的組件為基礎,而不是要檢測其程式碼的組件。

  • version 是選擇性參數。 建議您提供版本,以防發行多個版本的程式庫和變更檢測遙測。

注意

OpenTelemetry 使用替代詞彙 'Tracer' 和 'Span'。 在 .NET 中,'ActivitySource' 是 Tracer 的實作,而 Activity 是 'Span' 的實作。 .NET 的 Activity 型別的時間會遠遠早於 OpenTelemetry 規格,原始 .NET 命名已針對 .NET 生態系統內的一致性和 .NET 應用程式相容性保留。

活動

使用 ActivitySource 物件,在有意義的工作單位周圍啟動和停止 Activity 物件。 使用如下所示的程式碼更新 DoSomeWork():

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                await StepOne();
                await StepTwo();
            }
        }

執行應用程式現在會顯示正在記錄的新 Activity:

> dotnet run
Activity.Id:          00-f443e487a4998c41a6fd6fe88bae644e-5b7253de08ed474f-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:36:51.4720202Z
Activity.Duration:    00:00:01.5025842
Resource associated with Activity:
    service.name: MySample
    service.instance.id: 067f4bb5-a5a8-4898-a288-dec569d6dbef

備註

  • ActivitySource.StartActivity 會同時建立並啟動活動。 列出的程式碼模式是使用 using 區塊,這會在執行區塊之後自動處置已建立的 Activity 物件。 處置 Activity 物件會將其停止,因此程式碼不需要明確呼叫 Activity.Stop()。 這可簡化編碼模式。

  • ActivitySource.StartActivity 在內部判斷是否有任何接聽程式記錄 Activity。 如果沒有已註冊的接聽程式,或有不感興趣的接聽程式,StartActivity() 將會傳回 null 並避免建立 Activity 物件。 這是效能最佳化,因此程式碼模式仍可用於經常呼叫的函式中。

選擇性:填入標記

活動支援稱為 Tags 的索引鍵/值資料,通常用來儲存對於診斷可能有用之工作的任何參數。 更新 DoSomeWork() 以將其納入:

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                activity?.SetTag("foo", foo);
                activity?.SetTag("bar", bar);
                await StepOne();
                await StepTwo();
            }
        }
> dotnet run
Activity.Id:          00-2b56072db8cb5a4496a4bfb69f46aa06-7bc4acda3b9cce4d-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:37:31.4949570Z
Activity.Duration:    00:00:01.5417719
Activity.TagObjects:
    foo: banana
    bar: 8
Resource associated with Activity:
    service.name: MySample
    service.instance.id: 25bbc1c3-2de5-48d9-9333-062377fea49c

Example work done

最佳作法

  • 如上所述,ActivitySource.StartActivity 所傳回的 activity 可能是 Null。 C# 中的 Null 聯合運算子 ?. 是方便的簡短操作,只有在 activity 不是 Null 時才叫用 Activity.SetTag。 此行為與寫入相同:
if(activity != null)
{
    activity.SetTag("foo", foo);
}
  • OpenTelemetry 提供一組建議的慣例,用於在代表常見應用程式工作類型的 Activity 上設定 Tag。

  • 如果您要檢測具有高效能需求的函式,則 Activity.IsAllDataRequested 是提示,指出任何接聽 Activity 的程式碼是否想要讀取輔助資訊,例如 Tag。 如果沒有接聽程式會讀取,則不需要檢測的程式碼花費 CPU 週期來填入。 為了簡單起見,此範例不會套用該最佳化。

選擇性:新增事件

事件是時間戳記訊息,可將額外診斷資料的任意串流附加至 Activity。 將一些事件新增至 Activity:

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                activity?.SetTag("foo", foo);
                activity?.SetTag("bar", bar);
                await StepOne();
                activity?.AddEvent(new ActivityEvent("Part way there"));
                await StepTwo();
                activity?.AddEvent(new ActivityEvent("Done now"));
            }
        }
> dotnet run
Activity.Id:          00-82cf6ea92661b84d9fd881731741d04e-33fff2835a03c041-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:39:10.6902609Z
Activity.Duration:    00:00:01.5147582
Activity.TagObjects:
    foo: banana
    bar: 8
Activity.Events:
    Part way there [3/18/2021 10:39:11 AM +00:00]
    Done now [3/18/2021 10:39:12 AM +00:00]
Resource associated with Activity:
    service.name: MySample
    service.instance.id: ea7f0fcb-3673-48e0-b6ce-e4af5a86ce4f

Example work done

最佳作法

  • 事件會儲存在記憶體內部清單中,直到可以傳輸事件為止,讓這項機制只適用於錄製少量的事件。 對於大型或未繫結的事件量,最好使用著重於這項工作的記錄 API,例如 ILogger。 不論應用程式開發人員是否選擇使用分散式追蹤,ILogger 也可確保記錄資訊可供使用。 ILogger 支援自動擷取使用中的 Activity 識別碼,因此透過該 API 記錄的訊息仍可與分散式追蹤相互關聯。

選擇性:新增狀態

OpenTelemetry 可讓每個 Activity 報告狀態,代表工作的通過/失敗結果。 .NET 目前沒有適用於此用途的強型別 API,但有使用 Tag 的已建立慣例:

  • otel.status_code 是用來儲存 StatusCode 的 Tag 名稱。 StatusCode 標記的值必須是以下其中一個字串:"UNSET"、"OK" 或 "ERROR",分別對應至 StatusCode 的列舉 UnsetOkError
  • otel.status_description 是用來儲存選擇性 Description 的 Tag 名稱

更新 DoSomeWork() 以設定狀態:

        static async Task DoSomeWork(string foo, int bar)
        {
            using (Activity activity = source.StartActivity("SomeWork"))
            {
                activity?.SetTag("foo", foo);
                activity?.SetTag("bar", bar);
                await StepOne();
                activity?.AddEvent(new ActivityEvent("Part way there"));
                await StepTwo();
                activity?.AddEvent(new ActivityEvent("Done now"));

                // Pretend something went wrong
                activity?.SetTag("otel.status_code", "ERROR");
                activity?.SetTag("otel.status_description", "Use this text give more information about the error");
            }
        }

選擇性:新增其他 Activity

Activity 可以成為巢狀以描述較大工作單位的部分。 在可能無法快速執行的程式碼部分或更妥善當地語系化來自特定外部相依性的失敗時,這很實用。 雖然此範例會在每個方法中使用 Activity,但這只是因為已最小化額外的程式碼。 在較大型且更實際的專案中,在每個方法中使用 Activity 會產生非常詳細的追蹤,因此不建議這麼做。

更新 StepOne 和 StepTwo,以針對這些個別步驟新增更多追蹤:

        static async Task StepOne()
        {
            using (Activity activity = source.StartActivity("StepOne"))
            {
                await Task.Delay(500);
            }
        }

        static async Task StepTwo()
        {
            using (Activity activity = source.StartActivity("StepTwo"))
            {
                await Task.Delay(1000);
            }
        }
> dotnet run
Activity.Id:          00-9d5aa439e0df7e49b4abff8d2d5329a9-39cac574e8fda44b-01
Activity.ParentId:    00-9d5aa439e0df7e49b4abff8d2d5329a9-f16529d0b7c49e44-01
Activity.DisplayName: StepOne
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:40:51.4278822Z
Activity.Duration:    00:00:00.5051364
Resource associated with Activity:
    service.name: MySample
    service.instance.id: e0a8c12c-249d-4bdd-8180-8931b9b6e8d0

Activity.Id:          00-9d5aa439e0df7e49b4abff8d2d5329a9-4ccccb6efdc59546-01
Activity.ParentId:    00-9d5aa439e0df7e49b4abff8d2d5329a9-f16529d0b7c49e44-01
Activity.DisplayName: StepTwo
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:40:51.9441095Z
Activity.Duration:    00:00:01.0052729
Resource associated with Activity:
    service.name: MySample
    service.instance.id: e0a8c12c-249d-4bdd-8180-8931b9b6e8d0

Activity.Id:          00-9d5aa439e0df7e49b4abff8d2d5329a9-f16529d0b7c49e44-01
Activity.DisplayName: SomeWork
Activity.Kind:        Internal
Activity.StartTime:   2021-03-18T10:40:51.4256627Z
Activity.Duration:    00:00:01.5286408
Activity.TagObjects:
    foo: banana
    bar: 8
    otel.status_code: ERROR
    otel.status_description: Use this text give more information about the error
Activity.Events:
    Part way there [3/18/2021 10:40:51 AM +00:00]
    Done now [3/18/2021 10:40:52 AM +00:00]
Resource associated with Activity:
    service.name: MySample
    service.instance.id: e0a8c12c-249d-4bdd-8180-8931b9b6e8d0

Example work done

請注意,StepOne 和 StepTwo 都包含參照 SomeWork 的 ParentId。 主控台不是巢狀工作樹狀結構的絕佳視覺效果,但許多 GUI 檢視器 (例如 Zipkin) 可以將其顯示為甘特圖:

Zipkin Gantt chart

選擇性:ActivityKind

Activity 具有 Activity.Kind 屬性,描述 Activity、其父代與其子系之間的關聯性。 根據預設,所有新的 Activity 都會設定為 Internal,適用於沒有遠端父代或子系之應用程式內部作業的 Activity。 您可以使用 ActivitySource.StartActivity 上的 kind 參數來設定其他種類。 關於其他選項,請參閱 System.Diagnostics.ActivityKind

在批次處理系統中發生工作時,單一 Activity 可能會同時代表許多不同要求,每個要求都有自己的追蹤識別碼。雖然 Activity 限制為具有單一父代,但可以使用 System.Diagnostics.ActivityLink 連結到其他追蹤識別碼。 每個 ActivityLink 都會填入 ActivityContext,其會儲存所連結 Activity 的識別碼資訊。 ActivityContext 可以使用 Activity.Context 從同處理序 Activity 物件擷取,也可以使用 ActivityContext.Parse(String, String) 從序列化識別碼資訊進行剖析。

void DoBatchWork(ActivityContext[] requestContexts)
{
    // Assume each context in requestContexts encodes the trace-id that was sent with a request
    using(Activity activity = s_source.StartActivity(name: "BigBatchOfWork",
                                                     kind: ActivityKind.Internal,
                                                     parentContext: default,
                                                     links: requestContexts.Select(ctx => new ActivityLink(ctx))
    {
        // do the batch of work here
    }
}

不同於可隨選新增的事件和 Tag,連結必須在 StartActivity() 期間新增,且之後不可變。

重要

根據 OpenTelemetry 規格,連結數目的建議限制為 128。 不過,請務必注意,系統不會強制執行此限制。