銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

OpenTelemetry .NET ハンズオンを行いました

C# Tokyoでの企画でOpenTelemetryの初心者向けハンズオンを行いました。

csharp-tokyo.connpass.com

OpenTelemetry .NETについての発表資料はこちらです。

www.slideshare.net

ハンズオン課題はこちらのリポジトリのhandson1フォルダに配置しています。

github.com

当日参加者の方にはすぐに使えるNew Relicのアカウントを提供しましたが、もし試す場合はこちらの記事を参考にNew Relicの無償アカウントをセットアップするか、別のバックエンドに送信できます。

tech.tanaka733.net

OpenTelemetry .NETを理解する (2) ASP.NET CoreにOpenTelemetryをまずいれてみる

今回からは実際にOpenTelemetry .NETを使って計測してみることにします。まずは、ASP.NET Core (.NET 6)を題材にします。

OpenTelemetryのドキュメントと重なる部分も多いのであわせて参照してみてください。

opentelemetry.io

最初の計装

まずは、計測対象のASP.NET Coreプロジェクトを作ります。dotnet new web で作れますが、.NET 6だと1ファイルで完結する形でかんたんなWebアプリが作れるので以下のコードをスタートにします。

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
    
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

var summaries = new[]
{
    "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};

app.MapGet("/weatherforecast", () =>
{
    var forecast = Enumerable.Range(1, 5).Select(index =>
       new WeatherForecast
       (
           DateTime.Now.AddDays(index),
           Random.Shared.Next(-20, 55),
           summaries[Random.Shared.Next(summaries.Length)]
       ))
        .ToArray();
    return forecast;
})
.WithName("GetWeatherForecast");

app.Run();

internal record WeatherForecast(DateTime Date, int TemperatureC, string? Summary)
{
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

ここから計測コードを実装する計装作業を行います。まず、必要なライブラリはNuGetで提供されているので、以下の2つをまず追加します。

  <ItemGroup>
    <PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.0.0-rc8" />
    <PackageReference Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.0.0-rc8" />
  </ItemGroup>

またこれらのパッケージは、OpenTelemetryOpenTelemetry.Apiに依存しています。この2つは計装のためのOpenのAPIとSDKの言語ごとの実装であり、計装するために最低限必要なライブラリという位置づけになっています。APIとSDKについては、ライブラリ開発者がOpenTelemetry .NETを使うユースケースで説明しようと思っています。

OpenTelemetry.Extensions.HostingとOpenTelemetry.Instrumentation.AspNetCoreは計装の手間をへらすために、よく使われるライブラリ(ここではASP.NET Core)を計装する処理を提供します。前回の記事の自動計装(automatic instrumentation)と呼ばれる機能です。APIとSDKは安定版ですが、自動計装部分についてはRC版となっています。

実際の計装コードは次のように記述します。先程のコードの先頭部分に次のように追加します。

using OpenTelemetry.Resources;
using OpenTelemetry.Trace;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

var tags = new Dictionary<string, object>
        {
            { "Environment", "Production" },
            { "Level", 99 },
        };

builder.Services.AddOpenTelemetryTracing((builder) => builder
        .AddAspNetCoreInstrumentation()
        .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Hello-Otel").AddAttributes(tags))
    );

SetResourceBuilderメソッドの中の「Hello-Otel」はこのアプリケーションを識別するための名前をつけます。またアプリケーションを分類するためのタグを追加しています。これだけでASP.NET CoreアプリケーションをOpenTelemetryの標準的な機能で計測することができます。ただ、これだけだと計測したデータをどこにも送信していません。まずはコンソールに出力して動作確認することにします。

ConsoleExporterの追加

Exporterは出力先ごとにNuGetライブラリが提供されています。Consoleに出力する場合、以下のライブラリを追加します。

  <ItemGroup>
    <PackageReference Include="OpenTelemetry.Exporter.Console" Version="1.1.0" />
  </ItemGroup>

Exporterを設定するのは、先程のbuilder.Services.AddOpenTelemetryTracingメソッドの中で、次のように設定します。

builder.Services.AddOpenTelemetryTracing((builder) => builder
        .AddConsoleExporter()
        .AddAspNetCoreInstrumentation()
        .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Hello-Otel").AddAttributes(tags))
    );

これだけで完了です。実際に実行してエンドポイントにアクセスすると次のようなデータがコンソールに出力されるはずです。

Activity.Id:          00-016a34b6053f5d51737b3419a41e4604-c08de677265e1e72-01
Activity.ActivitySourceName: OpenTelemetry.Instrumentation.AspNetCore
Activity.DisplayName: WeatherForecast
Activity.Kind:        Server
Activity.StartTime:   2022-01-06T06:33:00.9914510Z
Activity.Duration:    00:00:00.0496200
Activity.TagObjects:
    http.host: localhost:7174
    http.method: GET
    http.target: /WeatherForecast
    http.url: https://localhost:7174/WeatherForecast
    http.user_agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
    http.route: WeatherForecast
    http.status_code: 200
    otel.status_code: UNSET
Resource associated with Activity:
    Environment: Production
    Level: 99
    service.name: Hello-Otel
    service.instance.id: 509603d5-39e6-4395-9c71-884e27f61072

実行された処理がトレースとして計測されたものです。トレースを識別するためのIdや名前、時刻、経過時間、タグなどが記録されていることがわかります。また、「Resource associated with Activity」ではSetResourceBuilderで設定したアプリの名前やタグも確認できます。

OTLPExporterでNew Relicに送信する

ほとんどのケースではコンソールではなく、別のどこかに送信し、最終的には記録・利用のためのオブザーバビリティバックエンドに送信することになります。前回の記事の「OpenTelemetery で計測するために必要なもの」の図ではコレクターを経由して送信しています。実用的にはコレクターを経由するケースが多いと思うのですが、ひとまず検証するという意味ではアプリケーションのOpenTelemetryコードから直接オブザーバビリティバックエンドに送信することができます。その場合、バックエンドがOpenTelemetry .NET向けにExporterライブラリを提供していれば利用できます。特に、OTLP形式のエンドポイントをサポートしているバックエンドであればOpenTelemetry .NETが提供しているOTLPExporterが利用できます。そして、New RelicはOTLP形式のエンドポイントをサポートしているので試しに利用できます。

New Relic側の準備については公式ブログを参照してください。具体的にはライセンスキーとエンドポイントをメモしておいてください。

newrelic.com

それは実装してみます。まずはOTLPExporterのライブラリを追加します。先程のConsoleExpoterは不要ですが、残しておいても問題はありません。

  <ItemGroup>
    <PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.1.0" />
  </ItemGroup>

AddConsoleExporterの代わりにAddOtlpExporterを呼びます。そして、以下のようにエンドポイントと認証のためのヘッダーを設定します。ここではエンドポイントをOTEL_ENDPOINTという名前の、認証ヘッダーのキーがapi-keyで値がOTEL_API_KEYという名前の、環境変数に設定しています。

builder.Services.AddOpenTelemetryTracing((builder) => builder
        .AddAspNetCoreInstrumentation()
        .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("Hello-Otel").AddAttributes(tags))
        .AddOtlpExporter(options => 
        {
            options.Endpoint = new Uri(Environment.GetEnvironmentVariable("OTEL_ENDPOINT"));
            options.Headers = $"api-key={Environment.GetEnvironmentVariable("OTEL_API_KEY")}";
        })
    );

エンドポイントや認証のために利用するヘッダーのキーの名前はOTLPをサポートしているバックエンドごとに提供されています。なのでほぼ同じ書き方で設定できるはずです。また、その他のExpoterも設定はこのような書き方でできるものが多いです。これでOTLPフォーマットで送信する設定は完了です。再度アプリケーションを起動し、アクセスすると送信されるはずです。

送信されたデータをどのように表示するかはオブザーバビリティバックエンドの機能に依存します。New Relicの場合、送信するだけでアプリケーションごとに以下のようなダッシュボードに表示されます。

f:id:tanaka733:20220119225731p:plain

また、先程コンソールに出力したようなトレースの詳細は分散トレース(Distributed Tracing)のUIで確認できます。

f:id:tanaka733:20220119231437p:plain

このようにOpenTelemetry .NETはよく使われるフレームワークであれば、まず使い始めるのは非常にかんたんになっています。

Azure Chaos Studio を試してみる サービス直接ターゲット編

f:id:tanaka733:20220118233433p:plain 先日、カオスエンジニアリングをマネージドサービスとして提供するAzure Chaos Studioが発表され、プレビューとして利用できるようになっています。

azure.microsoft.com

docs.microsoft.com

せっかくなので試してみようと思い、いろいろやった結果をまとめておきます。

Chaos Studioの概念

ドキュメントでいうとここからのセクションに説明されています。

docs.microsoft.com

一番大きな単位が「カオス実験」で、どんな障害をどんなリソースに実行するかが記述されています。 どんな障害を「ロジック」として定義し、どんなリソースにを「セレクター」として定義します。この2つがカオス実験を構成する単位です。 ロジックは1つずつ逐次実行する「ステップ」から構成され、ステップは同時に実行される「分岐(Branch)」から構成されます。 分岐は対象となるリソースをさらに定義するセレクターと、なにを実行するかの「アクション」で構成されます。 アクションは、障害もしくは待機の2種類があります。 これらの組み合わせで1つの実験を定義し、実行することになります。

現在利用できる障害は以下の3種類にわけられるようです。

  • サービス直接ターゲット: Azureのサービスに直接作用する
  • エージェントベース: OSにインストールしたエージェントが作用する
  • Chaos Mesh: AKSクラスター上のkubernetesリソースにChaos Meshを利用して作用する

まず、サービス直接ターゲットを使ったものを試してみました。

CosmosDBにサービス直接ターゲットを設定して障害を起こす

試したアクションはCosmosDBをFailoverさせるものです。基本的にはこのドキュメントに沿って設定すればOKです。が、いくつかはまったところがあるのと、実は最終的に実験の結果がFailedになってしまい未解決です。

docs.microsoft.com

まずはCosmosDBを用意します。Failoverさせることから、読み取りリージョンを1つ以上追加しておかないとあとで設定できなくなります。 f:id:tanaka733:20220118232721p:plain f:id:tanaka733:20220118191744p:plain

空だとさみしいので、コンテナとアイテムを用意しておきました。実験を実行するだけの場合、なくてもかまいません。

f:id:tanaka733:20220118185203p:plain

まず、Azure Chaos Studioのページから対象となるリソースを選択します。そして、大事なことですが、ポータルの言語を英語にしておきます。Previewのせいなのか、日本語だとロジックがうまく実行できませんでした。最初日本語で作成していたので画面キャプチャは日本語になっています。

f:id:tanaka733:20220118185259p:plain

実験を作成します。このときのリソースグループとリージョンは対象と一致していなくてもよいはずですが、今回は同じものを選んでいます。

f:id:tanaka733:20220118185748p:plain

実験を作成したら、ロジックを作成します。ステップとブランチ1つの中に、障害アクションを1つ作成します。

f:id:tanaka733:20220118185839p:plain

翻訳の表記ゆれをしていますが、フォールトの選択で障害の種類を選びます。CosmosDB Failverを選びます。

f:id:tanaka733:20220118185903p:plain

どのリージョンにFailoverさせるかを選択します。ここで注意しないといけないのは、Failover先は対象のCosmosDBの読み取りリージョンとして追加されたものでないといけません。そして、ポータルの言語が日本語のときの問題はここで発生しました。例えばここで「英国西部」を選択していますが、すると「英国西部」という値がアクションに設定されてしまい、実験の実行時に「英国西部」なんていうリージョンはないとエラーになります。正しくは「UK West」ですが、これを設定するためにはポータルを英語表示にする必要がありました*1

f:id:tanaka733:20220118192036p:plain

どのCosmosDBを対象にするかを選びます。

f:id:tanaka733:20220118190102p:plain

これで実験は作成できました。この後、セキュリティーのため、対象となるCosmosDBにこの実験を操作してもよいという許可設定をする必要があります。カオス実験はマネージドIDを利用しているので、CosmosDBのIAMの設定でロールを追加します。「Cosmos DB 演算子」というロールを選びます。Cosmos DB Operatorの訳のようです。

f:id:tanaka733:20220118190416p:plain

マネージドIDを選びます。カオス実験につけた名前でIDが作られています。

f:id:tanaka733:20220118190507p:plain

実験の実行はChaos Studioのページから行います。実行履歴も確認できます。

f:id:tanaka733:20220118233010p:plain

実行すると、実際にフェールオーバーが起きたことを確認できます。

f:id:tanaka733:20220118193006p:plain

10分経過後、フェールバックして元のリージョンに戻ったところまで確認できたのですが、なぜか実験はFailedになっており、詳細メッセージもありません。

f:id:tanaka733:20220118233149p:plain

まだプレビューということもあり、動作が不安定なのでフィードバックしています。次回はエージェント方式の障害を試してみようと思います。

*1:実際に設定されている内容はJSONビューで確認できます。