Requisiti minimi per un logger personalizzato, con checklist per i test di integrazione

· Aggiornato il: · · Sviluppo Windows, Logging, Test di integrazione, Progettazione di test, Affidabilità

Se puoi usare un framework di logging esistente, quella è la scelta più sicura. Ci sono tuttavia situazioni in cui i vincoli dell’applicazione o le circostanze operative rendono un logger personalizzato inevitabile. La prima cosa su cui le persone si tormentano lì è quanto implementare perché la progettazione sia “né troppo approssimativa né troppo pesante”.

In questo articolo restringiamo il bersaglio ai log applicativi usati per l’investigazione dei fallimenti. Invece di affrontare audit trail, distributed tracing, una piattaforma di metriche e aggregazione cloud tutti insieme, definiamo prima una configurazione minima utile sul campo, e poi illustriamo gli angoli di test di integrazione necessari per rendere quella configurazione genuinamente affidabile.

La conclusione prima di tutto

Le cose essenziali da fissare nella prima versione sono queste.

  • Usare UTF-8 JSON Lines come formato
  • Non infrangere mai la regola di un record per riga
  • I campi richiesti sono timestamp, level, category, message, fields strutturati, sessionId e processId
  • La baseline è un file per processo
  • Usare scritture sincrone a basso volume; a volume più alto usare writer singolo + coda limitata
  • Fare flush sincrono dei record Error / Critical e di inizio/fine sessione
  • Includere rotazione e retention dalla v1
  • Quando la destinazione log non è disponibile, non deviare silenziosamente in un’altra posizione

Restringere a questo livello rende sia l’implementazione che le operazioni molto meno inclini al collasso.

Prima, restringi lo scope

I logger personalizzati tendono a diventare difficili perché provano a gestire tutto fin dall’inizio. Provare a combinare log diagnostici, log di audit, misurazione delle prestazioni, tracing distribuito e analisi del comportamento utente in un unico meccanismo fa esplodere i requisiti all’istante.

Il bersaglio qui è i log diagnostici usati per isolare i fallimenti applicativi. Cioè, diamo priorità alla capacità di tracciare retrospettivamente “quando”, “in quale operazione”, “cosa è successo” e “qual era il contesto in quel momento”. Solo questa restrizione rende le decisioni progettuali iniziali considerevolmente più facili.

I requisiti minimi

1. Il formato è UTF-8 JSON Lines

Puoi tenere i log come testo concatenato, ma diventano difficili da processare meccanicamente in seguito. Viceversa, partire con un formato binario proprietario pesante danneggia l’osservabilità in operatività.

Il compromesso comodo è UTF-8 JSON Lines. Con un record per riga, il file è leggibile come testo e facile da analizzare in seguito con script e strumenti. Anche se una scrittura viene interrotta a metà, è facile isolare quale riga si è rotta — un vantaggio pratico.

2. Fissa i campi richiesti in anticipo

L’insieme minimo di campi da avere è questo:

  • timestamp
  • level
  • category
  • message
  • fields
  • sessionId
  • processId

Un log di sole stringhe message diventa un problema quando in seguito si moltiplicano i criteri di ricerca. Viceversa, troppi campi aumentano il carico sui call site. È più sicuro fissare l’insieme a circa questa dimensione all’inizio, e considerare aggiunte solo quando genuinamente necessarie.

3. Baseline: un file per processo

Una progettazione in cui più processi accodano a un unico file porta con sé più potenziale di incidenti di quanto sembri. Mutua esclusione, scritture parziali, tempistica di rotazione e gestione della terminazione anomala diventano difficili tutti insieme.

Parti con un file per processo come baseline. Se vuoi combinare più processi, è più sicuro aggregare a valle, oppure mettere su esplicitamente un processo di aggregazione dedicato.

4. Dividi la strategia di scrittura per carico

Mentre il volume di log è basso, le scritture sincrone sono più facili da capire e da investigare in caso di fallimento. Forzare l’asincronia può far perdere i log scritti subito prima dell’uscita, o lasciare ambigue le condizioni di flush sulle eccezioni.

D’altra parte, se il volume di log cresce e l’I/O sincrono diventa il collo di bottiglia, adotta writer singolo + coda limitata. Ciò che conta è decidere in anticipo la policy di overflow. Non lasciare vago se droppi i log vecchi, i nuovi o emetti un avviso.

5. Decidi le condizioni di flush

Fare flush sincrono di Error e Critical, più i log di inizio e fine sessione, ripaga nelle investigazioni dei fallimenti. Fare flush di tutto fino agli Info di routine rallenta, quindi non trattare tutto allo stesso modo è la scelta realistica.

6. Includi rotazione e retention dalla v1

La rotazione è spesso considerata qualcosa da “aggiungere in seguito”, ma è una funzionalità la cui assenza fa male all’improvviso una volta in operatività. Lo schema può essere qualsiasi — per dimensione, giornaliero, per avvio — ma al minimo devi trovarti in uno stato in cui “non cresce senza limiti” e “quanti file vengono conservati” siano decisi.

7. Nessuno storage di fallback improvvisato in caso di fallimento del salvataggio

Una progettazione che scrive silenziosamente da qualche altra parte quando la destinazione log non è disponibile rende l’investigazione successiva difficile. Il semplice fatto che i log non siano “nel posto in cui dovrebbero essere” ritarda la risposta iniziale del team operativo a un incidente.

Se il salvataggio fallisce, rendi il fallimento visibile attraverso un canale esplicito: una notifica in-app, il log eventi, lo standard error o simile. Al minimo, evita lo stato in cui “nessuno sa dove siano finiti i log”.

Una configurazione v1 minimale

Per la prima versione, qualcosa del genere spesso basta.

  • UTF-8 JSON Lines
  • Un file per processo
  • Nomi di file per sessione
  • Rotazione per dimensione o per avvio
  • Un limite superiore ai file conservati
  • Flush sincrono di Error / Critical
  • Un’API che accetta fields strutturati

Per tutto ciò che va oltre, aggiungere funzionalità solo dopo che le operazioni reali rivelano “cosa ha effettivamente fatto male” produce un logger più facile da mantenere.

Anti-pattern comuni

Ecco i pattern tipici da evitare.

  • Infilare tutto nella stringa message
  • Condividere lo stesso file tra più processi
  • Andare completamente asincroni senza decidere le condizioni di flush
  • Posticipare rotazione e retention
  • Deviare silenziosamente in un’altra cartella in caso di fallimento del salvataggio
  • Mettere trasmissione di rete o storage DB locale nella v1

Ognuno sembra comodo a prima vista, ma tendono tutti a appesantire l’isolamento e l’operatività.

Pensa ai test di integrazione in termini di file reali, thread reali, processi reali

Un logger è un componente che i soli unit test non possono farti fidare. Verificare solo la formattazione delle stringhe e la serializzazione JSON manca ciò che causa problemi in produzione: I/O, concorrenza, rotazione, flush allo shutdown e errori di permessi.

Quindi i test di integrazione devono verificare con file reali, thread reali e, dove necessario, processi reali. Al minimo, vuoi evitare lo stato di “passa tutti i giorni, ma non è affidabile durante un incidente”.

Elementi di test di integrazione che vale la pena eseguire

Salute di una singola scrittura

  • Ogni riga è esattamente un record JSON?
  • È rileggibile come UTF-8?
  • I campi richiesti sono presenti ogni volta?
  • Un newline incorporato ha rotto un record su più righe?

Concorrenza all’interno dello stesso processo

  • I record restano intatti quando più thread scrivono simultaneamente?
  • Il conteggio dei record non è né inferiore né superiore?
  • Con una coda in uso, l’ordinamento e la perdita si comportano secondo le specifiche?

Comportamento di flush e shutdown

  • Error / Critical sono riflessi immediatamente?
  • La coda è vuota dopo uno shutdown normale?
  • I log finali necessari sopravvivono su percorsi vicini a un’uscita eccezionale?

Rotazione e retention

  • Il logger passa a un nuovo file quando la condizione di rotazione è soddisfatta?
  • I file vecchi oltre il limite di retention vengono eliminati secondo le specifiche?
  • Le righe JSON restano intatte immediatamente prima e dopo la rotazione?

Percorsi di fallimento

  • Comportamento quando la directory di destinazione non esiste
  • Comportamento quando mancano i permessi di scrittura
  • Notifica o valore di ritorno quando una scrittura fallisce in condizioni simili a disco pieno
  • Comportamento in caso di overflow della coda

Gestione di più processi

Se la specifica è un file per processo, allora lo stesso fatto che un altro processo non tenti di accedere allo stesso file può essere esso stesso un target di verifica. Viceversa, con uno schema a processo di aggregazione, la verifica deve includere i fallimenti di passaggio a quel processo.

L’insieme minimo di test da far passare in v1

Provare a fare tutto all’inizio rende i test troppo pesanti. Il minimo da far passare in v1 è circa questo:

  1. Scritture normali da un singolo thread
  2. Scritture simultanee da più thread
  3. Flush di Error / Critical
  4. Rotazione e retention
  5. Notifica di fallimento quando la destinazione non è disponibile
  6. Svuotamento e flush finale allo shutdown normale

Anche solo con questi sei che passano, sei già molto lontano da “un logger che emette stringhe ma non è affidabile in operatività”.

Riassunto

Il primo obiettivo di un logger personalizzato non è la ricchezza di funzionalità ma “essere credibile durante un incidente”. Per arrivarci, è efficace fissare il formato a UTF-8 JSON Lines, tenere stretto l’insieme dei campi richiesti, fare di un file per processo la baseline, e decidere in anticipo flush, rotazione, retention e comportamento in caso di fallimento.

E se quella progettazione funziona davvero deve essere verificata con test di integrazione che usano file reali, thread reali e processi reali. Prima di far crescere l’implementazione, blocca la configurazione minima e l’insieme minimo di test, e il logger diventa facile da far crescere senza sforzo in seguito.

Articoli recenti con gli stessi tag per approfondire argomenti vicini.

Queste pagine collocano l’argomento in un contesto più ampio di servizi e decisioni.

L’articolo è direttamente collegato ai servizi seguenti.

Domande frequenti

Domande che ricorrono nelle consulenze sull’argomento dell’articolo.

Quale formato dovrebbe usare un logger personalizzato?
UTF-8 JSON Lines, con un record per riga, è il compromesso comodo. Un testo concatenato semplice è difficile da processare meccanicamente in seguito, mentre un formato binario proprietario pesante danneggia l'osservabilità in operatività. Con JSON Lines il file resta leggibile come testo, è facile da analizzare con script e strumenti, e anche se una scrittura viene interrotta a metà è facile isolare quale riga si è rotta. L'insieme minimo di campi è timestamp, level, category, message, structured fields, sessionId e processId.
Le scritture di log dovrebbero essere sincrone o asincrone?
Dividere la strategia per carico. Finché il volume di log è basso, le scritture sincrone sono più facili da capire e da investigare in caso di fallimento; forzare l'asincronia può far perdere i log scritti subito prima dell'uscita. Se il volume cresce e l'I/O sincrono diventa un collo di bottiglia, adottare un writer singolo con una coda limitata, e decidere in anticipo la policy di overflow invece di lasciarla vaga. In ogni caso, fare flush sincrono dei record Error e Critical più dei log di inizio e fine sessione.
Cosa dovrebbe fare un logger quando non riesce a scrivere sulla destinazione?
Non deviare silenziosamente i log in un'altra posizione. Una progettazione che scrive altrove quando la destinazione non è disponibile rende l'investigazione successiva difficile, perché il semplice fatto che i log non siano dove dovrebbero essere ritarda la risposta iniziale del team operativo a un incidente. Invece, rendi visibile il fallimento attraverso un canale esplicito come una notifica in-app, il log eventi di Windows o lo standard error, in modo che nessuno si chieda dove siano finiti i log.
Perché un logger personalizzato ha bisogno di test di integrazione, non solo unit test?
Verificare solo la formattazione delle stringhe e la serializzazione JSON manca ciò che causa problemi in produzione: I/O, concorrenza, rotazione, flush allo shutdown e errori di permessi. I test di integrazione devono usare file reali, thread reali e, dove necessario, processi reali. L'insieme minimo che vale la pena superare in v1 copre sei punti: scritture normali da un singolo thread, scritture simultanee da più thread, flush di Error e Critical, rotazione e retention, notifica di fallimento quando la destinazione non è disponibile, e svuotamento più flush finale allo shutdown normale.

Profilo dell’autore

Pagina di presentazione dell’autore dell’articolo.

Go Komura

Rappresentante di KomuraSoft LLC

Specializzato nello sviluppo di software Windows, nella consulenza tecnica e nell’analisi dei malfunzionamenti, soprattutto nei progetti con sistemi esistenti e guasti difficili da riprodurre.

Torna al blog