5 Modi Semplici ed Efficaci per Utilizzare il Logging in Python

5 Simple and Effective Ways to Use Logging in Python

Usare Python Logging come un professionista

Immagine creata dall'autore

Scommetto che quasi tutti gli sviluppatori Python usano “print” per il debugging. Non c’è niente di sbagliato in ciò per il prototipazione, ma per la produzione ci sono modi molto più efficaci per gestire i log. In questo articolo, mostrerò cinque motivi pratici per cui “logging” di Python è molto più flessibile e potente e perché assolutamente dovresti usarlo se non lo hai già fatto.

Entriamo nel vivo della questione.

Codice

Per rendere le cose più pratiche, consideriamo un esempio pratico. Ho creato una piccola applicazione che calcola una regressione lineare per due liste Python:

import numpy as npfrom sklearn.linear_model import LinearRegressionfrom typing import List, Optionaldef do_regression(arr_x: List, arr_y: List) -> Optional[List]:    """ Regressione lineare per liste X e Y """    try:        x_in = np.array(arr_x).reshape(-1, 1)        y_in = np.array(arr_y).reshape(-1, 1)        print(f"X: {x_in}")        print(f"y: {y_in}")        reg = LinearRegression().fit(x_in, y_in)        out = reg.predict(x_in)        print(f"Out: {out}")        print(f"Score: {reg.score(x_in, arr_y)}")        print(f"Coef: {reg.coef_}")        return out.reshape(-1).tolist()    except ValueError as err:        print(f"ValueError: {err}")    return Noneif __name__ == "__main__":    print("App started")    ret = do_regression([1,2,3,4], [5,6,7,8])    print(f"Risultato della regressione lineare: {ret}")

Questo codice funziona, ma possiamo fare di meglio? Chiaramente sì. Vediamo cinque vantaggi nell’utilizzare il “logging” invece del “print” in questo codice.

1. Livelli di logging

Cambiamo un po’ il nostro codice:

import loggingdef do_regression(arr_x: List, arr_y: List) -> Optional[List]:    """Regressione lineare per liste X e Y Python"""    try:        x_in = np.array(arr_x).reshape(-1, 1)        y_in = np.array(arr_y).reshape(-1, 1)        logging.debug(f"X: {x_in}")        ...    except ValueError as err:        logging.error(f"ValueError: {err}")    return Noneif __name__ == "__main__":         logging.basicConfig(level=logging.DEBUG, format='%(message)s')    logging.info("App started")    ret = do_regression([1,2,3,4], [5,6,7,8])    logging.info(f"Risultato della regressione lineare: {ret}")

Qui ho sostituito le chiamate “print” con le chiamate “logging”. Abbiamo apportato una piccola modifica, ma rende l’output molto più flessibile. Utilizzando il parametro “level”, possiamo ora impostare diversi livelli di logging. Ad esempio, se usiamo “level=logging.DEBUG”, allora tutti gli output saranno visibili. Quando siamo sicuri che il nostro codice sia pronto per la produzione, possiamo cambiare il livello in “logging.INFO” e i messaggi di debug non verranno più visualizzati:

Livello di debug “INFO” a sinistra e “DEBUG” a destra, Immagine di autore

E ciò che è importante è che non è necessario apportare alcuna modifica al codice, tranne l’inizializzazione del logging stesso!

A proposito, tutte le costanti disponibili possono essere trovate nel file logging / __init__.py:

ERROR = 40WARNING = 30INFO = 20DEBUG = 10NOTSET = 0

Come possiamo vedere, il livello “ERROR” è il più alto; abilitando il livello di log “ERROR”, possiamo sopprimere tutti gli altri messaggi e verranno visualizzati solo gli errori.

2. Formattazione

Come possiamo vedere dall’ultima schermata, è facile controllare l’output del logging. Ma possiamo fare molto di più per migliorarlo. Possiamo anche regolare l’output fornendo la stringa di “formato”. Ad esempio, posso specificare la formattazione in questo modo:

logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] %(filename)s:%(lineno)d: %(message)s')

Senza apportare altre modifiche al codice, sarò in grado di vedere i timestamp, i nomi dei file e persino i numeri di riga in output:

Logging output, Image by author

Ci sono circa 20 diversi parametri disponibili, che possono essere trovati nel paragrafo “Attributi LogRecord” del manuale.

3. Salvataggio dei log su un file

Il logging di Python è un modulo molto flessibile e la sua funzionalità può essere facilmente espansa. Diciamo che vogliamo salvare tutti i nostri log su un file per analisi future. Per farlo, dobbiamo aggiungere solo due righe di codice:

logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] %(message)s', handlers=[logging.FileHandler("debug.log"), logging.StreamHandler()])

Come possiamo vedere, ho aggiunto un nuovo parametro “handlers”. Uno StreamHandler visualizza il log sulla console e il FileHandler, come possiamo immaginare dal suo nome, salva lo stesso output sul file.

Questo sistema è davvero flessibile. Sono disponibili molti diversi oggetti “handler” in Python e incoraggio i lettori a consultare il manuale da soli. E come sappiamo già, il logging funziona quasi automaticamente; non sono richieste ulteriori modifiche al codice.

4. Rotazione dei file di log

Salvare i log su un file è una buona opzione, ma ahimè lo spazio su disco non è illimitato. Possiamo facilmente risolvere questo problema utilizzando il file di log rotativo:

from logging.handlers import TimedRotatingFileHandler...if __name__ == "__main__":    file_handler = TimedRotatingFileHandler(            filename="debug.log",            when="midnight",            interval=1,            backupCount=3,        )    logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] %(message)s', handlers=[file_handler, logging.StreamHandler()])

Tutti i parametri sono autoesplicativi. Un oggetto TimedRotatingFileHandler creerà un file di log, che verrà cambiato ogni mezzanotte, e verranno conservati solo gli ultimi tre file di log. I file precedenti verranno rinominati automaticamente con qualcosa come “debug.log.2023.03.03”, e dopo un intervallo di 3 giorni, verranno eliminati.

5. Invio di log tramite socket

Il logging di Python è sorprendentemente flessibile. Se non vogliamo salvare i log su un file locale, possiamo semplicemente aggiungere un gestore socket, che invierà i log a un altro servizio utilizzando un IP e una porta specifici:

from logging.handlers import SocketHandlerlogging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] %(message)s', handlers=[SocketHandler(host="127.0.0.1", port=15001), logging.StreamHandler()])

Ecco fatto; non sono richieste ulteriori modifiche al codice!

Possiamo anche creare un’altra applicazione che ascolterà sulla stessa porta:

import socketimport loggingimport pickleimport structfrom logging import LogRecordport = 15001stream_handler = logging.StreamHandler()def create_socket() -> socket.socket:    """Creare il socket"""    sock = socket.socket(socket.AF_INET)    sock.settimeout(30.0)    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)    return sockdef read_socket_data(conn_in: socket.socket):    """Leggere i dati dal socket"""    while True:        data = conn_in.recv(4)  # Dati: 4 byte di lunghezza + corpo        if len(data) > 0:            body_len = struct.unpack(">L", data)[0]            data = conn_in.recv(body_len)            record: LogRecord = logging.makeLogRecord(pickle.loads(data))            stream_handler.emit(record)        else:            logging.debug("Connessione socket persa")            returnif __name__ == "__main__":    logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] %(message)s', handlers=[stream_handler])    sock = create_socket()    sock.bind(("127.0.0.1", port))  # Solo connessioni locali    sock.listen(1)  # Un solo client può essere connesso    logging.debug("Thread di ascolto dei log avviato")    while True:        try:            conn, _ = sock.accept()            logging.debug("Connessione socket stabilita")            read_socket_data(conn)        except socket.timeout:            logging.debug("Ascolto socket: nessun dato")

La parte complicata qui è utilizzare il metodo emit, che aggiunge tutti i dati remoti ricevuti da un socket a un attivo StreamHandler.

6. Bonus: Filtri di registro

Infine, un piccolo bonus per i lettori che sono stati abbastanza attenti da leggere fino a questa parte. È anche facile aggiungere filtri personalizzati ai registri. Supponiamo di voler registrare solo i valori X e Y nel file per un’analisi futura. È facile creare una nuova classe di filtro, che salverà nel registro solo le stringhe contenenti “x:” o “y:” record:

from logging import LogRecord, Filterclass DataFilter(Filter):    """Filtro per i messaggi di registro"""    def filter(self, record: LogRecord) -> bool:        """Salva solo i dati filtrati"""        return "x:" in record.msg.lower() or "y:" in record.msg.lower()

Poi possiamo facilmente aggiungere questo filtro al registro del file. La nostra uscita della console rimarrà intatta, ma il file avrà solo valori “x:” e “y:”.

file_handler = logging.FileHandler("debug.log")file_handler.addFilter(DataFilter())logging.basicConfig(level=logging.DEBUG,                     format='[%(asctime)s] %(message)s',                    handlers=[file_handler, logging.StreamHandler()])

Conclusione

In questo breve articolo, abbiamo imparato diversi modi facili per incorporare i registri nell’applicazione Python. La registrazione in Python è un framework molto flessibile e vale sicuramente la pena di dedicare del tempo per capire come funziona.

Grazie per la lettura e buona fortuna con i futuri esperimenti.

Se hai apprezzato questa storia, sentiti libero di iscriverti a Nisoo e riceverai notifiche quando verranno pubblicati i miei nuovi articoli, nonché l’accesso completo a migliaia di storie di altri autori.