.NETの汎用ホストでOpenTelemetry

# 概要
OpenTelemetryを.NETの汎用ホスト(Generic Host)上で使用するサンプルを作成したため、共有します。誰かの一助になれば幸いです。

# サンプル
[MyOpenTelemetryDotnetSample](https://github.com/kwhrkzk/MyOpenTelemetryDotnetSample)

# 概要図

```mermaid
flowchart 

subgraph compose[docker compose]
zipkin[Zipkin]
myopentelemetryconsole[my opentelemetry console]
sqlite[sqlite]
end

browser[browser]
browser <-->|port:9411| zipkin
myopentelemetryconsole -->|/api/v2/spans| zipkin
myopentelemetryconsole -->|EFCore| sqlite
```

docker compose内でzipkinと.NETアプリを用意し、.NETアプリからzipkinにトレースを送信しています。
EFCoreを介してデータベース(今回はsqlite)を操作することで操作内容もトレースとして送信できました。([OpenTelemetry.Instrumentation.EntityFrameworkCore](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.EntityFrameworkCore/#readme-body-tab)使用)

# 環境
.NET SDK Version: 8.0.100
Docker Compose version v2.15.1
OpenTelemetry Version="1.7.0"
OpenTelemetry.Instrumentation.EntityFrameworkCore Version="1.0.0-beta.9"

# 設定内容例

```c#: IHostBuilder(builder)の設定内容抜粋[Program.cs]
～中略～
builder.ConfigureLogging((HostBuilderContext ctxt, ILoggingBuilder builder) =>
{
    // ILogger<T>の内容をOpenTelemetryのLogsとして出力したい場合には設定必要
    builder.AddOpenTelemetry((OpenTelemetryLoggerOptions logging) => logging.AddConsoleExporter());
})

～中略～

builder.ConfigureServices((HostBuilderContext ctxt, IServiceCollection services) =>
{
～中略～
    string environmentName = "このアプリの名称";

    // アクティビティを生成するための源をDIしておく。毎回その場で生成しても良いが引数の名称をAddSourceで登録しておかないといけない.
    services.AddSingleton<ActivitySource>(new ActivitySource(environmentName));

    services.AddOpenTelemetry()
    .WithTracing((TracerProviderBuilder tracing) => tracing
         // 汎用ホストはサービス名を手動で設定する必要有り. 設定しないとunknown-service-nameと表示される.
        .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(environmentName))
        // ActivitySourceの名称を事前に登録しておく.引数はparams string[].
        .AddSource(environmentName)
        // EFCoreの内容をトレースとして出力するための設定.アクティビティに何をどのように出力するか指定できる.
        .AddEntityFrameworkCoreInstrumentation((EntityFrameworkInstrumentationOptions op) =>
        {
            op.EnrichWithIDbCommand = (Activity activity, IDbCommand command) =>
            {
                activity.DisplayName = $"{command.CommandText[..10]}";
                activity.AddTag("db.CommandText", command.CommandText);
            };
        })
        // トレースの内容をコンソールに表示するために必要.
        .AddConsoleExporter()
        // トレースの内容をzipkinに送信するために必要.
        .AddZipkinExporter((ZipkinExporterOptions op) => op.Endpoint = new Uri("http://myzipkin:9411/api/v2/spans"))
    );
});

```

# 使用例

```C#: 実行箇所の抜粋[MyCommand.cs]

        // DIしたActivitySourceからActivityを生成。引数に入れる文字列はスパン名になる.
        Activity? activity = MyActivitySource.StartActivity("mycommand");
        if (activity is null)
            return;

        // データベースを消したり生成したり追加したり参照したり.
        try
        {
            Db.Database.EnsureDeleted();
            Db.Database.EnsureCreated();

            Db.MyModels.Add(new MyModel() { Id = 1, Name = "test" });
            Db.SaveChanges();

            foreach (var item in Db.MyModels.ToList())
                activity.SetTag(item.Name, item.Id);

            activity.SetStatus(ActivityStatusCode.Ok);

            Db.Database.EnsureDeleted();
        }
        catch (Exception ex)
        {
            activity.SetStatus(ActivityStatusCode.Error, ex.Message);
        }
        finally
        {
            // 最後にdispose必須.
            activity.Dispose();
        }
```

# サンプルの実行方法

```sh
git clone https://github.com/kwhrkzk/MyOpenTelemetryDotnetSample.git
cd MyOpenTelemetryDotnetSample
docker compose up -d
docker logs -f myopentelemetryconsole

```

でとりあえず動作します。終わったら docker compose down

# コンソールの出力結果抜粋

```
Activity.TraceId:            ab68449ad7a9a042bd33d9c82cd6310d
Activity.SpanId:             3e36902623b7e639
Activity.TraceFlags:         Recorded
Activity.ParentSpanId:       d7e4cac2888eb352
Activity.ActivitySourceName: OpenTelemetry.Instrumentation.EntityFrameworkCore
Activity.DisplayName:        CREATE TAB
Activity.Kind:               Client
Activity.StartTime:          2024-01-09T06:27:47.5812126Z
Activity.Duration:           00:00:00.0092473
Activity.Tags:
    db.system: sqlite
    db.name: main
    peer.service: /workspace/mysqlite.db
    db.statement_type: Text
    db.CommandText: CREATE TABLE "MyModels" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_MyModels" PRIMARY KEY AUTOINCREMENT,
    "Name" TEXT NULL
);
```
Activity.DisplayNameはProgram.cs上でsqlの先頭10文字に変更しています。
また、db.CommandTextというキーでsqlの内容をタグに追加しています。

# Zipkinでの表示例
[http://localhost:9411/zipkin](http://localhost:9411/zipkin)
※表示されるまで時間かかります。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/ed17c0f8-f142-ca57-f449-b92d87c3d3c8.png)

# コンテナでの開発

```sh
docker compose -f docker-compose.yml -f docker-compose.development.yml up -d
docker exec -it myopentelemetryconsole-development /bin/bash
cd /workspace/myopentelemetryconsole
```

でソースに行けるので自由にビルド可能です。

# まとめ
OpenTelemetryというと分散トレーシングとかマイクロービスとか難しい感じがしますけど単純なバッチに仕込んでもボトルネックの把握に有用だと思いました。
定期ジョブでエラーが発生した際も、ログファイルに出力しただけだと検知しにくかったり取りにいったり面倒ですがとりあえずZipkinとかに突っ込んでおけば気付きやすくなりそうです。