業務アプリの調子を知りたいとき、ログはサーバーやPCごとのファイルに散らばり、どの処理に時間がかかったのかは記録を突き合わせないと分かりません。監視サービスを入れようとすると、そのサービス専用のSDKをアプリに組み込むことになり、あとから別のサービスへ移るのが難しくなります。
この記事では .NET の業務アプリのログ、メトリクス、トレースを、OpenTelemetryで集める方法をまとめます。答えの要点は3つです。.NETにはログ、メトリクス、トレースを記録するAPIが最初から入っているので、アプリのコードはそのAPIで計測します。OpenTelemetryのSDKがそこからデータを集め、OTLPというベンダーに依存しない形式で送ります。送り先は設定で決めるので、監視サービスを替えてもアプリのコードは書き直さずに済みます。
対象は .NET 10(現在のLTS)で動くWindowsサービス、ASP.NET Core、Windowsフォーム・WPFの常駐アプリです。.NET 8 と .NET 9 のサポートは2026年11月10日に終わります(「.NET 8と.NET 9のサポート終了に備える」)。仕様とパッケージの状態は2026年11月8日にOpenTelemetryとMicrosoftの資料で確かめました。C# の例は GitHub Actions の Windows Server 2025 のランナーで、.NET 10 でビルドしました。Windows サービスの例は、手元に立てた Collector(v0.165.0)にログとメトリクスが届くことと、HttpClient のタイムアウトで取り込みの処理から例外が外に出ないことを確かめました。Collector の設定例は v0.165.0 の検証で通り、Aspire ダッシュボードは Linux の Docker で起動して、gRPC と HTTP の両方で送れることを確かめました。Windows フォームの例はビルドだけで、画面を動かしてはいません。
ログ・メトリクス・トレースの役割
Microsoftの資料はログ、メトリクス、分散トレースを組み合わせて使うものとして説明しています1。ログは受注の取り込みの失敗のような個々の出来事を記録します。メトリクスは完了した処理の件数や処理時間の分布のような数値を、カウンターやヒストグラムで集計します。分散トレースは1つの要求が複数の部品をまたいでどう進んだかを追い、どこで時間を使い、どこで失敗したかを示します。
業務アプリに当てはめると、次のように使い分けます。「今朝の取り込みはなぜ失敗したか」はログで調べます。「取り込みの件数と時間は先週と比べてどうか」はメトリクスで見ます。「この1件の処理は、外部のAPI、データベース、ファイルの書き込みのどこで待たされたか」はトレースで追います。
flowchart TB
accTitle: ログ・メトリクス・トレースが答える問い
accDescr: ログは個々の出来事を記録して何が起きたかに答え、メトリクスは件数や処理時間を集計して傾向に答え、トレースは1つの処理が部品をまたいでどこで時間を使ったかに答えることを示す。
APP["業務アプリ"] --> LOG["ログ"]
APP --> MET["メトリクス"]
APP --> TRC["トレース"]
LOG --> Q1["何が起きたか"]
MET --> Q2["件数と時間の傾向"]
TRC --> Q3["どこで時間を使ったか"]
図1: ログは出来事、メトリクスは傾向、トレースは1件の処理の流れを記録する。
3種類のデータは同じ処理について別の面を記録します。ログにはトレースの識別子が付くので、保存先が対応していれば、遅かった1件のトレースから、その処理中に出たログへたどれます。
図の実線は常に成り立つ関係、破線は条件付きの関係です(成立条件は詳細ページの各関係の説明に記載)。関係すべての一覧(全7件、根拠・確度つき)と主要概念の定義は知識マップ詳細ページにまとめています。データ: JSON-LD / Turtle
.NET に最初からある計測の API
.NETには、ログ、メトリクス、トレースを記録するAPIがフレームワークに入っています。OpenTelemetryの.NET実装では計測のために独自のAPIを使う必要がなく、次の.NETのAPIから記録を集めます1。
- ログ:
Microsoft.Extensions.LoggingのILogger<T> - メトリクス:
System.Diagnostics.Metrics.Meter - トレース:
System.Diagnostics.ActivitySourceとSystem.Diagnostics.Activity
OpenTelemetryの用語ではトレースを作る部品をTracer、処理の1区間をSpanと呼びます。.NETでは ActivitySource がTracerに、Activity がSpanに当たります。Activity はOpenTelemetryの仕様より前からあった型なので、.NETの名前のまま残されています2。
flowchart TB
accTitle: .NETの計測APIとOpenTelemetryの関係
accDescr: アプリのコードとライブラリは.NETのILogger、Meter、ActivitySourceで計測し、OpenTelemetryのSDKがそれらからデータを集めてエクスポーターで外へ送ることを示す。
CODE["アプリのコードとライブラリ"] --> API[".NETの計測API"]
API --> SDK["OpenTelemetryのSDK"]
SDK --> EXP["エクスポーター"]
EXP --> DEST["保存先"]
図2: コードは.NETのAPIで計測し、集めて送る部分だけをOpenTelemetryが受け持つ。
この分担のおかげで、ライブラリの作者は.NETのAPIだけを使えばよく、アプリの開発者が集め方と送り先を選べます2。アプリのコードにOpenTelemetryの型が出てくるのは、起動時の設定の部分だけです。
.NET自身もこのAPIで計測しています。HttpClient は .NET 8 から System.Net.Http という名前のMeterで、要求の時間や接続の数を記録します3。.NET 9 からはGC、スレッドプール、例外などの値を System.Runtime というMeterで記録します4。ILogger の使い方は「Windowsイベントログ・ETW入門」で、ホストの仕組みは「.NET Generic Hostとは」で説明しています。
OpenTelemetry が受け持つもの
OpenTelemetryは計測データを集めて送るための、言語やプラットフォームをまたぐオープンな標準です1。データの名前と中身の付け方を決めたセマンティック規約、保存先ごとの形式で送るエクスポーター、ベンダーに依存しない送信の形式であるOTLPを含みます。
仕様では、トレース、メトリクス、ログのどれもOTLPの部分が安定版です5。.NETの実装も、トレース、メトリクス、ログのそれぞれが安定版とされています6。
OTLPの送り方には、gRPCとHTTP(protobuf)があります。.NETのOTLPエクスポーターの既定はgRPCで、送り先の既定は、gRPCなら localhost:4317、HTTPなら localhost:4318 です7。.NET Framework と .NET Standard 2.0 向けのビルドでは、既定がHTTPになります8。
flowchart TB
accTitle: OTLPで送ったデータの行き先
accDescr: 業務アプリはOTLPでデータを送り、送り先はCollectorでも保存先でもよく、Collectorを置くとそこから複数の保存先へ送り分けられることを示す。
APP["業務アプリ"] -->|"OTLP"| COL["Collector"]
APP -.->|"OTLP 直接"| B1["保存先A"]
COL --> B1
COL --> B2["保存先B"]
図3: アプリはOTLPで送るだけで、行き先は設定で決まる。
OpenTelemetryを使うと、Prometheus・Grafanaのようなオープンソースの製品、MicrosoftのAzure Monitor、OpenTelemetryに対応した各社の監視サービスのどれにでも送れます1。アプリは監視サービス専用のAPIやデータ構造を使わずに済むので、送り先を替えるときに変えるのは設定だけです。
Windows サービスに組み込む
夜間に取引先のAPIから受注を取り込むWindowsサービスを例にします。Windowsサービスの作り方そのものは「Windowsサービスの作り方と運用」にまとめています。
パッケージを入れる
次のパッケージを使います。OpenTelemetryの3つ9と、Windowsサービスとして動かすための Microsoft.Extensions.Hosting.WindowsServices です10。
dotnet add package OpenTelemetry.Extensions.Hosting
dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol
dotnet add package OpenTelemetry.Instrumentation.Http
dotnet add package Microsoft.Extensions.Hosting.WindowsServices
OpenTelemetry.Extensions.Hosting はGeneric Hostの起動と終了に合わせて、トレースとメトリクスの仕組みを始めたり止めたりします11。OpenTelemetry.Exporter.OpenTelemetryProtocol がOTLPで送るエクスポーターです。OpenTelemetry.Instrumentation.Http は HttpClient の通信を記録するパッケージで、READMEでは安定版とされています12。Microsoft.Extensions.Hosting.WindowsServices は、次の例で使う AddWindowsService を提供します10。
Microsoftの手順の記事は1.19系を使っています。執筆時点でNuGetにある最新の安定版は、SDK、OpenTelemetry.Extensions.Hosting、OTLPエクスポーターが1.20.1、OpenTelemetry.Instrumentation.Http が1.20.2でした13。手順の記事は、APIが変わり続けているので最新のバージョンを使うよう勧めています9。
計測の部品を1つのクラスにまとめる
業務の処理を記録する ActivitySource と Meter は、1つのクラスにまとめておくと、名前を間違えずに設定と計測の両方から参照できます。
using System.Diagnostics;
using System.Diagnostics.Metrics;
public sealed class OrderImportTelemetry
{
public const string SourceName = "Komura.OrderImport";
public const string MeterName = "Komura.OrderImport";
public static readonly ActivitySource Source = new(SourceName, "1.0.0");
public Counter<long> ImportedOrders { get; }
public Histogram<double> ImportDuration { get; }
public OrderImportTelemetry(IMeterFactory meterFactory)
{
var meter = meterFactory.Create(MeterName);
ImportedOrders = meter.CreateCounter<long>(
"komura.order_import.orders", unit: "{order}",
description: "取り込んだ受注の件数");
ImportDuration = meter.CreateHistogram<double>(
"komura.order_import.duration", unit: "s",
description: "1回の取り込みにかかった時間",
advice: new InstrumentAdvice<double>
{
HistogramBucketBoundaries = [0.5, 1, 2, 5, 10, 30, 60, 120, 300]
});
}
}
Microsoftは ActivitySource を一度だけ作って静的な変数に入れ、使い続けるよう勧めています2。Meter はDIを使うアプリでは IMeterFactory から作ります。.NET 8 からは、ホストが IMeterFactory を自動でサービスに登録します14。名前はアセンブリ名や名前空間のような、ドットで区切った階層の名前にします。時間は秒で、double で記録します14。
ヒストグラムは記録した値を区切りごとの区間に数えます。OpenTelemetryの既定の区切りは 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 で、秒で記録すると5秒以下の取り込みはすべて同じ区間に入ります14。上のコードでは InstrumentAdvice で秒に合った区切りを勧めています。InstrumentAdvice は System.Diagnostics.DiagnosticSource の9.0.0から使え、OpenTelemetryのSDKは1.10.0からこの指定に従います。SDKの側で区切りを決めたいときは、AddView と ExplicitBucketHistogramConfiguration を使います15。
flowchart TB
accTitle: ヒストグラムの区切りと取り込み時間
accDescr: 取り込み時間を秒で記録すると、既定の区切りでは5秒以下の値がすべて同じ区間に入り、InstrumentAdviceで秒に合った区切りを勧めると値が複数の区間に分かれることを示す。
REC["取り込み時間を秒で記録"] --> DEF["既定の区切り"]
REC --> ADV["InstrumentAdvice の区切り"]
DEF --> ONE["5秒以下が1つの区間"]
ADV --> SPLIT["秒に合った区間に分かれる"]
図4: 既定の区切りのままでは、数秒かかる取り込みの分布が見えない。
起動時に送り先を設定する
Program.cs ではOpenTelemetryに何を集め、どこへ送るかを設定します。
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddWindowsService(options => options.ServiceName = "OrderImport");
builder.Services.AddSingleton<OrderImportTelemetry>();
builder.Services.AddSingleton(new HttpClient
{
BaseAddress = new Uri("https://partner.example.com/")
});
builder.Services.AddHostedService<OrderImportWorker>();
builder.Services.AddOpenTelemetry()
.ConfigureResource(resource => resource.AddService("order-import"))
.WithTracing(tracing => tracing
.AddSource(OrderImportTelemetry.SourceName)
.AddHttpClientInstrumentation())
.WithMetrics(metrics => metrics
.AddMeter(OrderImportTelemetry.MeterName)
.AddHttpClientInstrumentation()
.AddMeter("System.Runtime"))
.UseOtlpExporter();
builder.Build().Run();
ConfigureResource の AddService で、データを出したアプリの名前を付けます。WithTracing の AddSource と WithMetrics の AddMeter には、記録を受け取る ActivitySource と Meter の名前を指定します。名前を登録していない ActivitySource と Meter の記録は集められません1615。
UseOtlpExporter はログ、メトリクス、トレースの3つをまとめてOTLPで送る設定です。ログ(ILogger)を送る設定もこの呼び出しで有効になります。UseOtlpExporter は一度しか呼べず、トレースやメトリクスごとの AddOtlpExporter と一緒には使えません7。
flowchart TB
accTitle: 起動時のOpenTelemetryの設定の流れ
accDescr: AddOpenTelemetryで設定を始め、ConfigureResourceでアプリの名前を付け、WithTracingとWithMetricsで受け取る記録の名前を登録し、UseOtlpExporterでログ・メトリクス・トレースをOTLPで送ることを示す。
ADD["AddOpenTelemetry"] --> RES["アプリの名前を付ける"]
RES --> TR["トレースの名前を登録"]
RES --> ME["メトリクスの名前を登録"]
TR --> OTLP["UseOtlpExporter"]
ME --> OTLP
OTLP --> OUT["3種類をOTLPで送る"]
図5: 受け取る記録の名前を登録し、最後にOTLPの送り先をまとめて設定する。
HttpClient は .NET 9 から、トレースの記録を自分で出すようになりました。それでも AddHttpClientInstrumentation は、OpenTelemetryのSDKで相手のサービスへトレースの識別子を渡すために必要だとREADMEは説明しています12。HttpClient の正しい使い方は「HttpClientをusingで囲んではいけない」を参照してください。
業務の処理に計測を入れる
取り込みの処理では1回の取り込みを Activity で囲み、件数と時間をメトリクスに記録し、結果を ILogger で書きます。
using System.Diagnostics;
using System.Net.Http.Json;
public sealed record OrderDto(string OrderNo);
public sealed class OrderImportWorker(
OrderImportTelemetry telemetry,
HttpClient client,
ILogger<OrderImportWorker> logger) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(10));
while (await timer.WaitForNextTickAsync(stoppingToken))
{
await ImportOnceAsync(stoppingToken);
}
}
private async Task ImportOnceAsync(CancellationToken cancellationToken)
{
using var activity = OrderImportTelemetry.Source.StartActivity("ImportOrders");
var started = Stopwatch.GetTimestamp();
try
{
var orders = await client.GetFromJsonAsync<List<OrderDto>>(
"orders/new", cancellationToken) ?? [];
// ... 受注をデータベースへ保存する ...
telemetry.ImportedOrders.Add(orders.Count,
new KeyValuePair<string, object?>("komura.partner", "partner-a"));
activity?.SetTag("komura.order_count", orders.Count);
logger.LogInformation("Imported {OrderCount} orders from {Partner}",
orders.Count, "partner-a");
}
catch (Exception ex) when (!cancellationToken.IsCancellationRequested)
{
activity?.SetStatus(ActivityStatusCode.Error, ex.GetType().Name);
logger.LogError(ex, "Order import failed for {Partner}", "partner-a");
}
finally
{
telemetry.ImportDuration.Record(
Stopwatch.GetElapsedTime(started).TotalSeconds);
}
}
}
StartActivity は、記録を受け取る仕組みが登録されていないと null を返します。activity?.SetTag のように ?. で呼ぶのはこのためです2。失敗したときは SetStatus に ActivityStatusCode.Error を渡して、トレースの上でも失敗と分かるようにします。
例外を捕まえるかどうかは cancellationToken で判断します。.NET 5 以降の HttpClient は、タイムアウトしたときに OperationCanceledException を投げます17。ex is not OperationCanceledException のような条件にすると、タイムアウトを捕まえられません。例外が ExecuteAsync の外へ出ると、.NET 6 以降は既定でホストが止まり、Windowsサービスごと終わります18。サービスの停止で取り消されたときだけ例外をそのまま外へ出し、それ以外の失敗はトレースとログに記録して次の回を待ちます。
flowchart TB
accTitle: 取り込み中の例外の扱い
accDescr: 取り込み中に例外が出たとき、サービスの停止で取り消された場合だけ例外を外へ出し、HttpClientのタイムアウトなどそれ以外の失敗はトレースとログに記録して次の回を待つことを示す。
EX["取り込み中の例外"] --> Q{"停止で取り消されたか"}
Q -->|"はい"| OUT["例外を外へ出す"]
Q -->|"いいえ(タイムアウトなど)"| REC["トレースとログに記録"]
REC --> NEXT["次の回を待つ"]
図6: 停止による取り消しだけを外へ出し、タイムアウトは記録して処理を続ける。
ILogger で書いたログには、そのとき動いている Activity のトレースの識別子が自動で付きます19。保存先が対応していれば、遅かったトレースを選んで、その処理の中で出たログを並べて見られます。Activity の中で行った HttpClient の通信は、AddHttpClientInstrumentation によってその子の区間として記録されます12。例外をどこで捕まえてログに書くかは「例外処理でcatchとログはどこに置くべきか」を参照してください。
flowchart TB
accTitle: 1回の取り込みで記録されるデータ
accDescr: 1回の取り込みでは、ImportOrdersのActivityの区間にHttpClientの通信が子の区間として結び付き、ILoggerのログにトレースの識別子が付き、それとは別に件数と時間がメトリクスに記録されることを示す。
IMP["1回の取り込み"] --> ACT["ImportOrders の区間"]
ACT --> HTTP["HttpClient の子区間"]
ACT --> LOGS["ログ(識別子つき)"]
IMP --> CNT["件数のカウンター"]
IMP --> DUR["時間のヒストグラム"]
図7: 1回の取り込みで、区間に通信とログが結び付き、件数と時間もメトリクスに記録される。
送り先の指定(環境変数と appsettings.json)
送り先や名前はコードに書かずに環境変数で渡せます。よく使うのは次の4つです715。
| 環境変数 | 意味 |
|---|---|
OTEL_SERVICE_NAME |
アプリの名前(service.name) |
OTEL_RESOURCE_ATTRIBUTES |
アプリに付ける属性(deployment.environment.name=production など) |
OTEL_EXPORTER_OTLP_ENDPOINT |
送り先のURL |
OTEL_EXPORTER_OTLP_PROTOCOL |
grpc か http/protobuf |
OpenTelemetryの.NET実装はこれらの値を IConfiguration から読みます。そのため、Generic Hostを使うアプリでは、環境変数の代わりに appsettings.json やコマンドラインでも指定できます7。設定の重ね方は「appsettings.jsonだけじゃない」で説明しています。
ただし、コードの AddService で付けた名前は OTEL_SERVICE_NAME より優先されます。ResourceBuilder.CreateDefault は環境変数の値を先に読み込み、あとから加えた値が同じ名前の値を上書きするからです20。名前を環境変数で決めたいときは、AddService を書かずにおきます。
http/protobuf で送る場合、送り先のURLに /v1/traces のような種類ごとのパスが付くかどうかは、指定のしかたで変わります。OTEL_EXPORTER_OTLP_ENDPOINT で指定したときは、UseOtlpExporter でも AddOtlpExporter でもパスが自動で足されるので、ベースのURLだけを書きます。AddOtlpExporter の Endpoint をコードで指定したときはパスが足されないので、/v1/traces まで含めたURLを書く必要があります78。AddOtlpExporter では、OTEL_EXPORTER_OTLP_TRACES_ENDPOINT のような種類ごとの環境変数は使えません7。
flowchart TB
accTitle: http/protobufで送るときのURLのパス
accDescr: OTEL_EXPORTER_OTLP_ENDPOINTで送り先を指定すると種類ごとのパスが自動で足されるのでベースのURLだけを書き、AddOtlpExporterのEndpointをコードで指定するとパスが足されないので/v1/tracesまで含めたURLを書くことを示す。
SET["送り先の指定"] --> ENV["環境変数で指定"]
SET --> CODE["AddOtlpExporter の Endpoint"]
ENV --> BASE["ベースのURLだけ書く"]
CODE --> FULL["/v1/traces まで書く"]
図8: パスが自動で足されるかどうかは、送り先の指定のしかたで変わる。
デスクトップアプリに組み込む
Windowsフォームや WPF の常駐アプリでも、Generic Hostを使っていれば、OpenTelemetryの設定はWindowsサービスと同じコードで書けます(AddWindowsService は除きます)。デスクトップアプリでGeneric Hostを使う理由は「.NET Generic HostとBackgroundServiceをデスクトップアプリで使う理由」で説明しています。
Generic Hostを使わないアプリでは、Sdk.CreateTracerProviderBuilder と Sdk.CreateMeterProviderBuilder で、トレースとメトリクスの仕組みを自分で作ります。パッケージは OpenTelemetry.Exporter.OpenTelemetryProtocol と OpenTelemetry.Instrumentation.Http を使い、OpenTelemetry.Extensions.Hosting は要りません。ログは LoggerFactory.Create の中で AddOpenTelemetry を呼びます1621。次の例はWindowsフォームのアプリです。
using System.Diagnostics;
using System.Diagnostics.Metrics;
using Microsoft.Extensions.Logging;
using OpenTelemetry;
using OpenTelemetry.Logs;
using OpenTelemetry.Metrics;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
static class Program
{
[STAThread]
static void Main()
{
using var tracerProvider = Sdk.CreateTracerProviderBuilder()
.ConfigureResource(r => r.AddService("sales-client"))
.AddSource(SalesClientTelemetry.SourceName)
.AddHttpClientInstrumentation()
.AddOtlpExporter()
.Build();
using var meterProvider = Sdk.CreateMeterProviderBuilder()
.ConfigureResource(r => r.AddService("sales-client"))
.AddMeter(SalesClientTelemetry.MeterName)
.AddOtlpExporter()
.Build();
using var loggerFactory = LoggerFactory.Create(logging =>
logging.AddOpenTelemetry(options =>
{
options.SetResourceBuilder(
ResourceBuilder.CreateDefault().AddService("sales-client"));
options.AddOtlpExporter();
}));
ApplicationConfiguration.Initialize();
Application.Run(new MainForm(loggerFactory.CreateLogger<MainForm>()));
}
}
static class SalesClientTelemetry
{
public const string SourceName = "Komura.SalesClient";
public const string MeterName = "Komura.SalesClient";
public static readonly ActivitySource Source = new(SourceName);
public static readonly Meter Meter = new(MeterName);
}
ここで作った TracerProvider と MeterProvider は、アプリの起動時に一度だけ作り、終了時に破棄します。資料は、終了前に破棄し忘れると送り切れていない記録が失われることがあり、早く破棄しすぎるとそのあとの記録が集められないと説明しています2215。上のコードでは using var で Main の終わりに破棄されます。DIを使わないコードでは、Meter も ActivitySource と同じように一度だけ作って静的な変数に入れます14。
Generic Hostを使わない場合、Sdk.CreateTracerProviderBuilder は既定で環境変数だけから設定を読みます16。送り先や名前は環境変数で渡します。
flowchart TB
accTitle: デスクトップアプリでのOpenTelemetryの仕組みの寿命
accDescr: アプリの起動時にTracerProviderとMeterProviderを一度だけ作り、画面が動いているあいだ記録を集め、終了時に破棄して残りを送ることを示し、破棄を忘れると記録が失われ、早すぎると以後の記録が集められないことを補足する。
START["起動時に一度だけ作る"] --> RUN["画面が動くあいだ集める"]
RUN --> END["終了時に破棄して残りを送る"]
END -.-> FORGET["破棄を忘れると失われる"]
START -.-> EARLY["早く破棄すると集まらない"]
図9: 仕組みは起動時に作り、終了時に破棄する。
デスクトップアプリは台数が多く、利用者のPCから送ることになります。送り先には社内のCollectorを指定し、Collectorから保存先へ送る形にすると、各PCに保存先の認証情報を置かずに済みます。送り先で認証が要る場合、OTLPエクスポーターは OTEL_EXPORTER_OTLP_HEADERS でヘッダーを付けられます7。
送り先の選び方と Collector
OpenTelemetry Collectorは計測データを受け取って加工し、送り出す中継のプログラムで、特定のベンダーに依存しません23。OpenTelemetryの資料は、アプリの近くにCollectorを置くと、アプリはデータを素早くCollectorへ渡せて、再送、まとめて送る処理、暗号化、機密データの除去をCollectorに任せられると説明しています23。一方で、試すときや小規模・開発の環境なら、アプリから保存先へ直接送ってもよいとしています23。
flowchart TB
accTitle: Collectorを置くかどうかの判断
accDescr: 試すときや小規模・開発の環境ではアプリから保存先へ直接送ってよく、再送やまとめ送り、暗号化、機密データの除去をアプリから切り離したいときはアプリの近くにCollectorを置くことを示す。
Q{"アプリから切り離したい処理があるか"}
Q -->|"ない(試す・小規模・開発)"| DIRECT["保存先へ直接送る"]
Q -->|"ある(再送・暗号化・除去)"| COL["近くにCollectorを置く"]
図10: 小さく始めるなら直接送り、運用で必要な処理が増えたらCollectorに任せる。
Windows に Collector を入れる
Collectorには、Windows向けのMSIのインストーラーがあります。MSIはCollectorをWindowsサービスとして登録し、表示名は「OpenTelemetry Collector」になります。アプリケーションのイベントログにも、配布物の名前で発行元が登録されます24。
Collector の設定
Collectorの設定では受け取る部分(レシーバー)、加工する部分(プロセッサー)、送り出す部分(エクスポーター)を書き、service の pipelines で組み合わせます。設定に書いただけの部品は動かず、pipelines に並べたものだけが有効になります25。次の例は資料の設定例をもとに、アプリからOTLPで受け取り、確認用に画面へ出しながら保存先へHTTPで送るものです。
receivers:
otlp:
protocols:
grpc:
endpoint: localhost:4317
http:
endpoint: localhost:4318
exporters:
debug:
verbosity: normal
otlp_http:
endpoint: https://otlp.example.com:4318
service:
pipelines:
traces:
receivers: [otlp]
exporters: [debug, otlp_http]
metrics:
receivers: [otlp]
exporters: [debug, otlp_http]
logs:
receivers: [otlp]
exporters: [debug, otlp_http]
https://otlp.example.com:4318 は保存先のURLに置き換えます。部品の名前や書き方はCollectorのバージョンで変わることがあるので、入れたバージョンの資料で確かめます。たとえば otlp_http はv0.144.0で otlphttp から改名された名前で、それより前のバージョンでは otlphttp と書きます26。Collectorが受け付けるアドレスの既定は localhost です。資料の例にある 0.0.0.0 にすると、ほかのPCからも送れるようになる代わりに、ネットワーク上の誰からでも受け付けることになります25。社内のサーバーに置いてほかのPCから受け取るときは、受け付けるアドレスを変えたうえで、受け付ける範囲をファイアウォールで絞ります。
flowchart TB
accTitle: Collectorのパイプライン
accDescr: CollectorはレシーバーでOTLPのデータを受け取り、必要ならプロセッサーで加工し、エクスポーターで保存先へ送り、これらの部品はserviceのpipelinesに並べたものだけが有効になることを示す。
RCV["レシーバー(otlp)"] --> PRC["プロセッサー(任意)"]
PRC --> EXP["エクスポーター"]
EXP --> DST["保存先"]
PIPE["pipelines に並べたものだけ動く"] -.-> RCV
図11: Collectorは受け取り、加工し、送り出す部品を、パイプラインで組み合わせる。
開発中の Aspire ダッシュボード
保存先を決める前に、記録が正しく出ているかを手元で確かめたいときは、Aspireダッシュボードが使えます。AspireダッシュボードはDockerのコンテナーとして単独で動かせて、OTLPで送られたログ、メトリクス、トレースを画面で見られます9。
docker run --rm -it -p 18888:18888 -p 4317:18889 -p 4318:18890 --name aspire-dashboard mcr.microsoft.com/aspire/dashboard:latest
この例では、アプリはgRPCなら http://localhost:4317 へ、HTTPなら http://localhost:4318 へ送り、画面は http://localhost:18888 で開きます。ダッシュボードは既定でサインイン用のトークンを求め、トークンはコンテナーの出力に表示されます27。Microsoftの資料によると、このダッシュボードは開発者が確認するための道具で、本番の監視に使うものではありません9。
flowchart TB
accTitle: 開発中と本番の送り先
accDescr: 開発中は手元のAspireダッシュボードへOTLPで送って記録を確かめ、本番ではCollectorや保存先へ送り、送り先は設定で切り替えることを示す。
APP["業務アプリ"] --> SW{"送り先の設定"}
SW -->|"開発中"| DASH["Aspireダッシュボード"]
SW -->|"本番"| PROD["Collector や保存先"]
図12: 開発中と本番の送り先は、設定で切り替える。
既存の調査ツールとの使い分け
OpenTelemetryで集めた記録は、ふだんから続けて残すためのものです。その場で原因を深く調べるときは、これまでの調査ツールと組み合わせます。
自分で作った Meter の値は dotnet-counters でもその場で見られます。たとえば dotnet-counters monitor -n OrderImport --counters Komura.OrderImport のように、--counters にMeterの名前を渡します14。CPUを使っている関数やGCによる停止を詳しく調べるときは、dotnet-trace やPerfViewを使います(「PerfViewとdotnet-traceで「遅い」を特定する」)。メモリが増え続けるときの調べ方は「.NETでGC待ちとメモリリークを見分ける」にまとめています。
flowchart TB
accTitle: 調べたいことと道具の対応
accDescr: ふだんから記録を残して傾向やどこで遅いかを見るにはOpenTelemetry、その場で自作のMeterの値を見るにはdotnet-counters、CPUやGCを詳しく調べるにはdotnet-traceやPerfViewを使うことを示す。
Q["調べたいこと"] --> A["ふだんから傾向を残す"]
Q --> B["その場で値を見る"]
Q --> C["CPUやGCを詳しく調べる"]
A --> OT["OpenTelemetry"]
B --> DC["dotnet-counters"]
C --> DT["dotnet-trace・PerfView"]
図13: ふだんの記録はOpenTelemetry、その場の確認と詳しい調査は従来の道具を使う。
計測で気をつけること
メトリクスのタグの組み合わせ数
メトリクスに付けるタグ(OpenTelemetryでは属性と呼びます)の値の組み合わせごとに、集計の領域が作られます。Microsoftの資料は、1つの計測器で組み合わせが1000通り未満ならおおむね安全で、それを超えると保存先で絞り込むか、大量のデータを扱える作りが必要になるとしています。ヒストグラムはほかの種類より多くのメモリを使います14。
OpenTelemetryの.NET SDKには、1つのメトリクスあたり2000通りという上限が既定で設定されています。1.10.0 からは、上限を超えた値は捨てられず、otel.metric.overflow=true という属性だけを持つ1点にまとめられます。そうなると、タグで絞り込んだ集計は当てにならなくなります28。
flowchart TB
accTitle: メトリクスのタグに入れる値と入れない値
accDescr: 取引先や処理の結果のように値の種類が限られるものはメトリクスのタグに入れ、受注番号や顧客IDのように値が限りなく増えるものはトレースやログに書き、上限を超えるとotel.metric.overflowの1点にまとめられることを示す。
V["記録したい値"] --> FEW["種類が限られる値"]
V --> MANY["限りなく増える値"]
FEW --> TAG["メトリクスのタグ"]
MANY --> TL["トレースやログ"]
MANY -.-> OVF["上限を超えると1点にまとまる"]
図14: 種類が限られる値だけをタグにし、1件ごとの値はトレースやログに書く。
取引先や処理の結果のように種類が限られる値はタグに入れてかまいません。受注番号や顧客IDのように限りなく増える値は、メトリクスのタグに入れず、トレースの属性やログに書きます。
個人情報と機密の扱い
トレースの属性やログに書いた値は、そのまま保存先へ送られます。保存先が社外のサービスなら、顧客の氏名や住所、パスワード、APIキーを社外へ送ることになります。計測のためにこうした値を書かないことを先に決め、どうしても残る場合はCollectorで除去します23。
例外も同じです。ILogger に例外を渡すと、OTLPのログには例外の型、メッセージ、スタックトレースが付いて送られます29。例外のメッセージには、要求したURLや入力の値が含まれることがあります。上の取り込みの例で SetStatus に例外のメッセージではなく型の名前を渡しているのも、このためです。ログに例外そのものを渡すかどうかも、送り先に合わせて決めます。
Microsoftの資料によると、Aspireダッシュボードは表示する内容に機密が含まれうるため、既定でサインインを求めます9。
トレースを間引く
トレースは処理ごとに記録されるので、件数が多いアプリでは量が増えます。.NETのSDKは、何も指定しなければ親の判断に従い、親がなければすべて記録します(ParentBased(root=AlwaysOn))。環境変数 OTEL_TRACES_SAMPLER に parentbased_traceidratio を、OTEL_TRACES_SAMPLER_ARG に 0.25 を指定すると、親を持たないトレースの4分の1だけを記録します16。
flowchart TB
accTitle: トレースの間引き方
accDescr: 何も指定しなければ親の判断に従い、親がなければすべてのトレースを記録し、OTEL_TRACES_SAMPLERにparentbased_traceidratioとOTEL_TRACES_SAMPLER_ARGに割合を指定すると、親を持たないトレースをその割合だけ記録することを示す。
NEW["新しいトレース"] --> P{"親があるか"}
P -->|"ある"| FOLLOW["親の判断に従う"]
P -->|"ない・既定"| ALL["すべて記録"]
P -->|"ない・割合を指定"| PART["指定した割合だけ記録"]
図15: 間引く割合は環境変数で決め、親があるトレースは親の判断に従う。
導入の順番
一度に全部を入れようとせず、次の順番で進めます。
- 手元でAspireダッシュボードを動かし、パッケージを入れて
UseOtlpExporterを設定します。ログとHttpClientの記録が画面に出ることを確かめます。 - 取り込みや帳票の出力のような、業務で大事な処理に
ActivitySourceとMeterで計測を入れます。 - 社内のサーバーにCollectorを入れ、アプリの送り先をCollectorに変えます。
- 保存先を選び、Collectorのエクスポーターの設定で送ります。保存先を替えるときに変えるのは、この設定です。
flowchart TB
accTitle: OpenTelemetryを導入する順番
accDescr: 手元のAspireダッシュボードで記録が出ることを確かめ、業務で大事な処理に計測を入れ、社内にCollectorを置いて送り先を変え、最後に保存先を選んでCollectorから送る順番を示す。
S1["手元のダッシュボードで確かめる"] --> S2["大事な処理に計測を入れる"]
S2 --> S3["社内にCollectorを置く"]
S3 --> S4["保存先を選んで送る"]
図16: 手元で確かめてから、計測、Collector、保存先の順に広げる。
保存先を後から決められるのが、この進め方の利点です。最初の2段階はアプリの中の作業で、どの監視サービスを選んでも変わりません。
関連記事
- Windowsイベントログ・ETW入門
- .NET Generic Hostとは
- .NET Generic HostとBackgroundServiceをデスクトップアプリで使う理由
- Windowsサービスの作り方と運用
- HttpClientをusingで囲んではいけない
- PerfViewとdotnet-traceで「遅い」を特定する
- .NETでGC待ちとメモリリークを見分ける
- Windowsアプリのクラッシュ時にログとダンプを残す設計
- .NET 8と.NET 9のサポート終了に備える
参考リンク
-
Microsoft Learn, .NET Observability with OpenTelemetry. ログ・メトリクス・分散トレースの役割、.NETのOpenTelemetry実装が使うILogger・Meter・ActivitySource、OpenTelemetryの構成要素、OTLPで送れる保存先の例。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, Add distributed tracing instrumentation. ActivitySourceとActivityがTracerとSpanに当たること、ActivitySourceを静的に持つこと、StartActivityがnullを返す場合、SetTagとSetStatus。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, System.Net metrics. .NET 8から記録されるSystem.Net.HttpのMeter。 ↩
-
Microsoft Learn, .NET runtime metrics. .NET 9から記録されるSystem.RuntimeのMeter。 ↩
-
OpenTelemetry, Specification Status Summary. トレース、メトリクス、ログのOTLPが安定版であること。 ↩
-
OpenTelemetry .NET, OTLP Exporter for OpenTelemetry .NET. 既定のプロトコルと送り先、UseOtlpExporterの動作と制限、環境変数、http/protobufのパス。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
OpenTelemetry .NET, OtlpExporterOptions.cs. .NET Framework と .NET Standard 2.0 向けの既定のプロトコル、
OTEL_EXPORTER_OTLP_ENDPOINTから読んだときは種類ごとのパスを足し、Endpointをコードで指定したときは足さないこと。 ↩ ↩2 -
Microsoft Learn, Use OpenTelemetry with OTLP and the Aspire Dashboard. 使うパッケージとバージョン、Aspireダッシュボードの起動方法とトークン、本番の監視の道具ではないこと。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Create Windows Service using BackgroundService. Windowsサービスとして動かすために
Microsoft.Extensions.Hosting.WindowsServicesを入れ、AddWindowsServiceを呼ぶこと。 ↩ ↩2 -
OpenTelemetry .NET, OpenTelemetry.Extensions.Hosting. ホストに合わせて仕組みを始め、止めること、AddOpenTelemetry、ConfigureResource、WithTracing、WithMetrics。 ↩
-
OpenTelemetry .NET Contrib, HttpClient and HttpWebRequest instrumentation for OpenTelemetry. 安定版であること、HttpClientの通信を記録すること、.NET 9以降でもトレースの識別子を渡すために必要なこと。 ↩ ↩2 ↩3
-
NuGet Gallery, OpenTelemetry、OpenTelemetry.Instrumentation.Http. 執筆時点で最新だった安定版。 ↩
-
Microsoft Learn, Creating Metrics. IMeterFactory、DIを使わないときにMeterを静的に持つこと、名前と単位の付け方、dotnet-countersでの確認、タグの組み合わせの数の目安、ヒストグラムの既定の区切りとInstrumentAdvice。 ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
OpenTelemetry .NET, Customizing OpenTelemetry .NET SDK for Metrics. MeterProviderの寿命、OTEL_SERVICE_NAMEとOTEL_RESOURCE_ATTRIBUTES、ヒストグラムの区切りを決めるViewとAdvice。 ↩ ↩2 ↩3 ↩4
-
OpenTelemetry .NET, Customizing OpenTelemetry .NET SDK for Tracing. TracerProviderの作り方と破棄、既定のサンプラーとOTEL_TRACES_SAMPLER、ホストを使わないときは環境変数から設定を読むこと。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, HttpClient.GetAsync Method. .NET 5 以降ではタイムアウトのときに
OperationCanceledExceptionが投げられること。 ↩ -
Microsoft Learn, .NET 6 breaking change: Exception handling in hosting.
BackgroundServiceのExecuteAsyncから例外が出ると、既定でホストが止まること。 ↩ -
OpenTelemetry .NET, OpenTelemetry .NET Logs. ログがトレースと自動で結び付くこと。 ↩
-
OpenTelemetry .NET, ResourceBuilder.cs、Resource.cs.
CreateDefaultが環境変数の検出を先に加えることと、あとから合わせた値が衝突時に優先されること。 ↩ -
OpenTelemetry .NET, Customizing OpenTelemetry .NET SDK for Logs. LoggerFactory.CreateでのAddOpenTelemetryとSetResourceBuilder。 ↩
-
OpenTelemetry .NET, OpenTelemetry .NET Traces. TracerProviderの破棄を忘れたときと早すぎたときに起きること。 ↩
-
OpenTelemetry, Collector. Collectorの役目、置く理由、直接送ってよい場合。 ↩ ↩2 ↩3 ↩4
-
OpenTelemetry, Install the Collector on Windows. MSIがWindowsサービスとして登録し、イベントログの発行元も登録すること。 ↩
-
OpenTelemetry, Configuration. レシーバー、プロセッサー、エクスポーター、pipelinesに並べたものだけが有効になること、既定のアドレスがlocalhostであること。 ↩ ↩2
-
OpenTelemetry Collector, CHANGELOG. v0.144.0で
otlphttpエクスポーターをotlp_httpに改名し、otlphttpを非推奨の別名として残したこと。 ↩ -
Aspire, Standalone Aspire dashboard. コンテナーの名前、OTLPのgRPCとHTTPのポート、サインイン用のトークン。 ↩
-
OpenTelemetry .NET, OpenTelemetry .NET Metrics. 既定の上限2000とotel.metric.overflow。 ↩
-
OpenTelemetry .NET, ProtobufOtlpLogSerializer.cs. ログに例外があると、例外の型、メッセージ、スタックトレースを属性として送ること。 ↩
関連する記事
同じタグを共有する最新の記事です。さらに近い話題で知識を深められます。
.NET 8と.NET 9のサポート終了に備える ── 2026年11月10日までに業務アプリを棚卸しして.NET 10へ上げる手順
.NET 8と.NET 9のサポートは2026年11月10日に終わります。影響を受けるアプリの見つけ方、配布のしかたによる更新の届き方の違い、.NET 10へ上げる手順と破壊的変更、間に合わないときの選択肢をまとめます。
マルチスレッドの実務ベストプラクティス .NET編 ── スレッドを増やす前に決めておくこと
「スレッドを立てたら、たまに落ちる・固まる」を防ぐ設計の定石を.NET/C#向けに整理。スレッドを自分で作らずTaskに乗る、共有可変状態を減らす、ロックの規律、CancellationTokenでの停止設計、UIスレッドの扱いまで解説します。
WMI/CIMをC#・PowerShellから使う ── ハードウェア情報取得・プロセス監視・リモート照会の実務ガイド
PCのシリアル番号取得、ディスク空き監視、プロセス起動検知の定番がWMI/CIMです。Get-CimInstance等のCIMコマンドレットの使い方と旧Get-WmiObjectからの移行、C#のSystem.ManagementとCIM APIの使い分け、実例レシピと落と...
ネットワークドライブとUNCパスの落とし穴 ── 業務アプリでファイルサーバー(共有フォルダ)を扱う実務
業務アプリから共有フォルダへ出力・監視するときの定番トラブルを整理します。ドライブ文字(Z:)がサービスから見えない理由、実行アカウントごとに必要な権限、エラー1219、FileSystemWatcherの注意点まで解説します。
業務アプリの和暦・祝日・締め日処理 ── 改元に強い設計とJapaneseCalendar・営業日計算の実務
和暦表示・祝日を除く営業日計算・締め日処理という日本の業務アプリ特有の日付処理を整理します。JapaneseCalendarと改元に強い設計、内閣府CSVによる祝日マスタ運用、AddMonthsの丸めを踏まえた締め日計算まで解説します。
関連トピック
このテーマと近いトピックページです。記事を起点に、関連するサービスや他の記事へ進めます。
Windows技術トピック
Windows 開発、不具合調査、既存資産活用の技術トピックをまとめた入口です。
このテーマがつながるサービス
この記事は次のサービスページにつながります。近い入口からご覧ください。
Windowsアプリ開発
Windowsサービスや常駐アプリ、ASP.NET Coreで作った業務アプリに、ログ・メトリクス・トレースの計測を組み込む作業を、既存のコードと運用に合わせて進められます。
不具合調査・原因解析
「たまに遅い」「夜間の取り込みだけ失敗する」のような再現しにくい不具合は、どこで時間を使い、どこで失敗したかを記録できるようにしてから調べると、原因を絞り込みやすくなります。
よくある質問
この記事のテーマについて、相談時によくある質問をまとめています。
- OpenTelemetryを使うと、特定の監視サービスに縛られなくなるのはなぜですか?
- アプリはOpenTelemetryの仕組みと、ベンダーに依存しないOTLPという送信の形式でデータを送るからです。送り先は設定で変えられるので、Prometheus・Grafana、Azure Monitor、各社の監視サービスなどへ、アプリのコードを書き直さずに切り替えられます。
- .NETのアプリで、計測のためにOpenTelemetry専用のAPIを使う必要はありますか?
- ありません。.NETにはログのILogger、メトリクスのSystem.Diagnostics.Metrics.Meter、トレースのSystem.Diagnostics.ActivitySourceとActivityが最初から入っていて、OpenTelemetryの.NET実装はこれらのAPIからデータを集めます。アプリのコードは.NETのAPIで計測し、どこへ送るかを起動時の設定で決めます。
- Collectorは必ず置く必要がありますか?
- 必須ではありません。OpenTelemetryの資料は、試すときや小規模・開発の環境ならアプリから直接送ってもよいとしています。再送、まとめて送る処理、暗号化、機密データの除去をアプリから切り離したいときは、アプリの近くにCollectorを置くよう勧めています。
- メトリクスのタグに受注番号や顧客IDを入れてもよいですか?
- 入れないでください。タグの値の組み合わせごとに集計の領域が作られるので、受注番号のように値が限りなく増えるものを入れると、保存先で扱うデータが大きく増えます。OpenTelemetryの.NET SDKには1つのメトリクスあたり2000通りの上限があり、超えた分は別の1点にまとめられます。1件ごとの値はトレースやログに書きます。