【C#】Loggerを触ったので忘れないようにメモ

C#

本記事は、10分程度で読むことが可能です。

前提

Loggerとは、.NET の Microsoft.Extensions.Logging フレームワークが提供する、
メッセージテンプレートと構造化ログを前提とした、拡張可能なログ出力インターフェースです。
今回検証で利用するパッケージは下記になります。

Console.appで作成してください
(※nugetにて、下記をインストールしてください)。Microsoft.Extensions.Logging
→ ILogger の基盤
OpenTelemetry / OpenTelemetry.Logs
→ LogRecord への変換
OpenTelemetry.Exporter.Console
→ LogRecord の中身確認用
OpenTelemetry.Exporter.OpenTelemetryProtocol
→ Otel Collector 連携用
YamlDotNet
→ Yamlを使用する場合のみ

提供ソース

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Logs;
using System.Text.Json;
// using System.Text.Json.Serialization;

namespace LoggerConnect;

public partial class Program
{

    public static void Main()
    {
        // object変数
        var person = new Person { Name = "koba", Age = 23 };

        using var loggerFactory = LoggerFactory.Create(builder =>
        {
            builder.Configure(x =>
            {
                // Activity が存在する場合、TraceId/SpanId をログ属性として付与設定
                x.ActivityTrackingOptions = ActivityTrackingOptions.TraceId | ActivityTrackingOptions.SpanId;
            });
            // optionの設定
            builder.AddOpenTelemetry(options =>
            {
                // FormattedMessageを利用するかしないかのオプション
                // options.IncludeFormattedMessage = false;
                options.IncludeFormattedMessage = true;

                // 構造化ログを Attributes に展開
                options.ParseStateValues = true;

                // Console出力設定をして実際に確認できるようにする。
                // OtelColにても受信できるかを相互確認できる。
                options.AddConsoleExporter();

                options.AddOtlpExporter(opt =>
                {
                    // dockerにotelcolが立ち上がっている場合のエンドポイント
          // (※ここの詳細は省く)
                    opt.Protocol = OpenTelemetry.Exporter.OtlpExportProtocol.Grpc;
                    opt.Endpoint = new Uri("http://localhost:4317");
                });
            });
        });
        var logger = loggerFactory.CreateLogger<Program>();
        // logger.LogInformation("年齢: {Age:XYZ} 名前: {Name:nnn}", 23);
        // 出力時にプレースホルダの方が多い場合は、エラーが出力される(名前のパラメータが未定義)逆も実践してみて!

        //logger.LogInformation(new EventId(55, "Hello, World!"), $"年齢: {person.Age} name: {person.Name}");
        // プレースホルダを利用せずBodyのみに記載しているパターン
        
        logger.LogInformation("ConfigData: {Person}",person);
        // bodyとFormattedMessageが異なる正規表現(EventIDや例外も試してね!);

        // ブレークポイント用なので有無を問いません。
        Thread.Sleep(10000);
    }
}

public class Person
{
    public string Name { get; set; } = string.Empty;
    public int Age { get; set; }
}

出力結果

導入後実行していただくと、下記のような出力になると思います。

Logger connected.
-------------------------
LogRecord.Timestamp:               2026-01-11T12:09:49.2608449Z
LogRecord.CategoryName:            LoggerConnect.Program
LogRecord.Severity:                Info
LogRecord.SeverityText:            Information
LogRecord.FormattedMessage:        ConfigData: {"Name":"koba","Age":23}
LogRecord.Body:                    ConfigData: {Person}
LogRecord.Attributes (Key:Value):
    Person: {"Name":"koba","Age":23}
    OriginalFormat (a.k.a Body): ConfigData: {Person}

Resource associated with LogRecord:
telemetry.sdk.name: opentelemetry
telemetry.sdk.language: dotnet
telemetry.sdk.version: 1.14.0
service.name: unknown_service:LoggerConnect

-------------------------// bodyとFormattedMessageが異なる

出力結果の詳細について

bodyが評価しているのとFormattedMessageが評価しているものが異なるからです(あたりまえですが)。Body は「OriginalFormat(メッセージテンプレート)」を保持します。ソースコード上に記述されたテンプレート文字列そのものです。

ですが、FormattedMessageは、第一引数のプレースホルダと実際のパラメータを読み込みます。そのため出力に差分が出ています。

逆に、気になるのはAttributesだと思います。
なぜFormattedMessageと同じ出力がされるの不思議に思われたと思います。
これはソース内に記述してあるオプションのParseStateValues = true;のおかげです。

「LogRecord.Attributes (Key:Value)」と結果にあるように、構造化されたDataを出力してくれます。Attributes は OpenTelemetry の設計上、後段の集約・検索・分析で利用される「機械可読な構造データ」を保持するための領域です。

Lokiや、VictoriaLogsなどで、『Ageが20以上のログだけ抽出』みたいなクエリが監視系のアプリにてクエリをたたくことが可能になります。

補足・注意点

本記事でコメントアウトしている他のコードについても、
実際に実行して挙動を確認してみることをおすすめします。
実務では、以下のようなケースが問題になることがあります。

ログレベルによって出力されないにもかかわらず、
 引数の評価(文字列補間や JSON Serialize など)が先に行われ、
 不要な処理コストが発生してしまうケース

JSON を Serialize する過程でフォーマットが崩れ、
 構造化ログとして意図した形で扱えなくなるケース

ログ出力時にプレースホルダの数と引数の数が一致せず、
 実行時ではなくコンパイル時に Roslyn Analyzer によって
 エラーや警告として検出されるケース

これらを防ぐためにも、ログに渡すデータはできる限り静的に分解した構造化データ として扱い、文字列補間や過度な動的処理を避けることを推奨します。
一応、VSではエラーコードが出るみたいなので、Key(Body)/Valueに動的なメッセージを記述することしないかと思いますが、。

まとめ

余談ですが、今回は実際に現場の調査を兼ねて実施しました。
「logger.logLevel(EventID?, Execption?, String?, Objects[] args)」
で定義することが可能ですので、実際に挙動を提供したソースで触ってみてください。

本題のまとめなんですが、Logger は単なる文字列出力の仕組みではなく、
テンプレート・構造化データ・トレース情報 を組み合わせた観測基盤の一部
として設計されています。

今回の検証が、Logger や OpenTelemetry Logging の挙動を理解し、
より安全で効率的なログ設計を考えるきっかけになれば幸いです。

最後まで読んでいただき、ありがとうございました。

コメント

タイトルとURLをコピーしました