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.