Dela via


Kompilering av källgenerering för loggning

.NET 6 introducerar LoggerMessageAttribute typen. Det här attributet är en del av Microsoft.Extensions.Logging namnområdet och när det används genererar det högpresterande loggnings-API:er. Stöd för källgenereringsloggning är utformat för att leverera en mycket användbar och mycket högpresterande loggningslösning för moderna .NET-program. Den automatiskt genererade källkoden ILogger förlitar sig på gränssnittet tillsammans med LoggerMessage.Define funktioner.

Källgeneratorn utlöses när LoggerMessageAttribute den används på partial loggningsmetoder. När den utlöses kan den antingen automatiskt generera implementeringen av de metoder som den partial dekorerar eller producera kompileringstidsdiagnostik med tips om korrekt användning. Kompileringslösningen för tidsloggning är vanligtvis betydligt snabbare vid körning än befintliga loggningsmetoder. Det uppnår detta genom att eliminera boxning, tillfälliga allokeringar och kopior i största möjliga utsträckning.

Grundläggande användning

Om du vill använda LoggerMessageAttributemåste den förbrukande klassen och metoden vara partial. Kodgeneratorn utlöses vid kompileringstillfället och genererar en implementering av partial metoden.

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

I föregående exempel är static loggningsmetoden och loggnivån anges i attributdefinitionen. När du använder attributet i en statisk kontext krävs antingen instansen ILogger som en parameter eller ändrar definitionen för att använda nyckelordet this för att definiera metoden som en tilläggsmetod.

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

Du kan också välja att använda attributet i en icke-statisk kontext. Tänk dig följande exempel där loggningsmetoden deklareras som en instansmetod. I det här sammanhanget hämtar loggningsmetoden loggaren genom att komma åt ett ILogger fält i den innehållande klassen.

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

Från och med .NET 9 kan loggningsmetoden dessutom hämta loggningsverktyget från en ILogger primär konstruktorparameter i den innehållande klassen.

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

Om det finns både ett ILogger fält och en primär konstruktorparameter hämtas loggningsmetoden från fältet.

Ibland måste loggnivån vara dynamisk snarare än statiskt inbyggd i koden. Du kan göra detta genom att utelämna loggnivån från attributet och i stället kräva det som en parameter till loggningsmetoden.

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

Du kan utelämna loggningsmeddelandet och String.Empty kommer att tillhandahållas för meddelandet. Tillståndet innehåller argumenten, formaterade som nyckel/värde-par.

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

Överväg exempelloggningsutdata när du använder formateringsfunktionen 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}."
  }
}

Begränsningar för loggmetod

När du använder loggningsmetoderna LoggerMessageAttribute måste vissa begränsningar följas:

  • Loggningsmetoder måste vara partial och returnera void.
  • Namn på loggningsmetod får inte börja med ett understreck.
  • Parameternamn för loggningsmetoder får inte börja med ett understreck.
  • Loggningsmetoder kanske inte definieras i en kapslad typ.
  • Loggningsmetoder kan inte vara generiska.
  • Om en loggningsmetod är statickrävs instansen ILogger som en parameter.

Kodgenereringsmodellen beror på att kod kompileras med en modern C#-kompilator, version 9 eller senare. C# 9.0-kompilatorn blev tillgänglig med .NET 5. Om du vill uppgradera till en modern C#-kompilator redigerar du projektfilen till mål C# 9.0.

<PropertyGroup>
  <LangVersion>9.0</LangVersion>
</PropertyGroup>

Mer information finns i C#-språkversioner.

Loggmetodsanatomi

Signaturen ILogger.Log LogLevel accepterar och eventuellt en Exception, enligt nedan.

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

Som en allmän regel behandlas den första instansen av ILogger, LogLeveloch Exception särskilt i loggmetodsignaturen för källgeneratorn. Efterföljande instanser behandlas som normala parametrar för meddelandemallen:

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

Viktigt!

Varningarna som genereras ger information om rätt användning av LoggerMessageAttribute. I föregående exempel WarningLogMethod rapporterar will en DiagnosticSeverity.Warning av SYSLIB0025.

Don't include a template for `ex` in the logging message since it is implicitly taken care of.

Stöd för skiftlägesokänsliga mallnamn

Generatorn gör en skiftlägeskänslig jämförelse mellan objekt i meddelandemallen och argumentnamnen i loggmeddelandet. Det innebär att när ILogger tillståndet räknas upp hämtas argumentet av meddelandemallen, vilket kan göra loggarna snyggare att använda:

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

Överväg exempelloggningsutdata när du använder formateringsfunktionen 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}!"
  }
}

Obestämd parameterordning

Det finns inga begränsningar för ordningen på loggmetodparametrar. En utvecklare kan definiera ILogger som den sista parametern, även om det kan verka lite besvärligt.

[LoggerMessage(
    EventId = 110,
    Level = LogLevel.Debug,
    Message = "M1 {Ex3} {Ex2}")]
static partial void LogMethod(
    Exception ex,
    Exception ex2,
    Exception ex3,
    ILogger logger);

Dricks

Ordningen på parametrarna på en loggmetod krävs inte för att motsvara ordningen på mallplatshållarna. Platshållarnamnen i mallen förväntas i stället matcha parametrarna. Överväg följande JsonConsole utdata och ordningen på felen.

{
  "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}"
  }
}

Ytterligare loggningsexempel

Följande exempel visar hur du hämtar händelsenamnet, anger loggnivån dynamiskt och formaterar loggningsparametrar. Loggningsmetoderna är:

  • LogWithCustomEventName: Hämta händelsenamn via LoggerMessage attribut.
  • LogWithDynamicLogLevel: Ange loggnivå dynamiskt för att tillåta att loggnivån anges baserat på konfigurationsindata.
  • UsingFormatSpecifier: Använd formatspecificerare för att formatera loggningsparametrar.
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);
    }
}

Överväg exempelloggningsutdata när du använder formateringsfunktionen 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

Överväg exempelloggningsutdata när du använder formateringsfunktionen 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}"
  }
}

Sammanfattning

Med tillkomsten av C#-källgeneratorer är det mycket enklare att skriva högpresterande loggnings-API:er. Att använda källgeneratormetoden har flera viktiga fördelar:

  • Tillåter att loggningsstrukturen bevaras och aktiverar den exakta formatsyntax som krävs av meddelandemallar.
  • Tillåter att du anger alternativa namn för mallplatshållarna och använder formatspecificerare.
  • Tillåter att alla ursprungliga data skickas som de är, utan någon komplikation kring hur de lagras innan något görs med dem (förutom att skapa en string).
  • Tillhandahåller loggningsspecifik diagnostik och genererar varningar för duplicerade händelse-ID:n.

Dessutom finns det fördelar jämfört med att använda LoggerMessage.Define:

  • Kortare och enklare syntax: Deklarativ attributanvändning i stället för kodning av pannplåt.
  • Interaktiv utvecklarupplevelse: Generatorn varnar för att hjälpa utvecklare att göra det rätta.
  • Stöd för ett godtyckligt antal loggningsparametrar. LoggerMessage.Define stöder högst sex.
  • Stöd för dynamisk loggnivå. Detta är inte möjligt med LoggerMessage.Define ensam.

Se även