1. Thread safety del logging
Nelle applicazioni a thread singolo tutto è semplice: un solo thread scrive i log e nessuno lo ostacola. Nelle applicazioni reali — web service, microservizi — girano invece decine e centinaia di thread contemporaneamente. Immaginate che più persone scrivano con una penna sulla stessa riga di un quaderno allo stesso tempo — il risultato sarebbe, per usare un eufemismo, illeggibile.
Thread safety (sicurezza dei thread) — è la garanzia che anche se 100500 thread scrivono log contemporaneamente, i messaggi non si confondano, non si uniscano e non vadano persi.
Come è implementato nelle librerie?
Le moderne librerie di logging (Log4j 2, Logback, java.util.logging) sono progettate fin dall’inizio per essere thread-safe. Questo significa:
- Ogni thread può chiamare in sicurezza i metodi del logger.
- All’interno della libreria si usano sincronizzazione e code per evitare che i messaggi si intralcino.
- Anche se più thread scrivono nello stesso file contemporaneamente, i log non si mescoleranno.
IMPORTANTE: Il logger stesso (per esempio l’oggetto Logger di SLF4J o Log4j) può essere usato come campo static final in qualsiasi classe — non causerà problemi di concorrenza.
Esempio
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MultiThreadedLoggerExample {
private static final Logger logger = LoggerFactory.getLogger(MultiThreadedLoggerExample.class);
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 5; i++) {
logger.info("Il thread {} scrive il messaggio {}", Thread.currentThread().getName(), i);
}
};
Thread t1 = new Thread(task, "Primo");
Thread t2 = new Thread(task, "Secondo");
t1.start();
t2.start();
}
}
Nei log vedrete messaggi ordinati da entrambi i thread — senza confusione né sovrapposizioni.
2. Contesto di logging: MDC (Mapped Diagnostic Context)
Immaginate: la vostra applicazione elabora centinaia di richieste contemporaneamente, ciascuna nel proprio thread. Nei log appaiono messaggi, ma non è chiaro quale appartenga a quale richiesta. Vogliamo vedere non solo «che cosa è successo», ma anche con chi e in quale richiesta è successo.
MDC (Mapped Diagnostic Context) è un meccanismo speciale che consente di «allegare» ai log informazioni aggiuntive legate al thread corrente. Tutti i messaggi scritti dal thread ricevono automaticamente questi dati extra.
Esempio: registriamo l’identificativo della richiesta
In un’applicazione web a ogni richiesta si può assegnare un ID univoco (per esempio, UUID). Con MDC questo ID verrà aggiunto automaticamente a tutti i log scritti dal thread che gestisce la richiesta.
Come appare nel codice (SLF4J + Logback):
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.UUID;
public class MdcExample {
private static final Logger logger = LoggerFactory.getLogger(MdcExample.class);
public static void main(String[] args) {
Runnable task = () -> {
// Generiamo un identificatore univoco della richiesta
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId); // lo aggiungiamo a MDC
logger.info("Elaboriamo la richiesta");
doSomeWork();
logger.info("Elaborazione completata");
MDC.clear(); // pulire sempre al termine!
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
static void doSomeWork() {
logger.debug("Eseguiamo il lavoro...");
}
}
Configurazione del formato di log (per esempio, logback.xml):
<encoder>
<pattern>%d{HH:mm:ss} [%thread] %-5level %logger{36} [requestId=%X{requestId}] - %msg%n</pattern>
</encoder>
Risultato:
12:01:23 [Thread-1] INFO MdcExample [requestId=ad8d...f3] - Elaboriamo la richiesta
12:01:23 [Thread-1] DEBUG MdcExample [requestId=ad8d...f3] - Eseguiamo il lavoro...
12:01:23 [Thread-1] INFO MdcExample [requestId=ad8d...f3] - Elaborazione completata
Importante!
- MDC funziona solo all’interno di un singolo thread. Se passate il lavoro a un altro thread (per esempio tramite un thread pool), dovete propagare manualmente i valori di MDC (oppure usare librerie specifiche che lo fanno automaticamente).
- Non dimenticate di pulire MDC! Se non lo pulite, i dati possono «trasferirsi» alla richiesta successiva nello stesso thread (per esempio in un thread pool del web server). Usate MDC.clear() nel blocco finally.
3. Logging nelle applicazioni web
Un’applicazione web non è solo un programma che parte e funziona. È una vera catena di montaggio: le richieste arrivano, vengono elaborate, le risposte partono. E tutto questo — contemporaneamente, a centinaia. Qui il logging non è un lusso, ma una necessità!
Cosa registrare nelle applicazioni web?
- Richieste e risposte HTTP: metodo, URL, parametri, stato della risposta, tempo di elaborazione.
- Errori ed eccezioni: tutti i malfunzionamenti imprevisti, stack trace.
- Eventi di business: registrazione, login, creazione ordine, pagamento, ecc.
- Dettagli tecnici: interazione con il database, servizi esterni, tempo di esecuzione delle operazioni.
Regola principale: fate logging in modo che tra un mese, quando qualcosa si romperà alle 3 di notte, possiate capire che cosa sia andato storto.
Esempio: logging della richiesta HTTP (Spring Boot)
Il modo più semplice è usare un filtro o un aspetto che registri ogni richiesta in ingresso.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@Component
public class RequestLoggingFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
HttpServletRequest httpRequest = (HttpServletRequest) request;
logger.info("Richiesta: {} {}", httpRequest.getMethod(), httpRequest.getRequestURI());
long start = System.currentTimeMillis();
try {
chain.doFilter(request, response); // prosegue nella catena (fino al controller)
} finally {
long duration = System.currentTimeMillis() - start;
logger.info("Risposta inviata, tempo di elaborazione: {} ms", duration);
MDC.clear();
}
}
}
Logging di errori ed eccezioni
Nei framework web (per esempio, Spring) è prassi usare gestori di errori dedicati (@ExceptionHandler) per registrare in modo pulito tutti i malfunzionamenti imprevisti.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(Exception.class)
public String handleException(Exception ex) {
logger.error("Si è verificato un errore: ", ex); // log con lo stack trace completo!
return "error"; // restituiamo la pagina di errore
}
}
Integrazione con i framework web
Quasi tutti i framework web moderni (Spring, Jakarta EE, Micronaut ecc.) si integrano con i logger pronti all’uso. Di solito basta aggiungere al progetto la dipendenza SLF4J/Logback — e tutti i messaggi standard (avvio dell’applicazione, elaborazione delle richieste, errori) verranno registrati automaticamente.
4. Pratica: esempio di logging in un’attività multithread
Aggiungiamo alla nostra applicazione didattica (per esempio, un servizio di elaborazione ordini) l’elaborazione multithread e vediamo come il logging aiuta a mantenere la lucidità.
Esempio: elaborazione degli ordini in più thread con MDC
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.slf4j.MDC;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class OrderProcessingApp {
private static final Logger logger = LoggerFactory.getLogger(OrderProcessingApp.class);
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 5; i++) {
final int orderId = i;
executor.submit(() -> {
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId);
try {
logger.info("Iniziamo l'elaborazione dell'ordine {}", orderId);
processOrder(orderId);
logger.info("Ordine {} elaborato con successo", orderId);
} catch (Exception ex) {
logger.error("Errore durante l'elaborazione dell'ordine " + orderId, ex);
} finally {
MDC.clear();
}
});
}
executor.shutdown();
}
static void processOrder(int orderId) throws InterruptedException {
if (orderId % 2 == 0) {
throw new RuntimeException("Simulazione di errore per ordine pari");
}
Thread.sleep(500); // simulazione di lavoro
}
}
Cosa succede:
- Ogni ordine viene elaborato in un thread separato.
- Per ogni thread viene creato un requestId univoco (tramite MDC).
- Tutti i log relativi a un ordine si possono trovare tramite questo identificatore.
- Gli errori vengono registrati con lo stack completo.
Il formato del log è configurato per mostrare requestId.
5. Dettagli importanti e particolarità
- Thread, pool e MDC. Se lavorate con thread pool (e probabilmente lo fate), ricordate: i thread nel pool vengono riutilizzati! Se dimenticate di pulire MDC, i dati di una richiesta possono finire nei log di un’altra. Chiamate sempre MDC.clear() al termine del lavoro.
- MDC e attività asincrone. Nei framework web asincroni (per esempio, Spring WebFlux) MDC non sempre funziona «pronto all’uso», perché l’elaborazione della richiesta può passare tra thread diversi. Per questi casi esistono estensioni o adapter specifici.
- Logging nei microservizi. Nell’architettura a microservizi è prassi registrare non solo l’identificativo locale della richiesta, ma anche quello globale (traceId), propagato tra i servizi. Questo consente di seguire il percorso della richiesta in tutto il sistema (tracciamento distribuito). A questo scopo si usano spesso sistemi come Zipkin, Jaeger, OpenTelemetry.
6. Dimostrazione: differenza tra System.out.println e il logging
System.out.println stampa semplicemente una riga in console. In un ambiente multithread:
- I messaggi possono mescolarsi.
- Non ci sono informazioni su ora, thread, livello, contesto.
- Non puoi configurare output su file, formato, filtraggio per livello.
Il logger scrive messaggi strutturati, tiene conto dei thread, dei livelli, del formato, supporta output in varie destinazioni (file, console, rete).
Esempio di confronto
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class PrintVsLogger {
private static final Logger logger = LoggerFactory.getLogger(PrintVsLogger.class);
public static void main(String[] args) {
Runnable task = () -> {
for (int i = 0; i < 3; i++) {
System.out.println("System.out: " + Thread.currentThread().getName() + " passo " + i);
logger.info("Logger: passo {}", i);
}
};
new Thread(task, "T1").start();
new Thread(task, "T2").start();
}
}
Conclusione:
- System.out — i messaggi possono arrivare mescolati, senza ora e livello.
- Il logger — ogni messaggio contiene ora, thread, livello; si può filtrare e trovare rapidamente ciò che serve.
7. Errori tipici nel logging nelle applicazioni multithread e web
Errore n. 1: usare System.out.println invece del logger. In un ambiente multithread questo porta a «confusione» in console, impossibilità di filtrare i messaggi e perdita di informazioni di contesto.
Errore n. 2: ignorare MDC o usarlo in modo errato. Se non si usa MDC per propagare l’identificativo della richiesta/utente, i log diventano inutili — impossibile capire a quale richiesta appartenga l’errore. Se si dimentica di pulire MDC, i dati possono «trapelare» in un’altra richiesta.
Errore n. 3: il logger viene creato come variabile locale. Meglio usare private static final Logger — così il logger viene creato una sola volta per classe, non si spreca memoria e si riduce il rischio di errori.
Errore n. 4: logging di dati sensibili. Nei log non devono finire password, carte di credito, dati personali — è una violazione della sicurezza!
Errore n. 5: fare logging solo "INFO" o solo "ERROR". Usate i livelli adeguati: DEBUG per il debugging, INFO per eventi di business, ERROR per gli errori. Non scrivete tutto a un unico livello — altrimenti i log perdono significato.
Errore n. 6: non si registra lo stack trace delle eccezioni. Se si scrive semplicemente logger.error("Errore: " + ex.getMessage()), si perde l’informazione sulla causa. Registrate sempre l’eccezione completa: logger.error("Errore", ex).
Errore n. 7: logger artigianali non thread-safe. Se qualcuno decide di «scrivere il proprio logger» senza sincronizzazione — in un ambiente multithread è quasi garantita la perdita o la corruzione dei log.
GO TO FULL VERSION