Questa nota prende a piene mani dal corso From Zero to Hero: Logging in .NET.
Lβinterfaccia ILogger
Γ¨ il cuore del sistema di logging in .NET ed essa fornisce i metodi fondamentali per registrare i messaggi di log.
Creazione del logger
Console Application
Le applicazioni console di solito configurano il logging in Program.cs
usando LoggerFactory
. La configurazione Γ¨ semplice e diretta:
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.SetMinimumLevel(LogLevel.Information).AddConsole();
});
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Console application started");
Web Application
In questo caso il logger viene creato nel builder
var builder = WebApplication.CreateBuilder(args);
builder.Logging...
Il messaggio di log
Quando si logga un messaggio in .NET utilizzando lβinterfaccia ILogger
, il messaggio di log contiene diversi componenti chiave che forniscono informazioni contestuali e dettagliate. Ecco una descrizione approfondita di ciascuno di questi componenti:
1. Category
La categoria Γ¨ un identificatore associato a un logger specifico e tipicamente rappresenta il nome completo (incluso il namespace) della classe o del componente che genera il log.
La categoria aiuta a organizzare i log in base alle aree funzionali dellβapplicazione e consente di filtrare i messaggi di log per categoria.
Come category del log Γ¨ convenzione usare la classe che sta utilizzando il log. Nelle minimal Api o WebApi quello che Γ¨ automaticamente registrato nella DI nelle API Γ¨ sempre Ilogger<NomeClasse>
mentre invece la classe pura ILogger
non viene registrata.
public class MyService
{
public MyService(ILogger<MyService> logger)
{
}
}
In questo esempio, la categoria sarΓ MyService
.
2. Event ID
LβEventId
Γ¨ una struttura opzionale che rappresenta un identificatore univoco per un evento di log specifico.
Consente di associare i log a eventi specifici, facilitando la ricerca e il tracciamento di particolari occorrenze allβinterno del sistema.
PuΓ² includere un numero intero e una stringa descrittiva:
var eventId = new EventId(1001, "WorkStarted");
_logger.LogInformation(eventId, "Doing work");
// OR
_logger.LogInformation(1001, "Doing work");
3. Severity (LogLevel)
Il LogLevel
indica la gravitΓ del messaggio di log. I livelli di log predefiniti in .NET sono:
Trace
: Dettagli molto dettagliati. Utilizzato per diagnosticare problemi specifici.Debug
: Informazioni dettagliate utili per il debugging. Non viene normalmente abilitato in ambienti di produzione.Information
: Informazioni generali sul flusso dellβapplicazione. Utilizzato per tracciare lβesecuzione di operazioni regolari.Warning
: Indicazioni di potenziali problemi o situazioni inaspettate che non interrompono il flusso dellβapplicazione.Error
: Errori che impediscono il completamento di una specifica operazione.Critical
: Errori critici che richiedono unβattenzione immediata. Indicano problemi che potrebbero causare lβinterruzione dellβapplicazione.
4. Message
Il Message
Γ¨ la stringa di testo che descrive lβevento di log. Questo messaggio puΓ² includere informazioni dinamiche formattate tramite parametri:
public void DoWork(string taskName)
{
_logger.LogInformation("Starting task {TaskName}", taskName);
}
Se voglio formattare le stringhe le formattazioni vanno nei parametri del template
logger.logInformation("{Name:S} just turned: {Age:d}", Name, Age)
Attenzione che quando voglio passare dei parametri ai log non devo usare la string interpolation o lo string format altrimenti perdo lβinformazione sui parametri che passo. Per esempio fare questo Γ¨ sbagliato in quanto perdo lβinformazione sulla variabile β30β che ho passato:
var name = "Nick"
var age = 30;
logger.logInformation($"{name} just turned: {age}")
Lβidea Γ¨ che non voglio scrivere solo stringhe semplici ma stringhe con metadati, per esempio nel caso sopra voglio sia la stringa che il valore di β30β in modo che possa poi farci delle query o similari. Il modo corretto Γ¨
var name = "Nick"
var age = 30;
logger.logInformation("{Name} just turned: {Age}", name, age)
Notare che la stringa {Name}
non Γ¨ necessario che si chiami come la variabile, quello che conta Γ¨ lβordine.
Questo concetto Γ¨ utilissimo non nel ConsoleProvider
ma in altri provider piΓΉ complessi, come il json.
5. Exception
LβException
Γ¨ un oggetto opzionale che rappresenta unβeccezione associata al messaggio di log. Viene utilizzato per registrare informazioni dettagliate sugli errori che si verificano nellβapplicazione:
try
{
// Simulate work
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while doing work");
}
6. Log Scope
Il Log Scope
Γ¨ una funzionalitΓ opzionale che consente di creare contesti di log.
Non tutti i provider supportano gli scope, per esempio la Console non li supporta mentre li supporta la JsonConsole
.
Gli scope possono essere utilizzati per correlare i messaggi di log che appartengono alla stessa operazione o transazione:
using (_logger.BeginScope("TransactionId: {TransactionId}", transactionId))
{
// Ogni log qui dentro avrΓ specificato la transaction id
_logger.LogInformation("Starting transaction");
// Perform transaction
_logger.LogInformation("Transaction completed");
}
Mentre la categoria identifica la classe nel suo complesso, lo scope indica delle operazioni o dei metodi allβinterno della stessa categoria.
Esempio completo
Combiniamo tutti questi componenti in un esempio completo:
public class MyService
{
private readonly ILogger<MyService> _logger;
public MyService(ILogger<MyService> logger)
{
_logger = logger;
}
public void DoWork(string taskName)
{
var eventId = new EventId(1001, "WorkStarted");
using (_logger.BeginScope("TransactionId: {TransactionId}", Guid.NewGuid()))
{
_logger.LogInformation(eventId, "Starting task {TaskName}", taskName);
try
{
// Simulate work
_logger.LogDebug("Working on task {TaskName}", taskName);
throw new InvalidOperationException("An error occurred during work");
}
catch (Exception ex)
{
_logger.LogError(ex, "An error occurred while doing task {TaskName}", taskName);
}
_logger.LogInformation("Task {TaskName} completed", taskName);
}
}
}
Log su console
Per esempio un messaggio su console si struttura come segue:
info: Program[0]
Messaggio!
con info
che Γ¨ la gravitΓ , Program
indica la βcategoryβ, [0]
lβ event id e Messaggio!
il messaggio effettivo.
Provider
I provider sono responsabili di determinare dove e come i messaggi di log vengono registrati. .NET offre diversi provider di logging built-in, tra cui:
ConsoleLoggerProvider
: logga i messaggi sulla console.DebugLoggerProvider
: logga i messaggi nella finestra di debug.EventSourceLoggerProvider
: logga i messaggi usando EventSource.EventLogLoggerProvider
: logga i messaggi negli Event Logs di Windows.FileLoggerProvider
: un provider custom comunemente utilizzato per loggare i messaggi su file.
Configurazione di un Provider
La configurazione di un provider di logging in .NET Γ¨ solitamente gestita nel file Program.cs
o Startup.cs
, a seconda del tipo di applicazione. Ecco un esempio per una console application:
class Program
{
static void Main(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddConsole()
.AddDebug();
});
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Application started");
// Application code here
}
}
Per una web application, la configurazione avviene solitamente in Startup.cs
:
public class Startup
{
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILogger<Startup> logger)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
logger.LogInformation("Application configured");
}
}
Creazione di un provider custom
Oltre ai provider built-in, Γ¨ possibile creare provider custom per soddisfare esigenze specifiche. Ecco un esempio di un provider custom che logga i messaggi su un file:
Implementazione di un Provider Custom
- Creare la classe
FileLogger
:
public class FileLogger : ILogger
{
private readonly string _filePath;
private static readonly object _lock = new object();
public FileLogger(string filePath)
{
_filePath = filePath;
}
public IDisposable BeginScope<TState>(TState state) => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
var message = formatter(state, exception);
lock (_lock)
{
File.AppendAllText(_filePath, $"{logLevel}: {message}{Environment.NewLine}");
}
}
}
- Creare la classe
FileLoggerProvider
:
public class FileLoggerProvider : ILoggerProvider
{
private readonly string _filePath;
public FileLoggerProvider(string filePath)
{
_filePath = filePath;
}
public ILogger CreateLogger(string categoryName)
{
return new FileLogger(_filePath);
}
public void Dispose() { }
}
- Configurare il provider custom:
class Program
{
static void Main(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder.AddProvider(new FileLoggerProvider("log.txt"));
});
var logger = loggerFactory.CreateLogger<Program>();
logger.LogInformation("Application started");
// Application code here
}
}
Application Insights
Configurazione
Application Insights Γ¨ una potente soluzione di monitoraggio di Microsoft Azure che permette di raccogliere e analizzare dati telemetrici dalle applicazioni.
Uno dei componenti configurabili di Application Insights Γ¨ il canale di telemetria. InMemoryChannel
Γ¨ uno dei canali di telemetria che memorizza i dati in memoria prima di inviarli ad Application Insights, offrendo una soluzione efficace per gestire la telemetria in scenari con bassa latenza infatti Γ¨ usato per le Console Application standalone.
Questo oggetto ha:
- Bassa Latenza: Memorizza i dati in memoria, riducendo il tempo di attesa prima dellβinvio.
- ConfigurabilitΓ : Permette di configurare dimensioni del buffer e intervalli di invio.
- Resilienza: In caso di fallimento temporaneo della rete, i dati rimangono in memoria fino a quando non possono essere inviati.
Lβesempio seguente scrive i messaggi di log ad
Application insights
in modo estremamente semplice a meno di incollare la connection string.
using var channel = new InMemoryChannel();
try
{
IServiceCollection services = new ServiceCollection();
services.Configure<TelemetryConfiguration>(x => x.TelemetryChannel = channel);
services.AddLogging(x =>
{
x.AddApplicationInsights(
configureTelemetryConfiguration: teleConfig =>
teleConfig.ConnectionString =
"ADD HERE",
configureApplicationInsightsLoggerOptions: _ => { });
});
var serviceProvider = services.BuildServiceProvider();
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();
logger.LogInformation("Hello from console!");
}
finally
{
await channel.FlushAsync(default);
await Task.Delay(1000);
}
Analisi dei dati telemetrici
Una volta configurato e avviato il logging, i dati telemetrici verranno inviati ad Application Insights. Per filtrare i log si utilizza il linguaggio di query Kusto (KQL), esempio
traces
| where timestamp > ago(1h)
| summarize count() by resultCode
Puoi monitorare e analizzare questi dati nel portale di Azure.
- Dashboard di Overview: Fornisce una panoramica delle richieste, delle dipendenze, delle eccezioni e delle metriche chiave.
- Live Metrics Stream: Mostra le metriche in tempo reale per monitorare lβapplicazione mentre Γ¨ in esecuzione.
- Log Analytics:
- Metriche: Analizza le metriche personalizzate inviate dallβapplicazione.
- Analisi delle Eccezioni: Fornisce dettagli approfonditi sulle eccezioni rilevate, inclusi stack trace e contesto dellβerrore.
Monitoraggio
Potrebbe essere utile settare degli allarmi su determinate condizioni che deduco dai miei log, potenzialmente anche aggregando dati, facendo analisi sui percentili e così via.
Per fare questo devo usare unβaltra risorsa di Azure chiamata Monitor
.
Una volta collegata ad Application Insights
devo creare una nuova Alert Rule
e indicare la Condition
, quindi quando voglio triggerare lβalert.
Posso prendere direttamente le eccezioni oppure delle query specifiche usando Custom log search
dove posso indicare la query in linguaggio KQL
di ricerca dei messaggi.
Una volta definita la logica posso utilizzare un Action group
per notificare lβalert, per esempio via mail.
Esempio
Assumiamo che abbia un log che indica quanto ci ha messo una determinata operazione, per esempio
using (_logger.BeginTimedOperation("Weather retrieval"))
{
await Task.Delay(Random.Shared.Next(10, 100));
}
Questo codice scrive Weather retrieval completed in {OperationDurationMs}ms
.
Ora voglio creare un alert se il tempo impiegato Γ¨ maggiore di un k valore nel 95esimo percentile.
Per prima cosa devo scrivere la query KPL:
traces
| where customDimensions.MessageTemplate == "Weather retrieval completed in {OperationDurationMs}ms"
| order by timestamp
| summarize percentile(todouble(customDimensions.OperationDurationMs), 95)
che fornisce, per esempio, che il 95esimo percentile di OperationDurationMs
Γ¨ 229.
Ora inserisco la query in un alert Custom log search
indicando nella Alert logic
come operatore Greater than or equal
e come threshold 300
che Γ¨ il mio valore soglia.
Pacchetti NuGet
In una console application devo esplicitare i pacchetti nuget come segue
<!-- Contiene solo le interfaccie, tipo ILogger -->
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.1" />
<!-- Contiene le implementazioni delle astrazioni, per esempio LoggerFactory, ma senza i provider -->
<PackageReference Include="Microsoft.Extensions.Logging" Version="7.0.0" />
<!-- Contiene il provider per scrivere su console -->
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="7.0.0" />
Invece in una WebApplication
le dipendenze dai log sono implicite e non bisogna specificarle nel csproject.
Log Filters
I log filters sono meccanismi che permettono di controllare quali messaggi di log vengono emessi in base a criteri predefiniti.
Questi filtri possono essere configurati per livello di log, categoria, provider o una combinazione di questi.
I filtri di log in .NET sono configurati per ogni provider di log specifico e possono essere definiti tramite la configurazione dellβapplicazione (ad esempio, appsettings.json
) o tramite codice nel Program.cs
o Startup.cs
.
Essi determinano quali messaggi di log saranno registrati da ciascun provider, in base a:
- Livello di log (
LogLevel
) - Categoria del logger (tipicamente il nome della classe o del namespace)
- Provider di log (Console, File, Debug, ecc.)
Configurazione di Log Filters tramite appsettings.json
Un modo comune per configurare i filtri di log Γ¨ tramite il file di configurazione appsettings.json
. Ecco un esempio di configurazione:
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Information",
"Microsoft.Hosting.Lifetime": "Information",
"System": "Error"
},
"Console": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning"
}
},
"Debug": {
"LogLevel": {
"Default": "Debug"
}
}
}
}
In questo esempio:
- La configurazione generale (
Logging:LogLevel
) imposta il livello di log predefinito aWarning
, il livello di log perMicrosoft
aInformation
e perSystem
aError
. - Il provider Console ha una configurazione specifica che imposta il livello di log predefinito a
Information
e perMicrosoft
aWarning
. - Il provider Debug Γ¨ configurato per registrare i messaggi a partire dal livello
Debug
.
Configurazione di Log Filters tramite codice
Γ possibile configurare i filtri di log anche tramite codice, utilizzando il metodo AddFilter
sugli ILoggerFactory
o ILoggingBuilder
. Ecco un esempio per una console application:
class Program
{
static void Main(string[] args)
{
using var loggerFactory = LoggerFactory.Create(builder =>
{
builder
.AddConsole()
.AddDebug()
.AddFilter("System", LogLevel.Error)
.AddFilter<ConsoleLoggerProvider>("Microsoft", LogLevel.Warning)
.AddFilter<DebugLoggerProvider>("Default", LogLevel.Debug);
});
}
}
In questo esempio:
- Viene aggiunto il provider Console e Debug.
- Un filtro generale per la categoria
System
Γ¨ impostato aError
. - Un filtro specifico per il provider Console per la categoria
Microsoft
Γ¨ impostato aWarning
. - Un filtro specifico per il provider Debug per la categoria
Default
Γ¨ impostato aDebug
.
High Performance Logging
Dato che i metodi di log hanno in ingresso un object[]
quando passo dei parametri ho sempre delle operazioni di boxing.
Negli scenari ad alte performance posso voler evitare queste passando dei parametri giΓ tipizzati correttamente.
LoggerMessage.Define
La prima opzione Γ¨ usare il metodo LoggerMessage.Define<T1..Tn>
che permette di definire una Action
sullo specifico log con i parametri esplicitati del tipo corretto.
Per esempio se voglio loggare un messaggio con un parametro string
, uno decimal
e lβultimo int
posso scrivere
private static readonly Action<ILogger, string, decimal, int, Exception?> _logPayment =
LoggerMessage.Define<string, decimal, int>(
LogLevel.Information,
new EventId(5001, nameof(CreatePayment)),
"Customer {Email} purchased product {ProductId} at {Amount}"
);
e utilizzarlo in questo modo:
_logPayment(_logger, email, amount, productId, null);
Questo metodo ha lo svantaggio di essere estremamente verboso e poco leggibile (esempio devo sempre passare null
nel parametro dellβeccezione).
Source Generator
Per risolvere i problemi di cui sopra Microsoft fornisce in automatico lβattributo [LoggerMessage]
per creare codice autogenerato estremamente performante per ogni singolo log.
Si utilizza creando una extension di ILogger
che esplicita il singolo log che voglio rendere performante:
[LoggerMessage(Level = LogLevel.Information,
EventId = 5001,
Message = "Customer {Email} purchased product {ProductId} at {Amount}")]
public static partial void LogPaymentCreation(
this ILogger logger, string email, decimal amount, int productId);
lβutilizzo Γ¨ semplice:
_logger.LogPaymentCreation(email, amount, productId);
Lo svantaggio di questo metodo Γ¨ che, essendo una extension di ILogger
questo metodo viene suggerito ogni volta che faccio logger.
; se ho 1000 log ad alte performance avrΓ² 1000 extension.
Varie
Boxing e log levels
Gli args
che vengono passati al messaggio di log sono dei object[]
, quindi quando li passo ho un boxing sicuro che potrebbe non servire qualora il minimum log level non sia raggiunto (esempio livello minimo Warning
e sto loggando ad Info
).
La soluzione piΓΉ semplice Γ¨, nelle applicazioni ad alte performance, controllare che il log sia abilitato prima di chiamare il comando:
if (logger.IsEnabled(LogLevel.Inmformation))
logger.LogInformation("Message {Par1} - {Par2}", par1, par2)
oppure un modo piΓΉ elegante per risolvere il problema Γ¨ usare lβattributo LoggerMessage
, vedi capitolo sui Source Generators.
Timed log entries
Posso sfruttare lo using
per creare dei log che aggiungono al messaggio di log anche quanto ci ha messo una determinata operazione.
Per farlo posso creare una classe con delle extension come questa:
public class TimedOperation : IDisposable
{
private readonly ILogger _logger;
private readonly LogLevel _logLevel;
private readonly string _messageTemplate;
private readonly object?[] _args;
private readonly long _startingTimestamp;
public TimedOperation(ILogger logger, LogLevel logLevel, string messageTemplate, object?[] args)
{ _logger = logger;
_logLevel = logLevel;
_messageTemplate = messageTemplate;
_args = new object[args.Length + 1];
Array.Copy(args, _args, args.Length);
_startingTimestamp = Stopwatch.GetTimestamp();
}
public void Dispose()
{ var delta = Stopwatch.GetElapsedTime(_startingTimestamp);
_args[^1] = delta.TotalMilliseconds;
_logger.Log(_logLevel, $"{_messageTemplate} completed in {{OperationDurationMs}}ms", _args);
}
}
public static IDisposable BeginTimedOperation(
this ILogger logger, LogLevel logLevel, string messageTemplate, params object[] args)
{
return new TimedOperation(logger, logLevel, messageTemplate, args);
}
e chiamarla in questo modo:
using (var _ = logger.BeginTimedOperation("Operation {Par1}", par1))
{
// measured operation here
};
Cambiare livello di log runtime
Questa operazione Γ¨ possibile qualora il provider su cui mi sto appoggiando per la mia minimal api o applicazione asp.net fornisca lβhot reload del file appsettings.json
.
Se è così (e tutti i provider normali come Azure o AWS offrono questa cosa) allora basta modificare il file di cui sopra modificando il LogLevel
minimo e automaticamente posso visualizzare piΓΉ o meno log senza alcun riavvio.
Serilog
Serilog Γ¨ la libreria piΓΉ utilizzata in .NET per la gestione dei log ed Γ¨ famosa per la quantitΓ di provider che fornisce (che nel linguaggio di Serilog si chiamano Sink
).
Attenzione che Serilog non eredita da ILogger
di Microsoft ma da una sua implementazione di ILogger
.
Una delle caratteristiche piΓΉ importanti di Serilog Γ¨ che fornisce la possibilitΓ di utilizzare anche un logger statico, che puΓ² quindi essere acceduto in tutta lβapplicazione anche senza DI.
ILogger logger = new LoggerConfiguration()
.WriteTo.Console(theme:AnsiConsoleTheme.Code)
.CreateLogger();
// Da ora in poi posso accedere a Log.Logger in modo statico
Log.Logger = logger;
Structured data
In Serilog se metto la @
prima di una variabile questa viene visualizzata nei log serializzata Json in automatico.
logger.logInformation("New payment {@Payment}", payment)
Se voglio limitare il numero di property che vengono automaticamente serializzata nei log posso usare Decostruct.ByTransform
quando definisco il logger.
Timed operation
In Serilog bisogna aggiungere il pacchetto nuget Serilog.Timings
e poi scrivere
var op = logger.BeginOperation("Begin Operation")
await Task.Delay(100);
logger.Information("End Operation")
// Con questa riga scrive il messaggio sopra aggiungendo "completed in xxx ms"
op.Complete(); //op.Abandon();
Mask sensitive logs
Per prima cosa Γ¨ necessario aggiungere il pacchetto nuget Destructurama.Attributed
e poi aggiungere lβattributo LogMasked
sullβattributo della classe che voglio mascherare nei log.
using System;
using Serilog;
using Destructurama;
using Destructurama.Attributed;
// Definisci una classe con dati sensibili
public class UserLogin
{
public string Username { get; set; }
[LogMasked]
public string Password { get; set; }
}
ILogger logger = new LoggerConfiguration()
.WriteTo.Console()
// obbligatorio per dire a Serilog di usare gli attributi
.Destructure.UsingAttributes()
.CreateLogger();
Log.Information("User login attempt: {@UserLogin}", loginAttempt);
// 2023-06-06 12:34:56 [Information] User login attempt: UserLogin { Username: "user123", Password: "***" }
Con lβattributo posso fare varie mascherature:
- Specifica il Carattere di Mascheramento:
[LogMasked(Mask = "#")]
- Specifica la Lunghezza della Mascheratura:
[LogMasked(Mask = "#", Length = 5)]
- Solo le prime lettere visibili:
[LogMasked(ShowFirst = 2)]
Async logs
Quando noi scriviamo un log questo viene gestito in maniera sincrona fino al provider/sink e poi Γ¨ questo ultimo che penserΓ a come rendere il tutto asincrono, facendo batching e implementando logiche sue.
Non tutti i provider perΓ² forniscono implementazioni asincrone, per esempio la Console gestisce i log in maniera 100% sincrona.
Qualora volessi gestire un provider come asincrono sempre indipendentemente dalla sua implementazione posso usare il pacchetto nuget Serilog.Sinks.Async
.
Utilizzando Serilog.Sinks.Async
, i log vengono messi in una coda e inviati a un sink separato su un thread di background, riducendo cosΓ¬ lβimpatto delle operazioni di I/O sul thread principale dellβapplicazione.
Log.Logger = new LoggerConfiguration()
.WriteTo.Async(a => a.Console())
.CreateLogger();
Log.Information("Log message {Number}", i);
// Chiudi e svuota i log
Log.CloseAndFlush();
Custom sink
Analogamente al logger i MIcrosoft anche in Serilog Γ¨ molto semplice creare un sink custom, basta creare una classe figlia di ILogEventSink
compilando il metodo Emit
public class CustomSink : ILogEventSink
{
private readonly IFormatProvider? _formatProvider;
public CustomSink(IFormatProvider? formatProvider)
{
_formatProvider = formatProvider;
}
public CustomSink() : this(null)
{
}
public void Emit(LogEvent logEvent)
{
var message = logEvent.RenderMessage(_formatProvider);
Console.WriteLine($"{DateTime.UtcNow} - {message}");
}
}
public static class CustomSinkExtensions
{
public static LoggerConfiguration CustomSink(
this LoggerSinkConfiguration sinkConfiguration,
IFormatProvider? formatProvider = null)
{ return sinkConfiguration.Sink(new CustomSink(formatProvider));
}
}
E utilizzare lβextension nel metodo di configurazione dei log
ILogger logger = new LoggerConfiguration()
.WriteTo.NickSink()
.CreateLogger();
Minimal API
Usare Serilog al posto del logger classico in una minimal API Γ¨ molto semplice in quanto usare la DI per iniettare il logger di Serilog invece di quello built-in in questo modo.
var builder = WebApplication.CreateBuilder(args);
Serilog.ILogger logger = new LoggerConfiguration()
.WriteTo.Console(theme:AnsiConsoleTheme.Code)
.CreateLogger();
Log.Logger = logger;
builder.Services.AddSingleton(logger);
var app = builder.Build();
// il logger qui Γ¨ quello di Serilog in quanto lo ho iniettato con la DI sopra in "AddSingleton"
app.MapGet("/", (Serilog.ILogger log) =>
{
log.Information("Hello from the endpoint");
return "Hello World!";
});
app.Run();
Il limite di questo approccio Γ¨ che tutti i log di Microsoft verranno gestiti con il suo logger, mentre i miei con Serilog.
Nella prossima sezione vediamo come far sì che anche Microsoft usi Serilog e, ancora più importante, la mia applicazioni usi sempre ILogger
di Microsoft e Serilog sia solo un provider di questo ultimo.
ASP.NET Core
Uno dei limiti di Serilog Γ¨ la non compatibilitΓ automatica con il logger built-in di Microsoft, quindi lβinterfaccia e i metodi sono diversi.
Vorrei invece usare sempre lβinterfaccia ILogger
di Microsoft e usare Serilog solo come βdettaglioβ implementativo.
Questa operazione puΓ² essere fatta molto semplicemente in ASP.NET Core usando il pacchetto nuget Serilog.AspNetCore
e scrivendo così.
var builder = WebApplication.CreateBuilder(args);
// Definisco il logger che voglio impostando la variabile statica
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.CreateLogger();
// Inietta la variabile statica sopra ovunque io abbia un ILogger di Micorosft
builder.Host.UseSerilog();
Per esempio in questo controller il logger sembra quello di Microsoft ma invece internamente userΓ quello di Serilog.
public WeatherForecastController(ILogger<WeatherForecastController> logger)
{
_logger = logger;
}
Configurazione tramite File
Serilog puΓ² anche essere configurato tramite un file di configurazione JSON, che puΓ² essere particolarmente utile per separare la configurazione dal codice.
Ecco un esempio completo di file appsettings.json
:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Serilog": {
"MinimumLevel": {
"Default": "Information",
"Override": {
"Microsoft.AspNetCore": "Warning"
}
},
"WriteTo": [
{
"Name": "Console"
},
{
"Name": "File",
"Args": {
"path": "logs/log.txt",
"rollingInterval": "Day"
}
},
{
"Name": "ApplicationInsights",
"Args": {
"connectionString": "APPLICATION INSIGHTS CONNECTION STRING",
"telemetryConverter": "Serilog.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights"
}
}
],
"Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"],
"Properties": {
"Application": "LoggingCourse"
}
},
"AllowedHosts": "*"
}
Per utilizzare questo file di configurazione, Γ¨ necessario aggiungere il pacchetto Serilog.Settings.Configuration
:
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.CreateLogger();
Application Insights
Per poter usare come Sink Application Insights, oltre al pacchetto nuget, devo usare la seguente configurazione
{
"Name": "ApplicationInsights",
"Args": {
"connectionString": "APPLICATION INSIGHTS CONNECTION STRING",
"telemetryConverter": "Serilog.Sinks.ApplicationInsights.TelemetryConverters.TraceTelemetryConverter, Serilog.Sinks.ApplicationInsights"
}
}