ねののお庭。

かりかりもふもふ。

C# ではじめる OpenTelemetry。

この記事は Qiita C# Advent Calendar 2023 23 日目の記事です。

登壇版

.NET ラボ 2023/12/16 で登壇した際の資料です。 スライドはこちら

OpenTelemetry について

OpenTelemetry とはなんでしょう? 公式には「OpenTelemetry is an Observability framework and toolkit」と書かれています。 specification ではなく framework であり toolkit だぞ、と。

ということで OpenTelemetry は Observability を高めるための framework 及び toolkit なわけですが、Observability を高めるには何が重要でしょうか? それはテレメトリデータです。 テレメトリデータってなんじゃい?というと、トレース/メトリクス/ログ等の事です。トレース/メトリクス/ログなどを signal と呼んだりもします。 OpenTelemetry では Observability のために、こういったテレメトリに関わる様々な仕様を標準化して、標準化した上で各言語向けにそれらを実装をするという営みがなされています。 テレメトリに関する様々なものは、以前は各 OSS (e.g. Jaeger, Prometheus) に仕様が依存していたり、APM ベンダ依存だったのですが、そういうところから脱却しようとしているわけです。 素晴らしい。

ちなみに OpenTelemetry は CNCF のプロジェクトの一つだったりします。

プロジェクトの主要なコンポーネント

プロジェクトの主要なコンポーネントは以下のように書かれており、かなり野心的なプロジェクトである事が分かります。

  • A specification for all components
  • A standard protocol that defines the shape of telemetry data
  • Semantic conventions that define a standard naming scheme for common telemetry data types
  • APIs that define how to generate telemetry data
  • Language SDKs that implement the specification, APIs, and export of telemetry data
  • A library ecosystem that implements instrumentation for common libraries and frameworks
  • Automatic instrumentation components that generate telemetry data without requiring code changes
  • The OpenTelemetry Collector, a proxy that receives, processes, and exports telemetry data
  • Various other tools, such as the OpenTelemetry Operator for Kubernetes, OpenTelemetry Helm Charts, and community assets for FaaS

雑に訳すとこんなところでしょうか。

  • 全てのコンポーネントの仕様
  • テレメトリデータの標準プロトコル
  • テレメトリデータに関する意味的規則 (semantic conventions)
  • どのようにテレメトリデータの生成するかの API
  • 仕様に沿った各言語に対する SDK の実装
  • 広く使われているライブラリ向けの計装ライブラリ及びエコシステム
  • コードを変更する事なくテレメトリデータを生成するための自動計装
  • テレメトリデータのプロキシ (OpenTelemetry Collector)
  • 様々なツール (OpenTelemetry Operator for Kubernetes など)

API 及び SDK という言葉について

ここで、OpenTelemetry における API と SDK という言葉の意味について触れておきます。 API と SDK については、ちゃんと触れてないと仕様ないし仕様のステータスをみてもヨクワカラナイになってしまうので。

  • API
    • API packages consist of the cross-cutting public interfaces used for instrumentation.
  • SDK
    • The SDK is the implementation of the API provided by the OpenTelemetry project.

ちなみに SDK はアプリケーションオーナーがインストールし、管理するものであり、計装を行う人は SDK を直接参照してはならない(MUST NOT)と書かれています。 計装ライブラリとか出す場合は API だけを参照せよ、と。 この API と SDK は NuGet のパッケージとの対応をみると非常にすっきりします。

  • API : OpenTelemetry.Api
    • The API only surfaces necessary abstractions to instrument an application/library.
  • SDK : OpenTelemetry
    • OpenTelemetry SDK is a reference implementation of the OpenTelemetry API.
    • It implements the Tracing API, the Metrics API, and the Context API. This SDK also supports ILogger integration.

要するに XxxXxx.Abstractions でパッケージ分けるよくある構成をしているといった具合です。

仕様のステータス

ここまでで API と SDK の意味を抑えましたが、それが分かるとようやく仕様のステータスを眺める事ができるようになります。 2023/12/23 時点の仕様のステータスはこんな感じ

  • Tracing
    • API: stable, feature-freeze
    • SDK: stable
    • Protocol: stable
  • Metrics
    • API: stable
    • SDK: mixed
    • Protocol: stable
  • Logging
    • Bridge API: stable
    • SDK: stable
    • Event API: experimental
    • Protocol: stable

Metrics の SDK が mixed なのと、Logging の Event API が experimental ですが、それ以外は全て stable です。 ちなみに Logging の Bridge API と SDK の仕様が stable になったのが 2023/4 なので、今年になってからようやく Tracing/Metrics/Logging の一通り仕様が固まったなという感じです。

仕様のステータスを眺めていると気になる所が出てきます。 それは Logging については Bridge API と Evnet API というの二つの API がある事です。 現時点において重要なのは当然 stable になっている Bridge API なのですが、これはいったい何か。 まず前提として、Logging の歴史はコンピュータの歴史と同義と言えるほど深く、それが故に世の中には多種多様な logger が出回っています。 そのため OpenTelemetry の中の人たちは「OpenTelemetry が成功するためには、既存の logger とうまく付き合っていかないとアカンよね」と考えたようです。 結果として生まれたのが Bridge API で、これは既存の logger を OpenTelemetry の世界に統合するための API です。なので Bridge ですよと。 現代において C# では基本的に logger として Microsoft.Extensions.Logging を使うため、それと OpenTelemetry がうまく付き合うべく OpenTelemetry SDK では Microsoft.Extensions.Logging.ILogger integration が提供されているわけですが、その内部では Bridge API が活用される事となります。

各言語のサポート状況

なるほど、仕様はだいたい stable なんだな、と。 では実装の方はどうなんだ?という点についてはちゃんと公式ドキュメントに記載されています。 C# は全て Traces/Metrics/Logs その全てが stable ! 素晴らしい!

OpenTelemetry Collector

概要の次は OpenTelemetry を実際に使う上で極めて重要な OpenTelemetry Collector について見ていきましょう。

OpenTelemetry Collector はプロジェクトの主要なコンポーネントとして列挙されていたひとつでもあります。

  • The OpenTelemetry Collector, a proxy that receives, processes, and exports telemetry data

要するに OpenTelemetry Collector はテレメトリデータのプロキシであるわけですが、それがどの様に使われるのか既知でないと何の事やらという感じでしょう。 ということでここからは Collector の使われ方、具体的にはその有無で全体の構成がどうなるかを見ていきます。

OpenTelemetry Collector を使わない場合。

OpenTelemetry Collector を使わない場合、以下のような構成になります。

アプリケーション (図中の Application) には OpenTelemetry SDK を導入し、テレメトリデータを取ります。 そしてアプリケーションから生まれるテレメトリデータは、メトリクスやトレーシング等のそれぞれ用のバックエンド(図中の Backend)、例えば zipkin や jaeger にアプリケーションから直接 push したり、Prometheus 等に直接 pull されたりする事になります。 OpenTelemetry では OpenTelemetry Protocol (OTLP) というプロトコルが定められているのですが、Collector を用いない場合は OTLP が用いられるとは限らず、往々にして各バックエンド用のプロトコルを用いる事になります。 つまり、アプリケーションは結局 OpenTelemetry だけでなく、各バックエンドにも依存してしまう事になります。嫌ですね。 折角 OpenTelemetry を活用するのであれば、アプリケーションが依存するのはあくまで OpenTelemetry のみに留めておきたいところ。 また、利用したいバックエンドに対する C# 向けのパッケージ (Exporter) がなければ、自前で実装するか諦める他ないなどの問題もあります。

これでは最大限 OpenTelemetry の恩恵を享受できません。

OpenTelemetry Collector を使う場合。

では Collector を導入するとどうなるかというと、こんな感じになります。

あるいはこう。

どちらのケースでも重要なのは、アプリケーションから生まれたテレメトリデータは全て OTLP で Collector に送信されるという点です。 そして Collector が各バックエンド用のプロトコルに直し、それらを push したり pull されたりする事になります。 まさしくテレメトリデータのプロキシになっています。 こうする事で application が依存するのは OpenTelemetry だけになるので嬉しい。

また Collector より先のバックエンドとして何を使うかは様々な選択肢があるわけですが、Collector 向けの Exporter の実装さえあれば任意のバックエンドが使えるようになります。 バックエンド提供者としても、各言語向けの Exporter を実装して提供するより、一度 Collector 向けの Exporter を実装してしまえばあらゆる言語のアプリケーションでそのバックエンドが使えるようになりますから、やはり嬉しい。各言語向けに実装するより安定するハズなので利用者としても嬉しいでしょう。

そんなこんなでアプリケーション自体は各バックエンドから距離をとる事ができるようになりました。 これによりバックエンドの都合がアプリケーションに染み出してしまう事もなくなります。 また組織で開発しているとどうしても分業はする事になるので、このような分離がなされていた方が何かと動きやすいハズです。

また公式には以下のように Collector を使うメリットが記述されています

For trying out and getting started with OpenTelemetry, sending your data directly to a backend is a great way to get value quickly. Also, in a development or small-scale environment you can get decent results without a collector.

However, in general we recommend using a collector alongside your service, since it allows your service to offload data quickly and the collector can take care of additional handling like retries, batching, encryption or even sensitive data filtering.

というわけで Collector 無しでも OpenTelemetry は利用できますが、原則 Collector を挟んで置いた方がより OpenTelemetry の価値を引き出せるため良いでしょう。

Collector の実態とステータス

そんな Collector ですが、実態はなにかというと、様々なコンポーネントの塊です。

コンポーネントの種類を大きく分けると Receiver, Processer, Exporter, Extensions の四つ。 テレメトリデータの基本的な流れは Receiver で OTLP でテレメトリデータ受け取り、それを Processer でゴニョゴニョして、それを Exporter でどこかしかに吐き出すというシンプルなもの。

Collector は基本的に好きなコンポーネントを組み合わせて使うスタイルです。 分かりやすいのは Exporter で、例えば Prometheus や zipkin や jaeger など様々なバックエンドがありますが、それぞれ求めるプロトコルが異なるため、それぞれのバックエンド用のコンポーネントが存在します。 そしてそれらを自分で組み合わせて Collector を構築し、使いましょうねという感じです。

なお Collector のステータスが地味に厄介です。 前述の通り、Collector は様々なコンポーネントの集合体なわけですが、コンポーネントによってステータスが異なります。 現状おそらく、コンポーネントのステータスが一覧でまとまっているところは無く、GitHub でそれぞれのコンポーネントの ReadMe を逐次参照する他ありません。 現時点では OTLP の receiver/exporter の双方で log に関してはまだ beta ですし、他のコンポーネントも stable でないものが結構多い印象です。

お手軽な動かし方

Collector の手っ取早く動かすには、適当に config 書いて、docker でコンテナに立ち上げるのが良いでしょう。

config は以下のような形で。

receivers:
  otlp:
    protocols:
      grpc:

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"
  otlp:
    endpoint: tempo:4317
    tls:
      insecure: true
  debug:

processors:
  batch:

extensions:
  health_check:

service:
  extensions: [health_check]
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp, debug]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus, debug]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [loki, debug]

docker compose でコンテナ立ち上げるならこんな感じ。

  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.91.0
    command: ["--config=/etc/otel-collector.yml"]
    volumes:
      - ./containers/opentelemetry/otel-collector.yml:/etc/otel-collector.yml

公式から配布されている Collector の docker image としては otel/opentelemetry-collectorotel/opentelemetry-collector-contrib があります。 前者は比較的最小限のコンポーネントのみが含まれていますが、後者の image には open-telemetry/opentelemetry-collector-contrib で実装されているコンポーネントが含まれています。

なお、Collector は go lang で実装されているので、オレオレ processer とか書きたくなったら Go lang で頑張って実装してビルドする事になりますし、コンテナのサイズをミニマムにしようとしたらやっぱり自分でビルドする事になります。

OpenTelemetry の NuGet パッケージ

OpenTelemetry のパッケージはたくさんあるので、ここではほぼ必須で使うものと便利なものを紹介します。

ほぼ必須なパッケージ

アプリケーションに OpenTelemetry を導入する上で必要なパッケージは主に三種類で、SDK, Exporter, Instrumentation です。 もちろん Collector を用いるか否か、バックエンドに何を用いるか、何のテレメトリデータが欲しいか、等によって追加すべきパッケージは異なりますが、以下のパッケージはだいたい必須になるでしょう。

便利なパッケージ

以下によく使われて居そうな DB だったりの計装パッケージをのせます。 OpenTelemetry 用のパッケージは各ライブラリが自身でリリースしている他、open-telemetry/opentelemetry-dotnet-contrib にいろいろあるので何があるかは一度覗いておくと良いでしょう。

C#/ASP.NET Core での使い方

API は見慣れたいつものパターンなので、使い方自体は極めて簡単です。 まずは Logging から。

builder.logging.AddOpenTelemetry という、いつものスタイルでログに関する構成します。

var serviceName = "app-server";
var instanceId = $"{Environment.MachineName}-{Guid.NewGuid()}";

builder.Logging.AddOpenTelemetry(options =>
{
    options.IncludeScopes = true;
    options.IncludeFormattedMessage = true;
    options.ParseStateValues = true;

    options.SetResourceBuilder(ResourceBuilder.CreateDefault()
        .AddService(
            serviceName: serviceName,
            serviceInstanceId: instanceId
        ));

    // OTEL_EXPORTER_OTLP_ENDPOINT を環境変数に設定しておけばよしなに読み込んでくれる
    options.AddOtlpExporter();
});

途中 SetResourceBuilder というのが存在していますが、これは何か。 OpenTelemetry において Resource という言葉が何を意味するかはドキュメント上に記述されています。 ドキュメント上には以下のように記述されています

A Resource is an immutable representation of the entity producing telemetry as Attributes.

また文中で出てくる Attributes については以下のように記述されています

An Attribute is a key-value pair, (略)

要するに SetResourceBuilder ではテレメトリデータの発生源に関する情報の設定をここではやっているということです。

では次に Metrics 及び Tracing。

builder.Services.AddOpenTelemetry()
    .ConfigureResource(x =>
    {
        x.AddService(
            serviceName: serviceName,
            serviceInstanceId: instanceId
        );
    })
    .WithMetrics(providerBuilder =>
    {
        providerBuilder
            .AddRuntimeInstrumentation()
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddOtlpExporter();
    })
    .WithTracing(providerBuilder =>
    {
        providerBuilder
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddGrpcClientInstrumentation()
            .AddSignalRInstrumentation()
            .AddRedisInstrumentation()
            .AddNpgsql()
            .AddOtlpExporter();
    });

特段説明する事もないくらい分かりやすいですね。 強いて言うなら、追加したパッケージによって様々な AddXxxInstrumentation() という拡張メソッドが生えてくるので、それを WithMetrics 及び WithTracing 内で呼び出そうねというくらいでしょうか。

また、WithMetrics/WithTracing それぞれで AddOtlpExporter でエクスポーターを追加する必要があります。

ということで、C# のアプリケーションに OpenTelemetry を導入する事自体は簡単です。 独自の計装をする場合については今回触れていないのですが、実はそれもそこまで難しくはありません。

Example

OpenTelemetry がどんなものか分かった、C# での使い方も分かった。 となれば、動かしてみようと当然なります。 なるのですが、雑に動かしてみようにも OpenTelemetry Collector より先のバックエンドを構成するのが地味にメンドクサイ...! という事で、さくっと動かせる Example を用意しました

github.com

この example は以下のような構成になっています。

WebApi と書かれているのがユーザから叩かれるサービスだと思ってください。 WebApi リクエストを受け付けると、別のサービスである gRPC Service を叩きに行きます。 そして gRPC Service は PostgreSQL と Redis を叩きます。

なお WebApi と gRPC Service の中身は dotnet new で作成されるいつものテンプレートを少し弄っただけのものとなっています。

// WeatherForecastController.cs
    [HttpGet(Name = "GetWeatherForecast")]
    public async Task<ActionResult<IEnumerable<WeatherForecast>>> Get()
    {
        var res = await _greeterClient.SayHelloAsync(
            new HelloRequest() { Name = $"{Guid.NewGuid()}" },
            cancellationToken: this.HttpContext.RequestAborted
        );

        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
// GreeterService.cs
    public override async Task<HelloReply> SayHello(HelloRequest request, ServerCallContext context)
    {
        await using var connection = await _dbDataSource.OpenConnectionAsync(context.CancellationToken);

        // 適当なトレースのために
        await connection.QueryAsync("select 1");

        await _connectionMultiplexer.GetDatabase(0)
            .StringSetAsync(Guid.NewGuid().ToByteArray(), Guid.NewGuid().ToByteArray());

        return new HelloReply
        {
            Message = "Hello " + request.Name
        };
    }

WebApi と gRPC Service には OpenTelemetry SDK (パッケージとしては OpenTelemetry.Extensions.Hosting)が追加されており、SDK は収集したテレメトリデータを OpenTelemetry Collector に OTLP で投げます。 Collector はメトリクスであれば Prometheus、トレースであれば tempo、ログであれば loki にテレメトリデータを投げます (正確には Prometheus は Collector からテレメトリデータを pull します)。 最終的にそれらは Grafana で可視化、といった感じの構成です。

ここからは実際に生まれたテレメトリデータがどのように可視化されるか Grafana 上でみていきます。 まずはメトリクスについてみていきましょう。

メトリクスについては Prometheus に集約されていますから、Grafana で Exploer > Prometheus を選択すれば様々な情報がを見る事ができます。

一つ一つのメトリックについてはそのように確認できますが、ここではより便利なものを使いましょう。 .NET Conf 2023 にて華々しく登場した Aspire の中には、Grafana 上で metrics をみるための dashboard の設定が存在するのでそれを拝借してきましょう。 Aspire の中には二つの dashboard の設定があります。一つがアプリケーション全体のダッシュボード、もう一つがエンドポイントレベルのダッシュボードです。

アプリケーション全体のダッシュボードでは、コネクション数や合計のリクエスト数、未処理の例外がどれだけ発生したか等のメトリクスがいい感じに見えるようになっています。

エンドポイントレベルのダッシュボードでは、可視化したいエンドポイントを選択すると、以下のようにレスポンスのステータスコードや、実際に throw された未処理の例外の型等が見えるようになっています。 いい感じですね。

次にトレースを見ましょう。 Explore > tempo を選択すると以下のような画面がでてきます。 この画面で様々な検索を条件をいれて検索すると、下に該当のトレースの一覧が出てきます。 なお、トレースやスパンといった言葉が分からない場合は 【C#】ASP.NET Core と W3C Trace Context とお手軽ロギング。 をご覧ください。

画面上の Trace ID をクリックすると、以下のようなウォーターフォールチャートが表示されます。

すこしみてみましょう。 一番上のスパンから、my-app-server (全体の構成図における WebApi に該当) で HTTP の GET にて WetherForcast が叩かれている事がわかります。 その中では gRPC で、greet.Greeter/SayHallo を呼ばれ、またその内側で HTTP POST が行われた事がスパンから分かります(gRPC の中は HTTP/2 なので)。 そして別のサービスである grpc service で greet.Greeter/SayHallo が呼ばれ、その中では PostgreSQL と Redis が叩かれた事が読み取れます。 これでトレースの一連の流れが追えるようになりましたね。めでたしめでたし。

しかしトレースだけでは各サービスが内部でどのように振る舞っているかの詳細は分からないので、ログが見たい。 そんな時は各スパンをクリックするとよりスパンの詳細な情報と共に、Logs for this span というボタンが現れるのでそれをクリックしたり、各スパンにひっそりと存在する Log のボタンをクリックしましょう。

すると以下のような画面に飛びます。 ここでは先ほどのトレースに紐づいたログを抽出して表示してくれています。便利ですね。 この example では TraceId でログを引いていますが、設定次第では、SpanId でログを引く事もできます。

SignalR + OpenTelemetry

今まで普通の HTTP request とか gRPC の計装についてお話しました。 では SignalR は?というが気になるところでしょう。気になりますよね...!?

SignalR ではトランスポート層として幾つかの接続方法が選べますが、ここでは WebSocket を用いたと仮定してましょう。 SignalR の Hub method の呼び出しは SignalR のプロトコルの世界の話です。 そのため当然ながら HTTP の inbound の計装のみをもとにトレースすることは不可能です。 HTTP の inbound の計装から分かる事は WebSocket のコネクションの確立と切断のみです。

ですが、SignalR においてスパンは Hub method の呼び出し毎に切れていて欲しいのです。 そうでないと、各メソッドの振る舞いが全く分かりませんし、どのメソッドが重たいのかとかも分かりません。

この事は実際に動かしてみてトレースのウォーターフォールチャートを見ると分かりやすいです。 たとえば以下のような SignalR の Hub があったとしましょう。

public class ExampleHub : Hub<IExampleHubReceiver>, IExampleHub
{
    private readonly DbDataSource _dbDataSource;

    public ExampleHub(DbDataSource dbDataSource)
    {
        _dbDataSource = dbDataSource;
    }

    public async Task<string> PostMessage1(string message)
    {
        await using var connection = await _dbDataSource.OpenConnectionAsync(this.Context.ConnectionAborted);

        await connection.QueryAsync("select 1");

        return message;
    }

    public async Task<string> PostMessage2(string message)
    {
        await using var connection = await _dbDataSource.OpenConnectionAsync(this.Context.ConnectionAborted);

        await connection.QueryAsync("select 1");

        return message;
    }
}

そしてこの Hub を適当に叩いてテレメトリデータを作成し、Grafana でみてみると以下のようなウォーターフォールチャートが現れます。

WebSocket の接続内で DB が叩かれていることは分かるのですが、それ以上の事はなにもわかりません。 前述の通り、これでは各メソッドの振る舞いが全く分からないですし、どのメソッドが重たいのか等も分からりません。

これの問題は、Hub のメソッド単位でスパンが切られていない ということです。 という事で、この問題を解決するべく AspNetCore.SignalR.OpenTelemetry という新しいライブラリをリリースしました。

github.com

このライブラリが提供している機能は主に二つあり、一つがトレースの計装、もう一つが最低限のログの提供です。 トレースのため計装というのは、上の例のような問題を解決するために、Hub method の呼び出し事にスパンが切れていなかったのを切るようにしましたよという事。

ログの方は SignalR を使う上でほぼ必須な以下のようなログを提供します。

  • 接続時
    • Transport 層の情報も出力(WebSocket/SSE 等)
  • メソッド呼び出し時
    • HubName.MethodName
    • Duration
    • メソッド呼び出し毎にログのスコープ
      • HubName, MethodName, InvocationId を振っているのでログの検索性が向上
  • 切断時
    • 切断時に例外が発生していれば例外もログに出力

このように、イベントが起きたことをログに出すだけではなく、殆どの場合で欲しくなる情報も付与して出力するようにしています。

このライブラリの使い方は簡単で、AspNetCore.SignalR.OpenTelemetry が提供している AddHubInstrumentation という拡張メソッドを AddSignalR の後に追加し、 WithTracing() 内で AddSignalRInstrumentation() を呼んであげればいいだけです。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSignalR()
    .AddHubInstrumentation(); // <- 追加 !

// 中略

builder.Services.AddOpenTelemetry()
    .ConfigureResource(/** 略 **/)
    .WithTracing(providerBuilder =>
    {
        providerBuilder
            .AddAspNetCoreInstrumentation()
            .AddHttpClientInstrumentation()
            .AddGrpcClientInstrumentation()
            .AddSignalRInstrumentation() // <- 追加!
            .AddOtlpExporter();
    });

このような構成をした上で、先ほどと同様に SignalR の Hub を叩き、テレメトリデータを生成するとどうなるか。 以下のようなウォーターフォールチャートが得られます。

ExampleHub/PostMessage1 とか ExampleHub/PostMessage2 等の文字列が現れている事が分かるでしょうか。 このように、このライブラリを追加する事で、Hub method 呼び出し事にしっかりと Span が切られるようになりました。 これにて各メソッドの振る舞いが分かるようになりました。

おまけ

完全におまけ程度ですが、なんとなく上記のウォーターフォールチャートではビジュアル上の寂しさがあるので、以下のような Hub を用意し、適当に Hub を叩いてテレメトリデータを生成し、Grafana で見てみる事としましょう。

public class ExampleHub : Hub<IExampleHubReceiver>, IExampleHub
{
    private readonly Greeter.GreeterClient _greeterClient;
    private readonly DbDataSource _dbDataSource;

    public ExampleHub(Greeter.GreeterClient greeterClient, DbDataSource dbDataSource)
    {
        _greeterClient = greeterClient;
        _dbDataSource = dbDataSource;
    }

    public async Task<string> EnterRoom(string name)
    {
        var res = await _greeterClient.SayHelloAsync(
            new HelloRequest { Name = name },
            cancellationToken: this.Context.ConnectionAborted
        );

        return res.Message;
    }

    public async Task<string> PostMessage(string message)
    {
        await using var connection = await _dbDataSource.OpenConnectionAsync(this.Context.ConnectionAborted);

        await connection.QueryAsync("select 1");

        return message;
    }
}

こうする事で、DB 叩くだけよりは寂しくないウォーターフォールチャートが得られます。デモ的にはビジュアル大事ですからね(?)

まとめ

2023 年は OpenTelemetry の Tracing/Metrics/Logging それぞれの主要な仕様が全て stable となる年でした。 そして C# SDK のサポート状況は 2023/12/23 時点で全て stable となっています。 またご覧いただいたように C# で OpenTelemetry を利用するのはそれほど難しい事ではありませんし、.NET Conf 2023 で登場した Aspire も OpenTelemetry を活用していたりと、どんどん OpenTelemetry の機運は高まっています。

ということで C# は OpenTelemetry Ready です!活用していきましょう!