コンパイル時のログ ソース生成
.NET 6 では、
ソース ジェネレーターは、
基本的な使用方法
public static partial class Log
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Critical,
Message = "Could not open socket to `{HostName}`")]
public static partial void CouldNotOpenSocket(
ILogger logger, string hostName);
}
前の例では、ログ メソッドは ILogger
インスタンスがパラメーターとして必要になるか、this
キーワードを使用して該当のメソッドを拡張メソッドとするように定義を変更します。
public static partial class Log
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Critical,
Message = "Could not open socket to `{HostName}`")]
public static partial void CouldNotOpenSocket(
this ILogger logger, string hostName);
}
属性は非静的コンテキストでも使用できます。 ログ メソッドがインスタンス メソッドとして宣言されている場合の次の例を考えてみます。 このコンテキストでは、ログ メソッドにより、含んでいるクラスの
public partial class InstanceLoggingExample
{
private readonly ILogger _logger;
public InstanceLoggingExample(ILogger logger)
{
_logger = logger;
}
[LoggerMessage(
EventId = 0,
Level = LogLevel.Critical,
Message = "Could not open socket to `{HostName}`")]
public partial void CouldNotOpenSocket(string hostName);
}
.NET 9 以降では、ログ メソッドは、包含クラスの ILogger
プライマリ コンストラクター パラメーターから追加でロガーを取得できます。
public partial class InstanceLoggingExample(ILogger logger)
{
[LoggerMessage(
EventId = 0,
Level = LogLevel.Critical,
Message = "Could not open socket to `{HostName}`")]
public partial void CouldNotOpenSocket(string hostName);
}
ILogger
フィールドとプライマリ コンストラクター パラメーターの両方がある場合、ログ メソッドはフィールドからロガーを取得します。
場合によっては、コードに静的に組み込むのではなく、ログ レベルを動的にする必要があります。 これは、属性のログ レベルを省略し、代わりにログ メソッドのパラメーターとしてそれを要求することで行うことができます。
public static partial class Log
{
[LoggerMessage(
EventId = 0,
Message = "Could not open socket to `{HostName}`")]
public static partial void CouldNotOpenSocket(
ILogger logger,
LogLevel level, /* Dynamic log level as parameter, rather than defined in attribute. */
string hostName);
}
ログ メッセージは省略できます。そのメッセージに対しては、
using System.Text.Json;
using Microsoft.Extensions.Logging;
using ILoggerFactory loggerFactory = LoggerFactory.Create(
builder =>
builder.AddJsonConsole(
options =>
options.JsonWriterOptions = new JsonWriterOptions()
{
Indented = true
}));
ILogger<SampleObject> logger = loggerFactory.CreateLogger<SampleObject>();
logger.PlaceOfResidence(logLevel: LogLevel.Information, name: "Liana", city: "Seattle");
readonly file record struct SampleObject { }
public static partial class Log
{
[LoggerMessage(EventId = 23, Message = "{Name} lives in {City}.")]
public static partial void PlaceOfResidence(
this ILogger logger,
LogLevel logLevel,
string name,
string city);
}
{
"EventId": 23,
"LogLevel": "Information",
"Category": "\u003CProgram\u003EF...9CB42__SampleObject",
"Message": "Liana lives in Seattle.",
"State": {
"Message": "Liana lives in Seattle.",
"name": "Liana",
"city": "Seattle",
"{OriginalFormat}": "{Name} lives in {City}."
}
}
ログ メソッドの制約
ログ メソッドで LoggerMessageAttribute
を使用する場合、いくつかの制約に従う必要があります。
- ログ メソッドは
partial
であり、void
を返す必要があります。 - ログ メソッド名の先頭にアンダースコアを使用することは ''
* できません* ''。 - ログ メソッドのパラメーター名の先頭にアンダースコアを使用することは ''
* できません* ''。 - 入れ子になった型ではログ メソッドが定義 ''
* されない* '' 可能性があります。 - ログ メソッドを汎用にすることは ''
* できません* ''。 - ログ メソッドが
static
の場合、ILogger
インスタンスがパラメーターとして必要です。
コード生成モデルは、最新の C# コンパイラ バージョン 9 以降でコンパイルされるコードに依存します。 C# 9.0 コンパイラは、.NET 5 で使用できるようになりました。 最新の C# コンパイラにアップグレードするには、C# 9.0 をターゲットとするようにプロジェクト ファイルを編集します。
<PropertyGroup>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
詳細については、「C# 言語のバージョン管理」を参照してください。
ログ メソッドの構造
public interface ILogger
{
void Log<TState>(
Microsoft.Extensions.Logging.LogLevel logLevel,
Microsoft.Extensions.Logging.EventId eventId,
TState state,
System.Exception? exception,
Func<TState, System.Exception?, string> formatter);
}
一般的な規則として、
// This is a valid attribute usage
[LoggerMessage(
EventId = 110, Level = LogLevel.Debug, Message = "M1 {Ex3} {Ex2}")]
public static partial void ValidLogMethod(
ILogger logger,
Exception ex,
Exception ex2,
Exception ex3);
// This causes a warning
[LoggerMessage(
EventId = 0, Level = LogLevel.Debug, Message = "M1 {Ex} {Ex2}")]
public static partial void WarningLogMethod(
ILogger logger,
Exception ex,
Exception ex2);
重要
出力される警告により、
Don't include a template for `ex` in the logging message since it is implicitly taken care of.
大文字と小文字を区別しないテンプレート名のサポート
ジェネレーターでは、メッセージ テンプレート内の項目とログ メッセージ内の引数名の間で、大文字と小文字を区別しない比較を行います。 つまり、ILogger
により状態が列挙されると、引数がメッセージ テンプレートによって取得され、ログが使いやすくなります。
public partial class LoggingExample
{
private readonly ILogger _logger;
public LoggingExample(ILogger logger)
{
_logger = logger;
}
[LoggerMessage(
EventId = 10,
Level = LogLevel.Information,
Message = "Welcome to {City} {Province}!")]
public partial void LogMethodSupportsPascalCasingOfNames(
string city, string province);
public void TestLogging()
{
LogMethodSupportsPascalCasingOfNames("Vancouver", "BC");
}
}
JsonConsole
フォーマッタを使用する場合のログ出力の例を考えてみましょう。
{
"EventId": 13,
"LogLevel": "Information",
"Category": "LoggingExample",
"Message": "Welcome to Vancouver BC!",
"State": {
"Message": "Welcome to Vancouver BC!",
"City": "Vancouver",
"Province": "BC",
"{OriginalFormat}": "Welcome to {City} {Province}!"
}
}
不確定なパラメーターの順序
ログ メソッド パラメーターの順序に関する制約はありません。 開発者は ILogger
を最後のパラメーターとして定義できますが、少し見た目がよくない場合があります。
[LoggerMessage(
EventId = 110,
Level = LogLevel.Debug,
Message = "M1 {Ex3} {Ex2}")]
static partial void LogMethod(
Exception ex,
Exception ex2,
Exception ex3,
ILogger logger);
ヒント
ログ メソッドのパラメーターの順序は、テンプレート プレースホルダーの順序に対応する必要は ''
{
"EventId": 110,
"LogLevel": "Debug",
"Category": "ConsoleApp.Program",
"Message": "M1 System.Exception: Third time's the charm. System.Exception: This is the second error.",
"State": {
"Message": "M1 System.Exception: Third time's the charm. System.Exception: This is the second error.",
"ex2": "System.Exception: This is the second error.",
"ex3": "System.Exception: Third time's the charm.",
"{OriginalFormat}": "M1 {Ex3} {Ex2}"
}
}
その他のログの例
次のサンプルからは、イベント名の取得方法、ログ レベルを動的に設定する方法、ログ パラメーターを書式設定する方法がわかります。 ログ メソッドは次のようになります。
`LogWithCustomEventName` :`LoggerMessage` 属性を使用してイベント名を取得します。`LogWithDynamicLogLevel` : 構成入力に基づいてログ レベルを設定できるように、ログ レベルを動的に設定します。`UsingFormatSpecifier` : 書式指定子を使用して、ログ パラメーターの書式を設定します。
public partial class LoggingSample
{
private readonly ILogger _logger;
public LoggingSample(ILogger logger)
{
_logger = logger;
}
[LoggerMessage(
EventId = 20,
Level = LogLevel.Critical,
Message = "Value is {Value:E}")]
public static partial void UsingFormatSpecifier(
ILogger logger, double value);
[LoggerMessage(
EventId = 9,
Level = LogLevel.Trace,
Message = "Fixed message",
EventName = "CustomEventName")]
public partial void LogWithCustomEventName();
[LoggerMessage(
EventId = 10,
Message = "Welcome to {City} {Province}!")]
public partial void LogWithDynamicLogLevel(
string city, LogLevel level, string province);
public void TestLogging()
{
LogWithCustomEventName();
LogWithDynamicLogLevel("Vancouver", LogLevel.Warning, "BC");
LogWithDynamicLogLevel("Vancouver", LogLevel.Information, "BC");
UsingFormatSpecifier(logger, 12345.6789);
}
}
SimpleConsole
フォーマッタを使用する場合のログ出力の例を考えてみましょう。
trce: LoggingExample[9]
Fixed message
warn: LoggingExample[10]
Welcome to Vancouver BC!
info: LoggingExample[10]
Welcome to Vancouver BC!
crit: LoggingExample[20]
Value is 1.234568E+004
JsonConsole
フォーマッタを使用する場合のログ出力の例を考えてみましょう。
{
"EventId": 9,
"LogLevel": "Trace",
"Category": "LoggingExample",
"Message": "Fixed message",
"State": {
"Message": "Fixed message",
"{OriginalFormat}": "Fixed message"
}
}
{
"EventId": 10,
"LogLevel": "Warning",
"Category": "LoggingExample",
"Message": "Welcome to Vancouver BC!",
"State": {
"Message": "Welcome to Vancouver BC!",
"city": "Vancouver",
"province": "BC",
"{OriginalFormat}": "Welcome to {City} {Province}!"
}
}
{
"EventId": 10,
"LogLevel": "Information",
"Category": "LoggingExample",
"Message": "Welcome to Vancouver BC!",
"State": {
"Message": "Welcome to Vancouver BC!",
"city": "Vancouver",
"province": "BC",
"{OriginalFormat}": "Welcome to {City} {Province}!"
}
}
{
"EventId": 20,
"LogLevel": "Critical",
"Category": "LoggingExample",
"Message": "Value is 1.234568E+004",
"State": {
"Message": "Value is 1.234568E+004",
"value": 12345.6789,
"{OriginalFormat}": "Value is {Value:E}"
}
}
まとめ
C# ソース ジェネレーターの出現により、非常にパフォーマンスの高いログ API の作成がはるかに簡単になりました。 ソース ジェネレーターの方法を使用することには、いくつかの重要なベネフィットがあります。
- ログ構造を保持でき、
[ メッセージ テンプレート](https://messagetemplates.org) で必要とされる正確な形式の構文が有効になります。 - テンプレート プレースホルダーの代替名を指定し、書式指定子を使用できます。
- 元のデータをすべてそのまま渡すことができ、(
string
を作成する以外に) そのデータで何かを行う前にどのように格納するかといった複雑さもありません。 - ログ固有の診断を提供し、重複するイベント ID に関する警告を出力します。
さらに、
- より短く単純な構文: 定型コーディングではなく、宣言属性を使用します。
- ガイド付き開発者エクスペリエンス: ジェネレーターにより、開発者が適切な作業を行うのに役立つ警告が示されます。
- 任意の数のログ パラメーターのサポート。
`LoggerMessage.Define` では最大 6 つがサポートされます。 - 動的ログ レベルのサポート。 これは
`LoggerMessage.Define` のみでは不可能です。
関連項目
- .NET でのログの記録
- .NET での高パフォーマンスのログ
- コンソール ログの書式設定
[ NuGet: Microsoft.Extensions.Logging.Abstractions](https://www.nuget.org/packages/microsoft.extensions.logging.abstractions)
.NET