CodeGym /Corsi /C++ SELF /Logging: livelli info/warn/error, formato unico

Logging: livelli info/warn/error, formato unico

C++ SELF
Livello 67, Lezione 3
Disponibile

1. Logging — non è la stessa cosa di std::cout

Se prima scrivevate programmi «per voi stessi», è molto facile cominciare a stampare di tutto su std::cout: “ho letto l'argomento”, “entrato in if”, “uscito dal ciclo”, “mamma, sono un programmatore”. Funziona? In apparenza sì. Ma appena create una utility CLI, scoprite un fatto sgradevole: spesso std::cout non lo leggono persone ma altri programmi.

Immaginate che la vostra utility stampi una lista di task e l'utente voglia reindirizzare l'output su file: app --list > tasks.txt. Se nello stesso stream scrivete “DEBUG: entered parse_args”, nel file dell'utente finirà spazzatura. L'utente non sarà contento. Voi non sarete contenti. Sarà contento solo il caos.

Quindi introduciamo una disciplina: il risultato del programma va su std::cout, mentre la diagnostica (compresi i log) va su std::cerr. cerr è pensato proprio per questo: separa l'«output normale» dai «messaggi di errore/processo». Per questo motivo, nella pratica molte utility scrivono progresso e avvisi su stderr, e i dati su stdout.

Si può ricordare una semplice analogia. std::cout è «ciò che avete promesso all'utente per contratto». std::cerr è «come brontolate fra voi quando qualcosa va storto».

Piccolo schema dei flussi (per fissare l'idea visualmente):

flowchart LR
    A["Il tuo codice"] -->|risultato| OUT["std::cout (stdout)"]
    A -->|diagnostica/log| ERR["std::cerr (stderr)"]
    OUT --> F["reindirizzamento > file.txt"]
    ERR --> T["terminale / log errori"]

2. Livelli di log: info, warn, error

Quando si parla di log, i principianti spesso vogliono o «non loggare nulla», o «loggare tutto». Entrambi gli approcci finiscono di solito allo stesso modo: comunque non trovate quello che serve. Nel primo caso — perché non ci sono informazioni. Nel secondo — perché le informazioni sono troppe e la riga utile si perde nell'oceano di “entered function”.

Perciò adottiamo un set minimale ma utile di livelli:

  • info — messaggi neutri sul progresso del programma (avviato, modalità rilevata, operazione iniziata).
  • warn — qualcosa di strano, ma il programma continua a funzionare (ad es. l'utente ha passato un'opzione con valore sospetto; oppure una parte dell'input è stata ignorata).
  • error — errore che impedisce di eseguire l'azione (argomenti non validi, comando sconosciuto, parametro obbligatorio mancante).

Importante: il livello non è una questione di «bellezza». È il senso. E il senso influisce su due decisioni: dove stampare e cosa fare dopo.

Fissiamo tutto con una piccola tabella (le tabelle non sono hit‑list, sono un tentativo di salvare gli occhi):

Livello Di cosa parla il messaggio Il programma continua? Tipico stream
INFO
«Cosa stiamo facendo» Sì spesso std::cerr (diagnostica) o disattivato
WARN
«Strano, ma tollerabile» Sì std::cerr
ERROR
«Impossibile eseguire» Di solito no (o si salta il comando in interattivo) std::cerr

In questa lezione non costruiamo un sistema complesso di filtraggio dei log, non aggiungiamo “debug/trace”, non scriviamo su file. Il nostro obiettivo è più semplice: formato unico e struttura minima, così i vostri messaggi sembreranno quelli di un programma maturo, non l'urlo dell'anima su cout.

3. Formato unico del log: stabilità prima della bellezza

Se i log non hanno un formato, ogni volta che cercate qualcosa a occhio perdete tempo a «indovinare come oggi ho deciso di stampare gli errori». Il formato risolve due compiti pratici: scansionare velocemente a occhio e cercare rapidamente nel testo (anche con un semplice Ctrl+F).

Prendiamo un formato semplice:

[LEVEL] message

Ad esempio:

[INFO] mode=interactive
[WARN] unknown option ignored: --colour=neon
[ERROR] --name requires a value

Perché le parentesi quadre? Non è fondamentale. Si poteva usare “INFO:” o “(INFO)”. Ma quando il formato è uno solo, smettete di discutere con voi stessi e iniziate a scrivere codice.

Altro punto importante: il logger non dovrebbe essere sparso per tutto il codice. Cioè non scrivete in ogni punto manualmente std::cerr << "[ERROR]" ... Altrimenti dopo una settimana metà dei messaggi non avrà la ], un quarto non avrà il newline, e un messaggio finirà accidentalmente su cout. Facciamo una sola funzione log(...), e il formato vive in un unico posto.

4. Micro‑template switch: enum class → testo

Ora dobbiamo scegliere una rappresentazione comoda per i livelli. Il modo più semplice (e buono) è enum class. È più sicuro degli «int‑livelli», perché il compilatore non vi lascerà passare per sbaglio 42 al posto di LogLevel::warn.

Cominciamo con la definizione:

enum class LogLevel {
    info,
    warn,
    error
};

Ora vogliamo ottenere il testo "INFO", "WARN", "ERROR". Si può fare con if, ma risulta meno leggibile. Qui è praticamente naturale usare switch: è fatto per scegliere in base a un enum.

Ecco il template minimo, «canonico»:

#include <string_view>

enum class LogLevel { info, warn, error };

std::string_view to_string(LogLevel lvl) {
    switch (lvl) {
    case LogLevel::info:  return "INFO";
    case LogLevel::warn:  return "WARN";
    case LogLevel::error: return "ERROR";
    }
    return "UNKNOWN";
}

Notate l'ultima riga return "UNKNOWN";. Formalmente si potrebbe pensare: «non serve, abbiamo solo tre valori». Ma l'esperienza insegna che un ramo di difesa è come una chiave di scorta. Di solito non serve, finché un giorno improvvisamente non serve.

5. Scriviamo log(...): un punto di formattazione, uno stile

Adesso costruiamo il logger. Versione super‑minimale: stampiamo su std::cerr, perché il log è diagnostica.

#include <iostream>
#include <string_view>

enum class LogLevel { info, warn, error };

std::string_view to_string(LogLevel lvl) {
    switch (lvl) {
    case LogLevel::info:  return "INFO";
    case LogLevel::warn:  return "WARN";
    case LogLevel::error: return "ERROR";
    }
    return "UNKNOWN";
}

void log(LogLevel lvl, std::string_view msg) {
    std::cerr << '[' << to_string(lvl) << "] " << msg << '\n';
}

Questo è già utile, perché:

  1. il formato è in un solo posto;
  2. non dimenticate il livello — è un parametro;
  3. std::string_view permette di passare sia literali che std::string senza copie (purché la stringa viva abbastanza a lungo — ma per la durata della chiamata alla funzione generalmente va bene).

Esempio d'uso:

log(LogLevel::info, "App started");      // [INFO] App started
log(LogLevel::warn, "Suspicious input"); // [WARN] Suspicious input
log(LogLevel::error, "Bad arguments");   // [ERROR] Bad arguments

E sì, avete notato: l'output è mostrato nei commenti — aiuta a confrontare aspettative e realtà, soprattutto per i principianti.

6. std::ostringstream: quando è scomodo «concatenare a mano»

Con i log sorge presto un problema pratico: il messaggio spesso è composto da pezzi. Per esempio:

  • "unknown option: " + arg
  • "mode=" + modeName
  • "cannot open file: " + path

I principianti seguono di solito due strade. La prima — concatenazione con +, la seconda — molti << direttamente su std::cerr. Entrambe a volte vanno bene, ma hanno dei difetti.

La concatenazione con + richiede std::string, e se parte dei dati non è stringa (per esempio int argc), iniziate a ballare con std::to_string. Inoltre si ottiene facilmente un ammasso di parentesi e plus.

Stampare direttamente su std::cerr è possibile, ma il formato del messaggio si disperde nel codice: parte nel logger, parte nel punto di chiamata. È preferibile poter «costruire il messaggio» e poi inviare tutto in una chiamata a log().

Qui entra in gioco std::ostringstream — uno stream per l'output in stringa. Fa parte della famiglia stringstream (insieme a std::stringstream che probabilmente avete già usato).

Mini‑esempio: costruire la stringa «come con <<», non con +.

#include <sstream>
#include <string>

std::string build_argc_message(int argc) {
    std::ostringstream oss;
    oss << "argc=" << argc;
    return oss.str();
}

Uso:

log(LogLevel::info, build_argc_message(argc)); // [INFO] argc=3

Sembra un po' più verboso rispetto a std::to_string(argc), ma questo approccio scala: aggiungere pezzi è comodo, i tipi sono automaticamente formattati dallo stream, e non trasformate il codice in una ragnatela di plus.

7. Integriamo il logging in un'app CLI didattica

Per rendere gli esempi meno astratti, immaginiamo di scrivere una piccola utility chiamata TaskBox (nome convenzionale), che può funzionare in due modalità:

  • una tantum: taskbox --add "Buy milk"
  • interattiva: taskbox --interactive, poi comandi da stdin

Oggi non riscriviamo il parser degli argomenti (è materia di lezioni precedenti), ma mostreremo come inserire il logging in modo pulito.

Loggare il parsing di --interactive senza fanatismi

Se troviamo --interactive, è utile sapere che il programma ha capito la modalità.

#include <string_view>

bool is_interactive = false;

for (int i = 1; i < argc; ++i) {
    std::string_view arg = argv[i];
    if (arg == "--interactive") {
        is_interactive = true;
        log(LogLevel::info, "mode=interactive"); // [INFO] mode=interactive
    }
}

Perché questo è info e non warn? Perché è un percorso normale di funzionamento; semplicemente vogliamo vederlo nella diagnostica quando stiamo facendo debugging o quando l'utente si lamenta «non funziona».

Opzione sconosciuta: è error o warn?

Sottile differenza. Se per contratto avete deciso: «qualsiasi opzione sconosciuta è errore degli argomenti», allora è error e il programma termina. Se invece decidete: «ignoriamo opzioni sconosciute e proseguiamo», è warn.

Nelle CLI didattiche di solito è meglio essere severi: opzione sconosciuta = errore. Così l'utente impara più in fretta a usare correttamente lo strumento.

#include <sstream>
#include <string_view>

if (arg.starts_with("-")) {
    std::ostringstream oss;
    oss << "unknown option: " << arg;
    log(LogLevel::error, oss.str()); // [ERROR] unknown option: --kek
    return 2;
}

Notate che ostringstream è utile qui perché arg è una string_view, e ci permette di costruire la stringa senza concatenazioni manuali.

Ciclo interattivo: un errore di comando non sempre significa «moriamo»

In modalità interattiva spesso la logica è: un comando può fallire, ma non vogliamo chiudere l'intero programma. Allora error significa «errore del comando», ma non necessariamente «termina il processo».

#include <iostream>
#include <string>

std::string line;
while (std::getline(std::cin, line)) {
    if (line == "quit") break;

    if (line.empty()) {
        log(LogLevel::warn, "empty command"); // [WARN] empty command
        continue;
    }

    log(LogLevel::info, "command accepted"); // [INFO] command accepted
}

Qui warn per un comando vuoto è discutibile, ma utile a fini didattici: vedete cosa succede quando l'utente preme semplicemente Enter.

8. Pratica: rumore, info e livello minimo

Al momento stampiamo sempre tutto su std::cerr. Va bene per una versione didattica. Ma nelle utility reali spesso si vuole poter disabilitare gli info (ad es. mostrare per default solo warn/error).

Non introduciamo ancora flag completi come --verbose (è parte di un «CLI ricco»), ma possiamo predisporre un piccolo meccanismo: livello minimo.

LogLevel g_min_level = LogLevel::info;

bool enabled(LogLevel lvl) {
    return static_cast<int>(lvl) >= static_cast<int>(g_min_level);
}

E in log(...):

void log(LogLevel lvl, std::string_view msg) {
    if (!enabled(lvl)) return;
    std::cerr << '[' << to_string(lvl) << "] " << msg << '\n';
}

Un punto da notare: abbiamo usato static_cast<int> perché enum class non si converte automaticamente in int. Questo è normale, ma è importante ricordare: l'ordine dei valori nell'enum definisce il senso del confronto, quindi i valori info/warn/error devono essere in questo ordine.

È facile, sentendosi potenti con i log, cominciare a inserire log(info, "...") in ogni riga di codice. Dopo un giorno avrete 500 righe di output per un'esecuzione, e soprattutto smetterete di considerarli segnali utili.

Regola pratica semplice: loggate eventi, non righe. Un evento è «modalità selezionata», «file letto», «comando non valido ricevuto», «impossibile parseare un argomento». Una riga è «entrato in if» (di solito inutile).

Se volete seguire passo passo l'esecuzione — usate il debugger. I log servono quando volete capire il comportamento del programma senza aprirlo ogni volta.

9. Errori tipici

Errore n.1: scrivere i log su std::cout e rompere l'«output pulito».
È il problema più comune nelle CLI: l'utente si aspetta i dati su stdout (per reindirizzarli su file o passarli ad un altro programma), invece riceve mescolati «risultato» e «diagnostica». Tenete a mente la regola semplice: std::cout — per i risultati, std::cerr — per log ed errori.

Errore n.2: non avere un formato unico e creare un «zoo di messaggi».
Quando una parte dei messaggi appare come Error: ..., un'altra come [ERROR] ..., un'altra come !!! ..., cercare a occhio diventa doloroso. Si risolve facilmente: una funzione log() ed il formato restano lì. Nei punti di chiamata passate solo il significato, non l'ornamento.

Errore n.3: rappresentare il livello come stringa invece che come enum class.
Se il livello è una stringa, potete facilmente scrivere "ERORR" senza accorgervene. Se il livello è enum class LogLevel, il compilatore vi costringe a scegliere solo tra le opzioni valide. La conversione enum→testo si fa con il piccolo switch, più leggibile di qualsiasi serie di if.

Errore n.4: cercare di concatenare messaggi con + e impantanarsi in std::to_string.
La concatenazione di stringhe diventa presto un pasticcio, specialmente quando aggiungete numeri e tipi diversi. In questi casi std::ostringstream offre codice più uniforme e leggibile: scrivete il messaggio come lo stampereste su cout, poi usate oss.str().

Errore n.5: loggare «tutto e subito» e svuotare di valore i log.
Quando i log sono troppi, smettono di essere uno strumento di diagnostica e diventano rumore. Cercate di loggare eventi chiave, non ogni passo dell'algoritmo. Se volete una tracciatura passo passo — di solito è lavoro del debugger, non di log(info, "...").

1
Compito
C++ SELF, livello 67, lezione 3
Bloccato
Logger unificato
Logger unificato
1
Compito
C++ SELF, livello 67, lezione 3
Bloccato
Analisi del livello
Analisi del livello
1
Compito
C++ SELF, livello 67, lezione 3
Bloccato
Comandi con log
Comandi con log
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION