Generación de origen de registro en tiempo de compilación
.NET 6 presenta el tipo LoggerMessageAttribute
. Este atributo forma parte del espacio de nombres Microsoft.Extensions.Logging
y, cuando se usa, genera en origen API de registro eficaces. La compatibilidad con el registro de generación de origen está diseñada para ofrecer una solución de registro altamente utilizable y de alto rendimiento para aplicaciones .NET modernas. El código fuente generado automáticamente se basa en la interfaz ILogger junto con la funcionalidad LoggerMessage.Define.
El generador de origen se desencadena cuando LoggerMessageAttribute
se usa en métodos de registro partial
. Al desencadenarse, puede generar automáticamente la implementación de los métodos partial
que está decorando, o bien generar diagnósticos en tiempo de compilación con sugerencias sobre el uso adecuado. La solución de registro en tiempo de compilación suele ser considerablemente más rápida en tiempo de ejecución que los enfoques de registro existentes. Esto se consigue mediante la eliminación de la conversión boxing, las asignaciones temporales y las copias en la medida de lo posible.
Uso básico
Para usar LoggerMessageAttribute
, la clase y el método que consumen deben ser partial
. El generador de código se desencadena en tiempo de compilación y genera una implementación del método 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);
}
En el ejemplo anterior, el método de registro es static
y el nivel de registro se especifica en la definición de atributo. Cuando se usa el atributo en un contexto estático, la instancia de ILogger
es necesaria como parámetro, o bien se debe modificar la definición para usar la palabra clave this
a fin de definir el método como un método de extensión.
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);
}
También puede optar por usar el atributo en un contexto no estático. Considere el ejemplo siguiente donde el método de registro se declara como un método de instancia. En este contexto, el método de registro obtiene el registrador al acceder a un campo ILogger
de la clase que lo contiene.
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);
}
A partir de .NET 9, el método de registro también puede obtener el registrador de un parámetro de constructor principal de ILogger
en la clase contenedora.
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);
}
Si hay un campo ILogger
y un parámetro de constructor principal, el método de registro obtendrá el registrador del campo.
A veces, el nivel de registro debe ser dinámico, en lugar de estar integrado estáticamente en el código. Para ello, omita el nivel de registro del atributo y, en su lugar, solicítelo como un parámetro para el método de registro.
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);
}
Puede omitir el mensaje de registro y se proporcionará String.Empty para el mensaje. El estado contendrá los argumentos, con un formato de pares clave-valor.
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);
}
Observe la salida de registro de ejemplo al usar el formateador 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}."
}
}
Restricciones del método de registro
Al usar LoggerMessageAttribute
en los métodos de registro, se deben seguir algunas restricciones:
- Los métodos de registro deben ser
partial
y devolvervoid
. - Los nombres de métodos de registro no deben comenzar con un carácter de subrayado.
- Los nombres de parámetro de los métodos de registro no deben comenzar con un carácter de subrayado.
- Los métodos de registro no deben definirse en un tipo anidado.
- Los métodos de registro no pueden ser genéricos.
- Si un método de registro es
static
, la instancia deILogger
es necesaria como parámetro.
El modelo de generación de código depende de que el código se compile con un compilador de C# moderno, versión 9 o posterior. El compilador de C# 9.0 pasó a estar disponible con .NET 5. Para actualizar a un compilador de C# moderno, edite el archivo de proyecto para que el destino sea C# 9.0.
<PropertyGroup>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
Para obtener más información, vea Control de versiones del lenguaje C#.
Anatomía del método de registro
La firma ILogger.Log acepta LogLevel y, opcionalmente, Exception, como se muestra a continuación.
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);
}
Como regla general, la primera instancia de ILogger
, LogLevel
y Exception
se trata de forma especial en la firma del método de registro del generador de origen. Las instancias posteriores se tratan como parámetros normales en la plantilla de mensaje:
// 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);
Importante
Las advertencias emitidas proporcionan detalles sobre el uso correcto de LoggerMessageAttribute
. En el ejemplo anterior, WarningLogMethod
notificará una advertencia DiagnosticSeverity.Warning
de SYSLIB0025
.
Don't include a template for `ex` in the logging message since it is implicitly taken care of.
Compatibilidad con nombres de plantilla que no distinguen mayúsculas de minúsculas
El generador realiza una comparación sin distinguir mayúsculas de minúsculas entre los elementos de la plantilla de mensaje y los nombres de argumento del mensaje de registro. Esto significa que, cuando ILogger
enumera el estado, la plantilla de mensaje recoge el argumento, lo que puede hacer que los registros sean más cómodos de consumir:
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");
}
}
Observe la salida de registro de ejemplo al usar el formateador 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}!"
}
}
Orden de parámetros indeterminado
No hay ninguna restricción en el orden de los parámetros del método de registro. Un desarrollador podría definir ILogger
como último parámetro, aunque podría ser un poco extraño.
[LoggerMessage(
EventId = 110,
Level = LogLevel.Debug,
Message = "M1 {Ex3} {Ex2}")]
static partial void LogMethod(
Exception ex,
Exception ex2,
Exception ex3,
ILogger logger);
Sugerencia
El orden de los parámetros de un método de registro no tiene que corresponderse necesariamente con el orden de los marcadores de posición de plantilla. En su lugar, se espera que los nombres de marcador de posición de la plantilla coincidan con los parámetros. Considere la siguiente salida JsonConsole
y el orden de los errores.
{
"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}"
}
}
Ejemplos de registro adicionales
En los ejemplos siguientes se muestra cómo recuperar el nombre del evento, cómo establecer el nivel de registro de forma dinámica y cómo dar formato a los parámetros de registro. Los métodos de registro son:
LogWithCustomEventName
: recuperar el nombre del evento mediante el atributoLoggerMessage
.LogWithDynamicLogLevel
: establecer el nivel de registro dinámicamente, a fin de permitir que el nivel de registro se establezca en función de la entrada de configuración.UsingFormatSpecifier
: usar especificadores de formato para dar formato a los parámetros de registro.
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);
}
}
Observe la salida de registro de ejemplo al usar el formateador 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
Observe la salida de registro de ejemplo al usar el formateador 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}"
}
}
Resumen
Con la llegada de los generadores de origen de C#, es mucho más fácil escribir API de registro de alto rendimiento. El uso del enfoque del generador de origen ofrece varias ventajas clave:
- Permite conservar la estructura de registro y habilita la sintaxis de formato exacta que requieren las plantillas de mensaje.
- Permite proporcionar nombres alternativos para los marcadores de posición de plantilla y usar especificadores de formato.
- Permite pasar todos los datos originales tal cual, sin ninguna complicación en cuanto a cómo se almacenan antes de hacer algo con ellos (aparte de crear un elemento
string
). - Proporciona diagnósticos específicos del registro y emite advertencias para los id. de evento duplicados.
Además, ofrece varias ventajas con respecto al uso manual de LoggerMessage.Define:
- Sintaxis más corta y sencilla: uso de atributos declarativos, en lugar de código reutilizable.
- Experiencia guiada para desarrolladores: el generador emite advertencias para ayudar a los desarrolladores a realizar las acciones adecuadas.
- Compatibilidad con un número arbitrario de parámetros de registro.
LoggerMessage.Define
admite un máximo de seis. - Compatibilidad con el nivel de registro dinámico. Esto no es posible solo con
LoggerMessage.Define
.