Come Gestire File di Log Multipli

Una delle caratteristiche più importanti di una applicazione è quella di registrare messaggi di log che ne evidenzino il comportamento durante l’esecuzione. Queste registrazioni consentono di segnalare e persistere messaggi di errore e di avviso, nonché messaggi informativi (ad esempio statistiche di runtime) in modo che i messaggi possano essere successivamente recuperati e analizzati. L’oggetto che esegue la registrazione nelle applicazioni è in genere chiamato logger.

Un Caso d’Uso

In questo articolo presentiamo una soluzione basata sulle Java Logging API, disponibili nativamente nel JDK, per l’organizzazione dei messaggi di log su file distinti, allo scopo di semplificare l’accesso, il recupero e la distribuzione delle informazioni. Per comprendere le motivazioni che possono rendere necessarie organizzare la struttura dei log applicativi in questo modo si consideri lo scenario mostrato nella figura seguente.

Si tratta di una configurazione abbastanza comune, in cui in un server centrale sono collezionate tutte le operazioni che vengono eseguite su sistemi periferici (client) distribuiti sul territorio. Tipicamente il server espone un set di API (SOAP o REST) che sono invocate dai client dopo essersi autenticati con uno dei diversi meccanismi disponibili (base-authentication, mutua autenticazione con certificati, OAuth, etc.).

In questo scenario avere la possibilità di distinguere i log che il server centrale genera in base al sistema periferico che invoca le API può essere molto utile, soprattutto se i parametri che caratterizzano l’architettura, in termini di numero di client e di operazioni eseguite, sono talmente elevate da generare gigabyte di log.

Logging in Java

Come anticipato java include, nel package java.util.logging, le Java Logging API, ovvero un set di funzionalità che consentono di configurare, attraverso diversi parametri, i tipi di messaggi che si intende generare. Le singole classi dell’applicazione possono utilizzare queste API per generare i propi log. In particolare la classe Logger consente di creare e registrare un nuovo logger attraverso il metodo getLogger(), che richiede come parametro il nome del logger. Ad esempio, se si desidera generare un logger per la classe it.javaboss.MyClass, possiamo scrivere:

Il logger così generato fa parte di una gerarchia di logger in cui ogni punto nel nome del logger indica un livello nella gerarchia. Nel nostro caso ad esempio si è generato un logger per la chiave it.javaboss, che è quindi figlio del logger it, che a sua volta è figlio del logger per la stringa vuota. Questo implica che eseguendo la configurazione per un logger appartenente ad uno specifico livello della gerarchia, tale configurazione si riflette su tutti i suoi figli.

Nei seguenti paragrafi vediamo quali sono i diversi parametri che possono essere utilizzati per eseguire la configurazione di un logger.

Livello di Logging

I livelli di logging definisce la severità dei messaggi che si intende scrivere sul log. La classe Level definisce un set di costanti utili per definire il livello di logging desiderato. Tali costanti, in ordine decrescente di severità, sono le seguenti:

SEVERE (il più alto)
WARNING
INFO
CONFIG
FINE
FINER
FINEST

A queste si aggiungono i livelli OFF e ALL rispettivamente per disattivare la scrittura di tutti i messaggi o per abilitarli tutti. Ad esempio, il codice seguente imposta il logger sul livello INFO, il che significa che verranno registrati tutti i messaggi con livello severe, warning e info:

Log handler

L’handler è responsabile della presa in carico dei messaggi di log e della loro esportazione nel target specificato. Le API dispongono di diversi handler standard, quali:

  • ConsoleHandler: scrivere i messaggi di log sulla console;
  • FileHandler: scrive i messaggi di log su file system;
  • SocketHandler: scrive i messaggi di log su uno network stream. 

Si noti comunque che se il livello del log è impostato ad INFO o ad un qualsiasi livello superiore i messaggi verranno comunque scritti automaticamente anche sulla console, indipendentemente dall’handler specificato.

Log Formatter

Ad ogni handler è associato un formatter il quale è responsabile della creazione, a partire dal messaggio, della stringa che sarà poi esportata dall’handler. Anche in questo caso le API mettono a disposizione diversi formatter standard, due dei quali sono:

  • SimpleFormatter: genera tutti i messaggi come testo;
  • XMLFormatter: genera output XML per i messaggi di log.

E’ anche possibile scrivere il proprio formatter al fine di personalizzare il formato del log prodotto.

Soluzione Applicativa

A partire dalla descrizione delle Java Logging API, vista sopra, mostriamo una possibile soluzione che ci consente di risolvere il caso d’uso presentato inizialmente. La soluzione si basa sull’utilizzo di due semplici classi:

  1. una classe LogConfiguration che utilizzeremo per descrivere i parametri caratteristici del log che si vuole utilizzare;
  2. una classe LoggerFactory per la generazione dell’oggetto logger da restituire a chi ne fa richiesta.

L’implementazione della prima classe è molto banale ed è stata realizzata utilizzando il pattern Builder. I parametri che caratterizzano i logger che vogliamo generare sono sostanzialmente quelli che abbiamo visto sopra ovvero: il livello di logging e la formattazione che vogliamo dare al file. A questi si aggiunge una chiave che identifica il sistema cui il log si riferisce e, trattandosi di log su file system, la dimensione massima del file ed il numero massimo di file da conservare nello storico. L’implementazione suggerita è la seguente:

Utilizzando la classe LogConfiguration è possibile generare configurazioni per i nostri log con una sola linea di codice:

La classe LogFactory, infine, espone un unico metodo pubblico di tipo statico che restituisce un oggetto java.util.logging.Logger a partire dalla configurazione richiesta: getLogger():

Tutti i log generati sono conservati in una mappa di tipo ConcurrentHashMap che consente l’accesso concorrente ai suoi dati e che utilizza la proprietà key come chiave. Se il log non è presente nella mappa, viene generato attraverso il metodo privato createLogger():

Il metodo Logger.getLogger() restituisce sempre un logger per la chiave specificata, e questo è il motivo per cui i logger generati precedentemente sono conservati nella mappa. La logica applicativa che consente di suddividere i vari log su file differenti è concentrata nel metodo createHandler() che restituisce l’handler per il log richiesto:

In particolare il metodo restituisce un FileHandler ottenuto valorizzando i seguenti:

  • pattern che caratterizza il nome e la posizione del file di output;
  • limit che definisce la dimensione massima in byte del file;
  • count che limita il numero massimo di file da conservare;
  • append che specifica se i messaggi vanno appesi ai file già esistenti.

Per comprendere il comportamento di tale implementazione, consideriamo il seguente codice di esempio:

Come risultato otterremo due insiemi di file di log: 01234.log.X e 56789.log.X, dove X è un placeholder per un intero che va da 0 al numero massimo di file ammessi per ciascuna configurazione (proprietà number della classe LogConfiguration). Il contenuto dei due log, coerentemente con la loro definizione sarà quindi:

01234.log.0:
56789.log.0:

Codice Sorgente

Il codice sorgente del progetto è disponibile qui Logger.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.

*