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:
Web Application
In questo caso il logger viene creato nel builder
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.
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:
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:
Se voglio formattare le stringhe le formattazioni vanno nei parametri del template
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:
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 è
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:
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:
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:
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:
Per una web application, la configurazione avviene solitamente in Startup.cs
:
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
:
- Creare la classe
FileLoggerProvider
:
- Configurare il provider custom:
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.
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
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
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
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:
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:
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
e utilizzarlo in questo modo:
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:
l’utilizzo è semplice:
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:
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:
e chiamarla in questo modo:
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.
Structured data
In Serilog se metto la @
prima di una variabile questa viene visualizzata nei log serializzata Json in automatico.
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
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.
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.
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
E utilizzare l’extension nel metodo di configurazione dei log
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.
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ì.
Per esempio in questo controller il logger sembra quello di Microsoft ma invece internamente userà quello di Serilog.
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
:
Per utilizzare questo file di configurazione, è necessario aggiungere il pacchetto Serilog.Settings.Configuration
:
Application Insights
Per poter usare come Sink Application Insights, oltre al pacchetto nuget, devo usare la seguente configurazione