Passa al contenuto principale

Logging e osservabilità

Astrazione prima dell'implementazione​

Il codice dipende sempre da Microsoft.Extensions.Logging.ILogger<T>, mai da Serilog direttamente. Serilog è il backend (il sink che decide dove i log finiscono), ma il codice applicativo non lo conosce.

// Corretto: dipendenza sull'astrazione
public class OrdineService
{
private readonly ILogger<OrdineService> _logger;

public OrdineService(ILogger<OrdineService> logger)
{
_logger = logger;
}
}
// Sbagliato: dipendenza diretta su Serilog
private readonly Serilog.ILogger _logger = Log.ForContext<OrdineService>();

Questo mantiene il Core libero da dipendenze infrastrutturali e rende possibile cambiare backend (o usarne più di uno) senza toccare il codice applicativo.

Serilog​

Serilog si configura al livello più alto della solution: Program.cs del progetto Api o Console. È l'unico punto in cui appare il riferimento diretto a Serilog.

Si configurano i sink in base all'ambiente: console in sviluppo, file o aggregatori strutturati (Seq, Elastic, Application Insights) in produzione.

Livelli di log​

LivelloQuando usarlo
Tracedettagli granulari per diagnostica profonda, solo in sviluppo
Debuginformazioni utili durante lo sviluppo, disabilitate in produzione
Informationeventi significativi del flusso normale: avvio, operazioni completate
Warningsituazioni anomale ma gestite: retry, fallback, dati inattesi
Errorerrori che impediscono un'operazione ma non abbattono il sistema
Criticalerrori che compromettono il funzionamento del sistema

Cosa si logga​

Obbligatorio:

  • avvio e shutdown dell'applicazione
  • eccezioni non gestite, con stack trace completo
  • operazioni sul dominio rilevanti per audit o diagnostica
  • chiamate a servizi esterni, con esito e tempo di risposta

Regola fondamentale sugli errori: l'ultimo log prima del fallimento deve contenere tutte le informazioni necessarie per un rapido troubleshooting: id delle entità coinvolte, chiavi semantiche, utente, stack trace. Chi legge quel log deve poter ricostruire il contesto senza dover cercare altrove.

Catturare un'eccezione per rilanciarla decorata con informazioni di contesto è una buona pratica:

catch (Exception ex)
{
throw new InvalidOperationException(
$"Errore nell'elaborazione dell'ordine {ordineId} per il cliente {clienteId}", ex);
}

Facoltativo (Debug/Trace):

  • dettagli interni ai flussi, utili solo in fase di debug

Mai:

  • dati personali, password, token, chiavi API
  • contenuto integrale di request/response senza sanitizzazione
  • log su database, usare sistemi di log centralizzato (.NET offre middleware dedicati per la cattura delle eccezioni)

Log strutturati​

Serilog usa log strutturati: i valori non si concatenano nella stringa, si passano come proprietà. Questo li rende ricercabili e aggregabili.

// Corretto: log strutturato
_logger.LogInformation("Ordine {OrdineId} creato per cliente {ClienteId}", ordine.Id, cliente.Id);

// Sbagliato: interpolazione di stringa
_logger.LogInformation($"Ordine {ordine.Id} creato per cliente {cliente.Id}");

L'interpolazione vanifica la struttura e genera allocazioni inutili anche quando il livello di log è disabilitato.