Browse

Il logging strutturato in FastAPI non è più un optional, ma una componente fondamentale per garantire visibilità completa, tracciabilità e rapidità nella risoluzione di errori HTTP in applicazioni REST moderne. A livello Tier 2, emerge la necessità di un middleware specializzato, capace di intercettare richieste e risposte con serializzazione JSON controllata, integrazione con OpenTelemetry per tracing distribuito, correlazione automatica di trace ID e client IP, e filtraggio dinamico dei log in base all’ambiente di produzione. Questo approfondimento, ispirato all’esigenza di monitoraggio avanzato descritto nel Tier 2 “Logging strutturato con correlazione trace ID e tracing distribuito”, illustra passo dopo passo come implementare un middleware esperto che eleva il livello di osservabilità oltre il semplice logging, verso una vera piattaforma di diagnosticazione in tempo reale.

---

## 1. Introduzione al Logging Strutturato in FastAPI
Il logging tradizionale, basato su stringhe testuali non strutturate, rende praticamente impossibile il parsing automatico e la correlazione con sistemi avanzati come Sentry, Grafana o ELK. Il logging strutturato, invece, utilizza formati JSON controllati per registrare informazioni chiave con timestamp precisi, path richiesti, stato HTTP e, soprattutto, trace ID univoci che consentono di ricostruire il percorso di una richiesta attraverso microservizi. Questo approccio è essenziale in architetture distribuite dove un singolo errore può attraversare decine di componenti. Il formato JSON standardizzato garantisce interoperabilità con strumenti di analisi e tracciamento, trasformando i log da semplici cronologie in dati attivi per il monitoraggio proattivo.

**Campo critico:** Senza trace ID correlati, il debug di un errore HTTP in un sistema distribuito è come cercare un ago in un pagliaio. L’integrazione con OpenTelemetry, già citata nel Tier 2, consente di generare automatamente trace ID coerenti con ogni richiesta, collegabili ai log e alle metriche.

---

## 2. Architettura del Middleware di Logging Strutturato
Il middleware funge da gatekeeper tra la pipeline di richieste e la registrazione, intercettando ogni chiamata prima dell’esecuzione e dopo la risposta. La sua logica si basa su un ciclo chiaro: ricezione della richiesta, creazione del log iniziale con dati contestuali, esecuzione del percorso, logging della risposta con stato e durata, e infine gestione delle eccezioni con log di errore dettagliato.

In FastAPI, il middleware è implementato tramite il decoratore `@app.middleware("http")`, che accetta un ciclo di vita sincrono e asincrono perfettamente integrato con il framework. Il ciclo vitale prevede:

- **Log iniziale:** cattura header client, metodo HTTP, path, IP, timestamp UTC.
- **Esecuzione richiesta:** invio della chiamata interna all’app e cattura risposta.
- **Log risposta:** registrazione status, duration in ms, trace ID, path e header, eccezioni se presenti.
- **Gestione errori:** log di errore con stack trace (se disponibile) e propagazione errori controllati.

L’integrazione con il logger Python standard (`logging.getLogger`) è configurata per output JSON, livelli configurabili (INFO, WARNING, ERROR) e, crucialmente, supporto asincrono tramite `QueueHandler` per evitare blocchi nella thread principale—una necessità in ambienti ad alto carico dove ogni ms conta.

---

## 3. Implementazione Passo-Passo del Middleware di Logging Strutturato
### 3.1 Definizione della Classe Middleware Personalizzata

from fastapi import FastAPI, Request, Response
import logging
import json
from datetime import datetime
from fastapi.middleware.base import BaseHTTPMiddleware
from typing import Optional

class StructuredLoggingMiddleware(BaseHTTPMiddleware):
def __init__(self, app, logger: logging.Logger, level=logging.INFO):
super().__init__(app)
self.logger = logger
self.level = level

async def __call__(self, scope: dict) -> Response:
request = Request(scope)
self.log_request(request)

try:
response = await self.app(scope)
self.log_response(request, response, status=response.status_code)
except Exception as e:
self.log_error(request, e)
response = Response(status_code=500, body="{"error":"Internal Server Error","message":"{}"}".format(str(e)))
self.log_response(request, response, status=500, exception=e)
return response

def log_request(self, request: Request):
log_entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"method": request.method,
"path": request.url.path,
"headers": dict(request.headers.get("X-Forwarded-For", request.client.host), request.headers),
"client_ip": request.client.host if request.client else "unknown"
}
self.logger.log(self.level, json.dumps(log_entry, ensure_ascii=False))

def log_response(self, request: Request, response: Response, status: int, exception: Exception = None):
log_entry = {
"timestamp": datetime.utcnow().isoformat() + "Z",
"method": request.method,
"path": request.url.path,
"status": status,
"duration_ms": round(response.elapsed.total_seconds() * 1000),
"status_text": response.status_text,
"headers": dict(response.headers.get("X-Forwarded-For", response.client.host), response.headers)
}
if exception:
log_entry["exception"] = str(exception)
log_entry["trace_id"] = request.headers.get("x-trace-id") or "N/A"
self.logger.log(self.level, json.dumps(log_entry, ensure_ascii=False))

### 3.2 Registrazione e Configurazione del Middleware in FastAPI
Il logger Python base è configurato per output JSON, livello dinamico e handler asincroni per garantire prestazioni:

app = FastAPI()
logging.basicConfig(level=logging.INFO)
queue_handler = logging.handlers.QueueHandler()
app.add_middleware(StructuredLoggingMiddleware, logger=logging.getLogger("fastapi.logging"), level=logging.INFO)
app.add_middleware(queue_handler)

Il middleware viene registrato con il logger dedicato `fastapi.logging`, che può essere esteso per inviare log a file, database o endpoint di tracciamento.

### 3.3 Test Funzionale con Richieste Simulate
Per validare il comportamento, si possono simulare:
- Richieste valide: `GET /api/users` (status 200)
- Richieste errate: `GET /api/invalid` (status 404)
- Richieste con errori interni: endpoint che solleva `DatabaseConnectionError`

Ogni log deve contenere:
- Timestamp UTC preciso
- Trace ID univoco (generato tramite header `x-trace-id` o N/A)
- Trace ID correlato ai log downstream via OpenTelemetry
- Dettagli sul client IP e metodo HTTP

---

## 4. Analisi Approfondita del Tier 2: Configurazione Avanzata per Error Handling Distribuito
Il Tier 2 evidenzia che il logging strutturato non è solo registrazione, ma parte di un ecosistema di tracing distribuito. L’integrazione con OpenTelemetry permette di generare trace ID coerenti e propagati in header (`x-trace-id`, `x-span-id`), associabili ai log e alle metriche. Questo consente di ricostruire end-to-end il percorso di una richiesta attraverso microservizi, tracciando latenze, errori e cause radice.

### 4.1 Generazione e Propagazione del Trace ID
Il middleware può estrarre o generare automaticamente il trace ID da header esistenti o, in assenza, crearne uno nuovo:

import uuid

def get_or_generate_trace_id(request: Request) -> str:
trace_id = request.headers.get("x-trace-id")
if not trace_id:
trace_id = str(uuid.uuid4())
request.headers["x-trace-id"] = trace_id
return trace_id

Questo valore viene incluso in ogni log e propagato nei downstream.

### 4.2 Middleware Multi-Livello e Routing Focalizzato
Un middleware avanzato può combinare logging base con riconoscimento di errori HTTP:

async def __call__(self, scope: dict) -> Response:
request = Request(scope)
trace_id = get_or_generate_trace_id(request)
self.log_request(request, trace_id=trace_id)

try:
response = await self.app(scope)
self.log_response(request, response, status=response.status_code, trace_id=trace_id)
except Exception as e:
self.log_error(request, e, trace_id=trace_id)
response = Response(status_code=500, body=json.dumps({"error": "Internal Server Error"}))
self.log_response(request, response, status=500, exception=e, trace_id=trace_id)
return response

Questa architettura consente di arricchire i log in base al tipo di errore (4xx vs 5xx), ad esempio con dati aggiuntivi in 5xx per analisi predittiva.

### 4.3 Filtri Condizionali e Logging Dinamico
Il logging può essere configurato per escludere log di debug in produzione:

if self.level <= logging.WARNING:
# log solo warning e superiori

Inoltre, si può abilitare un sistema di alerting automatico basato su soglie:
- >10 errori 5xx in 5 min → trigger di alert via webhook o sistema di monitoraggio