컴파일 시간 로깅 소스 생성

.NET 6에는 LoggerMessageAttribute 형식이 도입되었습니다. 이 특성은 Microsoft.Extensions.Logging 네임스페이스에 포함되며, 사용되는 경우 성능이 뛰어난 로깅 API의 소스를 생성합니다. 소스 생성 로깅 지원은 최신 .NET 애플리케이션을 위한 뛰어난 가용성의 고성능 로깅 솔루션을 제공하도록 설계되었습니다. 자동 생성된 소스 코드는 LoggerMessage.Define 기능과 함께 ILogger 인터페이스를 사용합니다.

이 소스 생성기는 LoggerMessageAttributepartial 로깅 메서드에서 사용될 때 트리거됩니다. 트리거되면 데코레이트 중인 partial 메서드의 구현을 자동 생성하거나, 적절한 사용법에 대한 힌트를 사용하여 컴파일 시간 진단을 생성할 수 있습니다. 컴파일 시간 로깅 솔루션은 일반적으로 기존 로깅 방법보다 런타임에 훨씬 더 빠릅니다. boxing, 임시 할당, 복사를 가능한 최대 범위로 제거하여 이렇게 빠른 속도가 가능합니다.

기본 사용법

LoggerMessageAttribute를 사용하려면 사용하는 클래스와 메서드가 partial이어야 합니다. 코드 생성기는 컴파일 시간에 트리거되고 partial 메서드의 구현을 생성합니다.

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);
}

위의 예제에서 로깅 메서드는 static이고 로그 수준은 특성 정의에 지정됩니다. 정적 컨텍스트에서 특성을 사용하는 경우 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);
}

비정적 컨텍스트에서도 이 특성을 사용하도록 선택할 수 있습니다. 로깅 메서드가 인스턴스 메서드로 선언된 다음 예제를 살펴보세요. 이 컨텍스트에서 로깅 메서드는 포함하는 클래스의 ILogger 필드에 액세스하여 로거를 가져옵니다.

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);
}

로그 수준은 코드에 정적으로 빌드되지 않고 동적이어야 하는 경우도 있습니다. 특성에서 로그 수준을 생략하고 대신 로깅 메서드에 대한 매개 변수로 요구하여 이렇게 할 수 있습니다.

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);
}

로깅 메시지를 생략할 수 있으며, 생략할 경우 메시지로 String.Empty가 제공됩니다. 상태에는 키-값 쌍으로 형식이 지정된 인수가 포함됩니다.

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);
}

JsonConsole 포맷터를 사용할 경우 예제 로깅 출력을 살펴보세요.

{
  "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# 언어 버전 관리를 참조하세요.

로그 메서드 구조

ILogger.Log 시그니처는 아래와 같이 LogLevel과 선택적으로 Exception을 허용합니다.

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);
}

일반적으로 ILogger, LogLevel, Exception의 첫 번째 인스턴스는 특별히 소스 생성기의 로그 메서드 시그니처에서 처리됩니다. 후속 인스턴스는 메시지 템플릿에 대한 일반 매개 변수처럼 처리됩니다.

// 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);

Important

내보낸 경고는 LoggerMessageAttribute의 올바른 사용법에 관한 세부 정보를 제공합니다. 위의 예제에서 WarningLogMethodSYSLIB0025DiagnosticSeverity.Warning을 보고합니다.

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);

로그 메서드의 매개 변수 순서는 템플릿 자리 표시자의 순서와 일치하지 ‘않아도’ 됩니다. 대신 템플릿의 자리 표시자 이름은 매개 변수와 일치해야 합니다. 다음 JsonConsole 출력과 오류 순서를 살펴보세요.

{
  "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를 훨씬 쉽게 작성할 수 있게 되었습니다. 소스 생성기 방법을 사용하는 경우 다음과 같은 몇 가지 주요 이점이 있습니다.

  • 로깅 구조를 보존하고 메시지 템플릿에 필요한 정확한 형식 구문을 사용하도록 설정할 수 있습니다.
  • 템플릿 자리 표시자에 대한 대체 이름을 제공하고 형식 지정자를 사용할 수 있습니다.
  • string을(를) 만드는 것 외에 다른 작업을 수행하기 전에 저장 방법에 대한 복잡성 없이 모든 원본 데이터를 있는 그대로 전달할 수 있습니다.
  • 로깅 관련 진단을 제공하고 중복 이벤트 ID에 대한 경고를 내보냅니다.

또한 LoggerMessage.Define을 수동으로 사용하는 것에 비해 다음과 같은 이점이 있습니다.

  • 더 짧고 간단한 구문: 상용구 코딩 대신 선언적 특성 사용
  • 단계별 개발자 환경: 이 생성기는 개발자가 올바른 작업을 수행하는 데 도움이 되도록 경고를 제공합니다.
  • 임의 개수의 로깅 매개 변수 지원: LoggerMessage.Define은 최대 6개를 지원합니다.
  • 동적 로그 수준 지원: LoggerMessage.Define만 사용해서는 불가능합니다.

참고 항목