Sdílet prostřednictvím


Generování zdroje protokolování v čase kompilace

.NET 6 zavádí LoggerMessageAttribute typ. Tento atribut je součástí Microsoft.Extensions.Logging oboru názvů a při použití generuje výkonná rozhraní API protokolování. Podpora protokolování generování zdrojů je navržená tak, aby poskytovala vysoce použitelné a vysoce výkonné řešení protokolování pro moderní aplikace .NET. Automaticky generovaný zdrojový kód spoléhá na ILogger rozhraní ve spojení s funkcemi LoggerMessage.Define .

Zdrojový generátor se aktivuje při LoggerMessageAttribute použití metod partial protokolování. Při aktivaci je buď schopen automaticky vygenerovat implementaci partial metod, které zdobí, nebo vytvořit diagnostiku při kompilaci s radami o správném použití. Řešení protokolování v době kompilace je za běhu výrazně rychlejší než existující přístupy k protokolování. Dosahuje toho odstraněním boxingu, dočasných přidělení a kopií do maximálního možného rozsahu.

Základní použití

Chcete-li použít LoggerMessageAttribute, spotřebová třída a metoda musí být partial. Generátor kódu se aktivuje v době kompilace a vygeneruje implementaci partial metody.

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

V předchozím příkladu je static metoda protokolování a úroveň protokolu je zadána v definici atributu. Při použití atributu ve statickém kontextu se buď ILogger jako parametr vyžaduje instance, nebo upravte definici tak, aby používala this klíčové slovo k definování metody jako rozšiřující metody.

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

Můžete použít atribut i v nestatickém kontextu. Představte si následující příklad, ve kterém je metoda protokolování deklarována jako metoda instance. V tomto kontextu metoda protokolování získá protokolovací nástroj přístupem k ILogger poli v obsahující třídě.

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

Od rozhraní .NET 9 může metoda protokolování navíc získat protokolovací nástroj z parametru primárního ILogger konstruktoru v obsahující třídě.

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

Pokud existuje pole ILogger i parametr primárního konstruktoru, metoda protokolování získá logger z pole.

Někdy musí být úroveň protokolu dynamická, nikoli staticky integrovaná do kódu. Můžete to udělat tak, že z atributu vynecháte úroveň protokolu a místo toho ji potřebujete jako parametr metody protokolování.

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

Zprávu protokolování můžete vynechat a String.Empty je k dispozici pro tuto zprávu. Stav obsahuje argumenty formátované jako páry klíč-hodnota.

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

Při použití formátovače zvažte příklad výstupu JsonConsole protokolování.

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

Omezení metody protokolu

Při použití LoggerMessageAttribute metod protokolování je potřeba dodržovat některá omezení:

  • Metody protokolování musí být partial a vracet void.
  • Názvy metod protokolování nesmí začínat podtržítkem.
  • Názvy parametrů metod protokolování nesmí začínat podtržítkem.
  • Metody protokolování nemohou být obecné.
  • Pokud je staticmetoda protokolování , ILogger instance je vyžadována jako parametr.

Model generování kódu závisí na kompilaci kódu pomocí moderního kompilátoru jazyka C#, verze 9 nebo novější. Kompilátor C# 9.0 byl dostupný v .NET 5. Pokud chcete upgradovat na moderní kompilátor jazyka C#, upravte soubor projektu tak, aby cílil na C# 9.0.

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

Další informace najdete v tématu Správa verzí jazyka C#.

Anatomie metody protokolu

Podpis ILogger.Log přijímá LogLevel a volitelně i Exception, jak je znázorněno v následujícím příkladu kódu.

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

Obecně platí, že první instance ILogger, a LogLevelException jsou zpracovávány speciálně v protokolu metody podpis zdrojového generátoru. Další instance se považují za normální parametry šablony zprávy:

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

Důležité

Varovná upozornění poskytují podrobnosti o správném použití LoggerMessageAttribute. V předchozím příkladu WarningLogMethod hlásí DiagnosticSeverity.WarningSYSLIB0025.

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

Podpora názvů šablon bez rozlišování malých a velkých písmen

Generátor nerozlišuje malá a velká písmena mezi položkami v šabloně zprávy a názvy argumentů ve zprávě protokolu. To znamená, že při výčtu ILogger stavu se argument vyzvedne šablonou zprávy, která může znamenat, že protokoly budou příjemnější:

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

Při použití JsonConsole formátovače vezměte v úvahu příklad výstupu protokolování:

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

Neurčité pořadí parametrů

Řazení parametrů metody protokolu není nijak omezené. Vývojář může definovat ILogger jako poslední parametr, i když se může zdát trochu divný.

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

Návod

Pořadí parametrů v protokolové metodě nemusí odpovídat pořadí zástupných symbolů v šabloně. Místo toho se očekává, že zástupné názvy v šabloně odpovídají parametrům. Vezměte v úvahu následující JsonConsole výstup a pořadí chyb.

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

Další příklady protokolování

Následující ukázky ukazují, jak načíst název události, dynamicky nastavit úroveň protokolu a formátovat parametry protokolování. Metody protokolování jsou:

  • LogWithCustomEventName: Načtení názvu události prostřednictvím LoggerMessage atributu
  • LogWithDynamicLogLevel: Nastavte úroveň protokolu dynamicky, aby bylo možné nastavit úroveň protokolu na základě vstupu konfigurace.
  • UsingFormatSpecifier: K formátování parametrů protokolování použijte specifikátory formátu.
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);
    }
}

Při použití SimpleConsole formátovače vezměte v úvahu příklad výstupu protokolování:

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

Při použití JsonConsole formátovače vezměte v úvahu příklad výstupu protokolování:

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

Odstraňování citlivých informací z logů

Při protokolování citlivých dat je důležité zabránit náhodnému vystavení. I při generování metod protokolování v době kompilace může protokolování nezpracovaných citlivých hodnot vést k problémům s únikem dat a dodržováním předpisů.

Knihovna Microsoft.Extensions.Telemetry poskytuje pokročilé možnosti protokolování a rozšiřování telemetrie pro aplikace .NET. Rozšiřuje kanál protokolování tak, aby při zápisu protokolů automaticky použil redakci u klasifikovaných dat. Umožňuje vynucovat zásady ochrany dat v celé aplikaci integrací redakce do pracovního postupu protokolování. Je sestavená pro aplikace, které potřebují sofistikované přehledy telemetrie a protokolování.

Pokud chcete povolit redaction, použijte knihovnu Microsoft.Extensions.Compliance.Redaction . Tato knihovna poskytuje redaktory – komponenty, které transformují citlivá data (například vymazáním, maskováním nebo zatřiďováním) tak, aby byly bezpečné pro výstup. Redactory jsou vybrány na základě klasifikace dat, která umožňuje označovat data podle jejich citlivosti (například osobní, soukromé nebo veřejné).

Pokud chcete použít redakci se zdrojovými metodami protokolování, měli byste:

  1. Klasifikujte citlivá data pomocí systému klasifikace dat.
  2. Zaregistrujte a nakonfigurujte redaktory pro každou klasifikaci v kontejneru DI.
  3. Povolte redakci v protokolovacím kanálu.
  4. Zkontrolujte protokoly a ujistěte se, že nejsou vystavena žádná citlivá data.

Pokud máte například zprávu protokolu s parametrem, který je považován za soukromý:

[LoggerMessage(0, LogLevel.Information, "User SSN: {SSN}")]
public static partial void LogPrivateInformation(
    this ILogger logger,
    [MyTaxonomyClassifications.Private] string SSN);

Budete muset mít nastavení podobné tomuto:

using Microsoft.Extensions.Telemetry;
using Microsoft.Extensions.Compliance.Redaction;

var services = new ServiceCollection();
services.AddLogging(builder =>
{
    // Enable redaction.
    builder.EnableRedaction();
});

services.AddRedaction(builder =>
{
    // configure redactors for your data classifications
    builder.SetRedactor<StarRedactor>(MyTaxonomyClassifications.Private);
});

public void TestLogging()
{
    LogPrivateInformation("MySSN");
}

Výstup by měl vypadat takto:

User SSN: *****

Tento přístup zajišťuje, že se protokolují pouze redactovaná data, a to i při použití rozhraní API pro protokolování vygenerované časem kompilace. Pro různé datové typy nebo klasifikace můžete použít různé redaktory a logiku redakce aktualizovat centrálně.

Další informace o klasifikaci dat najdete v tématu Klasifikace dat v .NET. Další informace o redakci a redaktorech naleznete v tématu Redakce dat v .NET.

Shrnutí

S nástupem zdrojových generátorů jazyka C# je psaní vysoce výkonných rozhraní API protokolování jednodušší. Použití přístupu ke zdrojovému generátoru má několik klíčových výhod:

  • Umožňuje zachovat strukturu protokolování a povolit přesnou syntaxi formátu vyžadovanou šablonami zpráv.
  • Umožňuje zadat alternativní názvy zástupných symbolů šablony a použít specifikátory formátu.
  • Umožňuje předávání všech původních dat tak, jak je, bez jakýchkoli komplikací, které by se ukládaly před tím, než se s ním něco děje (kromě vytvoření string).
  • Poskytuje diagnostiku specifickou pro protokolování a generuje upozornění pro duplicitní ID událostí.

Kromě toho existují výhody oproti ručnímu použití LoggerMessage.Define:

  • Kratší a jednodušší syntaxe: Použití deklarativního atributu místo kódování často používaného atributu
  • Prostředí pro vývojáře s asistencí: Generátor poskytuje upozornění, která vývojářům pomůžou udělat správnou věc.
  • Podpora libovolného počtu parametrů protokolování LoggerMessage.Define podporuje maximálně šest.
  • Podpora dynamické úrovně protokolu To není možné jen s LoggerMessage.Define.

Viz také