OpenTelemetry トレーシング Shim

.NET は OpenTelemetry をサポートする他の言語やランタイムとは異なります。 トレースは System.Diagnostics API によって実装されており、ActivitySourceActivity といった既存のコンストラクトを再利用して、内部的に OpenTelemetry に準拠するようになっています。

OpenTelemetry for .NET は、System.Diagnostics ベースの実装の上に API Shim も提供しています。 この Shim は、同じコードベースで他の言語と OpenTelemetry を併用している場合や、OpenTelemetry の仕様と一貫した用語を使いたい場合に役立ちます。

トレースの初期化

トレースを初期化するには、主に2つの方法があります。 コンソールアプリを使用するか、ASP.NET Core ベースのものを使用するかによって異なります。

コンソールアプリ

コンソールアプリでトレースを開始するには、トレーサープロバイダーを作成する必要があります。

まず、適切なパッケージがインストールされていることを確認してください。

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

次に、プログラムの先頭、重要なスタートアップ処理の中で、以下のようなコードを使用してください。

using OpenTelemetry;
using OpenTelemetry.Trace;
using OpenTelemetry.Resources;

// ...

var serviceName = "MyServiceName";
var serviceVersion = "1.0.0";

using var tracerProvider = Sdk.CreateTracerProviderBuilder()
    .AddSource(serviceName)
    .SetResourceBuilder(
        ResourceBuilder.CreateDefault()
            .AddService(serviceName: serviceName, serviceVersion: serviceVersion))
    .AddConsoleExporter()
    .Build();

//...

ここで計装ライブラリを設定することもできます。

このサンプルではコンソールエクスポーターを使用しています。 別のエンドポイントにエクスポートする場合は、別のエクスポーターを使用する必要があります。

ASP.NET Core

ASP.NET Core ベースのアプリでトレースを開始するには、ASP.NET Core セットアップ用の OpenTelemetry エクステンションを使用してください。

まず、適切なパッケージがインストールされていることを確認してください。

dotnet add package OpenTelemetry --prerelease
dotnet add package OpenTelemetry.Instrumentation.AspNetCore --prerelease
dotnet add package OpenTelemetry.Extensions.Hosting --prerelease
dotnet add package OpenTelemetry.Exporter.Console --prerelease

次に、IServiceCollection にアクセスできる ASP.NET Core のスタートアップルーチンで設定してください。

using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

// これらは設定ファイルや定数ファイルなどから取得できます。
var serviceName = "MyCompany.MyProduct.MyService";
var serviceVersion = "1.0.0";

var builder = WebApplication.CreateBuilder(args);

// 重要な OpenTelemetry 設定、コンソールエクスポーター、計装ライブラリを構成
builder.Services.AddOpenTelemetry().WithTracing(tcb =>
{
    tcb
    .AddSource(serviceName)
    .SetResourceBuilder(
        ResourceBuilder.CreateDefault()
            .AddService(serviceName: serviceName, serviceVersion: serviceVersion))
    .AddAspNetCoreInstrumentation()
    .AddConsoleExporter();
});

上の例では、セットアップ時にサービスに対応する Tracer が注入されます。 これにより、エンドポイントマッピング(または古いバージョンの .NET を使用している場合はコントローラー)でインスタンスにアクセスできるようになります。

サービスレベルのトレーサーを注入することは必須ではなく、パフォーマンスの向上にもなりません。 ただし、トレーサーインスタンスをどこに配置するかは決める必要があります。

ここで計装ライブラリを設定することもできます。

このサンプルではコンソールエクスポーターを使用しています。 別のエンドポイントにエクスポートする場合は、別のエクスポーターを使用する必要があります。

トレーサーのセットアップ

トレースが初期化されると、Tracer を設定できます。 これにより、Span を使ってオペレーションをトレースできるようになります。

通常、Tracer は計装対象のアプリやサービスごとに一度だけインスタンス化されるため、共有の場所で一度インスタンス化するのがよいでしょう。 また、通常はサービス名と同じ名前が付けられます。

ASP.NET Core でのトレーサーの注入

ASP.NET Core では一般的に、Tracer のような長寿命のオブジェクトのインスタンスをセットアップ時に注入することが推奨されています。

using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

// ...

builder.Services.AddSingleton(TracerProvider.Default.GetTracer(serviceName));

// ...

var app = builder.Build();

// ...

app.MapGet("/hello", (Tracer tracer) =>
{
    using var span = tracer.StartActiveSpan("hello-span");

    // 処理を実行
});

TracerProvider からトレーサーを取得する

ASP.NET Core を使用していない場合や Tracer のインスタンスを注入したくない場合は、インスタンス化した TracerProvider から作成してください。

// ...

var tracer = tracerProvider.GetTracer(serviceName);

// グローバルにアクセスできる場所に割り当て

//...

この Tracer インスタンスを中心的な場所の変数に割り当てて、サービス全体でアクセスできるようにするとよいでしょう。

サービスごとに必要な数だけ Tracer をインスタンス化できますが、通常はサービスごとに1つ定義すれば十分です。

スパンの作成

スパンを作成するには、名前を付けて Tracer から作成します。

using var span = MyTracer.StartActiveSpan("SayHello");

// 'span' がトラッキングする処理を実行

ネストされたスパンの作成

あるオペレーションの一部として追跡したい個別のサブオペレーションがある場合、その関係を表すスパンを作成できます。

public static void ParentOperation(Tracer tracer)
{
    using var parentSpan = tracer.StartActiveSpan("parent-span");

    // parentSpan でトラッキングされる処理を実行

    ChildOperation(tracer);

    // parentSpan でトラッキングされる処理を再開
}

public static void ChildOperation(Tracer tracer)
{
    using var childSpan = tracer.StartActiveSpan("child-span");

    // childSpan で ChildOperation の処理をトラッキング
}

トレース可視化ツールでスパンを表示すると、child-spanparent-span の下にネストされたオペレーションとしてトラッキングされます。

同じスコープ内のネストされたスパン

同じスコープ内で親子関係を作成することもできます。 ただし、ネストされた TelemetrySpan を期待通りのタイミングで終了するよう注意する必要があるため、一般的には推奨されません。

public static void DoWork(Tracer tracer)
{
    using var parentSpan = tracer.StartActiveSpan("parent-span");

    // parentSpan でトラッキングされる処理を実行

    using (var childSpan = tracer.StartActiveSpan("child-span"))
    {
        // 同じ関数内で「子」の処理を実行
    }

    // parentSpan でトラッキングされる処理を再開
}

上の例では、using ブロックのスコープが明示的に定義されているため、childSpan は終了されます。 parentSpan のように DoWork 自体にスコープが設定されているのとは異なります。

独立したスパンの作成

前の例では、ネストされた階層に従うスパンの作成方法を示しました。 場合によっては、ネストではなく、同じルートの兄弟となる独立したスパンを作成したいことがあります。

public static void DoWork(Tracer tracer)
{
    using var parent = tracer.StartSpan("parent");
    // 'parent' は 'child1' と 'child2' の両方の共有された親になります

    using (var child1 = tracer.StartSpan("child1"))
    {
        // 'child1' がトラッキングする処理を実行
    }

    using (var child2 = tracer.StartSpan("child2"))
    {
        // 'child2' がトラッキングする処理を実行
    }
}

新しいルートスパンの作成

現在のトレースから完全に切り離された新しいルートスパンを作成することもできます。

public static void DoWork(Tracer tracer)
{
    using var newRoot = tracer.StartRootSpan("newRoot");
}

現在のスパンの取得

ある時点での現在の TelemetrySpan にアクセスして、追加情報で拡充できると便利な場合があります。

var span = Tracer.CurrentSpan;
// ここで追加の処理を実行

上の例では using を使用していないことに注意してください。 使用すると、スコープを抜けた時点で現在の TelemetrySpan が終了してしまいます。 これは望ましい動作ではないでしょう。

スパンに属性を追加する

属性を使用すると、TelemetrySpan にキーと値のペアを付加して、トラッキングしている現在のオペレーションに関する追加情報を持たせることができます。

using var span = tracer.StartActiveSpan("SayHello");

span.SetAttribute("operation.value", 1);
span.SetAttribute("operation.name", "Saying hello!");
span.SetAttribute("operation.other-stuff", new int[] { 1, 2, 3 });

イベントの追加

イベントは、TelemetrySpan のライフタイム中に「何かが起きた」ことを表す人間が読めるメッセージです。 プリミティブなログのようなものと考えることができます。

using var span = tracer.StartActiveSpan("SayHello");

// ...

span.AddEvent("Doing something...");

// ...

span.AddEvent("Dit it!");

イベントはタイムスタンプと属性のコレクションを指定して作成することもできます。

using var span = tracer.StartActiveSpan("SayHello");

// ...

span.AddEvent("event-message");
span.AddEvent("event-message2", DateTimeOffset.Now);

// ...

var attributeData = new Dictionary<string, object>
{
    {"foo", 1 },
    { "bar", "Hello, World!" },
    { "baz", new int[] { 1, 2, 3 } }
};

span.AddEvent("asdf", DateTimeOffset.Now, new(attributeData));

TelemetrySpan は、因果関係のある0個以上の Link を付けて作成できます。

// どこかからコンテキストを取得(パラメーターとして渡されるなど)
var ctx = span.Context;

var links = new List<Link>
{
    new(ctx)
};

using var span = tracer.StartActiveSpan("another-span", links: links);

// 処理を実行

スパンステータスの設定

ステータスはスパンに設定でき、通常はスパンが正常に完了しなかったことを示すために使用されます(StatusCode.Error)。 まれなシナリオでは、Error ステータスを Ok で上書きすることもできますが、正常に完了したスパンには Ok を設定しないでください。

ステータスはスパンが終了する前であればいつでも設定できます。

using var span = tracer.StartActiveSpan("SayHello");

try
{
	// 何らかの処理を実行
}
catch (Exception ex)
{
    span.SetStatus(new(StatusCode.Error, "Something bad happened!"));
}

スパンに例外を記録する

例外が発生したときに記録するのはよいプラクティスです。 スパンステータスの設定と組み合わせて行うことが推奨されます。

using var span = tracer.StartActiveSpan("SayHello");

try
{
	// 何らかの処理を実行
}
catch (Exception ex)
{
    span.SetStatus(new(StatusCode.Error, "Something bad happened!"));
    span.RecordException(ex)
}

これにより、現在のスタックトレースなどの情報がスパンの属性としてキャプチャされます。

次のステップ

手動計装をセットアップしたら、計装ライブラリを使用するとよいでしょう。 計装ライブラリは、使用している関連ライブラリを計装し、インバウンドおよびアウトバウンド HTTP リクエストなどのデータを生成します。

また、テレメトリーデータを1つ以上のテレメトリーバックエンドにエクスポートするための適切なエクスポーターを設定する必要もあります。