Tus logs en Python de forma eficiente

Esto de los logs es una auténtica maravilla. Se que de inicio, puede parecer algo insustancial y a lo que no le vas a sacar partido, pero, nada mas lejos de la realidad. Inicialmente, lo utilizas por aquello de utilizarlo, pero poco a poco, cada vez que pasa, le sacas mas partido. El uso del módulo logging en Python puede ser una herramienta valiosa para mejorar sus habilidades de programación y facilitar la depuración de errores en su código. Por un lado te ayudará a entender el flujo de tu programa, seguirás las buenas prácticas de programación. Por supuesto que te ayudará a localizar errores en el caso de que estos se produzcan, lo cual es una gran ventaja. Así, tienes muchas razones para hacer uso del logging, y de verdad, que te recomiendo que lo utilices, y que lo utilices cuanto antes.

Tus logs en Python de forma eficiente

Tus logs en Python de forma eficiente

¿Porque utilizar el logging en Python y en general?

Algunas de las ventajas de utilizar el logging tanto en Python como en el mundo de la programación en general serían las siguientes,

  • Ayuda a entender el flujo de su programa: Los registros pueden ayudar a comprender el flujo de su programa y cómo se están procesando los datos. Esto puede ser especialmente útil cuando está trabajando en código complejo o cuando necesita depurar un problema específico.
  • Fomenta una práctica recomendada de programación: El uso de registros es una práctica recomendada de programación que se utiliza en muchos entornos profesionales de desarrollo de software. Aprender a usar registros adecuadamente puede ser una habilidad valiosa que se puede aplicar en futuros proyectos y trabajos.
  • Facilita la depuración de errores: Los registros pueden ser muy útiles para encontrar y corregir errores en su código. Al agregar registros a su código, puede identificar fácilmente las áreas problemáticas y entender lo que está sucediendo en su código en tiempo real.
  • Promueve la escritura de código modular y escalable: Al organizar su código en módulos y escribir registros para cada módulo, puede crear una aplicación modular y escalable que sea más fácil de mantener y depurar a medida que crece y evoluciona.

Sobre el módulo logging de Python

El logging en Python es un módulo que permite registrar eventos y mensajes en un programa. Es útil para el seguimiento y la depuración del código, ya que permite al programador examinar el flujo de ejecución del programa y diagnosticar problemas en tiempo real.

El módulo de registro de Python proporciona una interfaz para que los programadores puedan registrar mensajes de diferentes niveles de importancia, como información de depuración, advertencias y errores. También es posible personalizar los mensajes de registro para incluir información adicional, como la hora en que se registró el mensaje, el nombre del módulo que lo registró y la función que lo generó.

Para utilizar el módulo de registro en Python, es necesario importarlo y configurarlo para que el programa registre los mensajes correctamente. Se pueden configurar diferentes manejadores de registro para escribir los mensajes en diferentes destinos, como un archivo de registro o la salida estándar.

Un ejemplo del uso del logging en Python podría ser el siguiente,

import logging

# Configurar el registro
logging.basicConfig(level=logging.INFO)

# Registrar un mensaje de información
logging.info('Este es un mensaje de información')

# Registrar un mensaje de advertencia
logging.warning('Este es un mensaje de advertencia')

# Registrar un mensaje de error
logging.error('Este es un mensaje de error')

En este ejemplo, el nivel de registro se establece en logging.INFO, lo que significa que se registrarán mensajes de información y niveles superiores. Luego se registran mensajes de información, advertencia y error utilizando la función correspondiente en el módulo de registro.

¿Como utilizar el logging en Python?

Existen diferentes opciones a la hora de implementar el logging en Python, al igual que sucede en otros lenguajes de programación. Algunas buenas prácticas para utilizar el logging de forma efectiva son las siguientes,

  • Configurar el logging al inicio del programa: Es importante configurar el logging al inicio del programa, antes de que se registre cualquier mensaje, para asegurarse de que se esté utilizando la configuración adecuada. La configuración puede incluir el nivel de registro, el formato de los mensajes y los manejadores de registro.
  • Usar diferentes niveles de registro: El módulo de logging permite registrar mensajes con diferentes niveles de importancia, como información de depuración, advertencias y errores. Es importante utilizar los diferentes niveles de registro según sea necesario para ayudar a diagnosticar problemas en el programa.
  • Incluir información adicional en los mensajes de registro: Es útil incluir información adicional en los mensajes de registro, como la hora en que se registró el mensaje, el nombre del módulo que lo registró y la función que lo generó. Esto puede ayudar a identificar la causa del problema más rápidamente.
  • Utilizar diferentes manejadores de registro: Es posible configurar diferentes manejadores de registro para escribir los mensajes en diferentes destinos, como un archivo de registro o la salida estándar. Esto puede ser útil para controlar el flujo de mensajes y asegurarse de que se estén registrando adecuadamente.
  • Evitar el uso excesivo de logging: El registro de mensajes puede tener un impacto en el rendimiento del programa. Es importante utilizar el logging de manera estratégica y solo registrar los mensajes necesarios para diagnosticar problemas en el programa.

¿Que niveles de registro existen y como implementarlos?

Existen varios niveles de registro disponibles en el módulo de logging de Python. Cada nivel de registro se utiliza para registrar mensajes de una cierta importancia o gravedad. Los niveles de registro se enumeran en orden descendente de importancia:

  • CRITICAL: El nivel más alto de registro, se utiliza para mensajes de error críticos que pueden hacer que el programa se detenga.
  • ERROR: Se utiliza para mensajes de error que pueden ser recuperables, pero que indican un problema importante en el programa.
  • WARNING: Se utiliza para mensajes de advertencia que no son críticos, pero que indican un comportamiento inesperado o problemático.
  • INFO: Se utiliza para mensajes informativos que indican el estado del programa o el progreso de la ejecución.
  • DEBUG: Se utiliza para mensajes de depuración que proporcionan información detallada sobre el funcionamiento interno del programa.
  • NOTSET: El nivel más bajo de registro, se utiliza para configurar un manejador de registro sin establecer un nivel de registro específico.

Para implementar estos niveles de registro en Python, se debe configurar el manejador de registro y luego registrar los mensajes utilizando la función correspondiente para el nivel de registro deseado. Aquí hay un ejemplo que utiliza los diferentes niveles de registro:

Por ejemplo,

import logging

# Configurar el registro
logging.basicConfig(level=logging.DEBUG)

# Registrar un mensaje de depuración
logging.debug('Este es un mensaje de depuración')

# Registrar un mensaje de información
logging.info('Este es un mensaje de información')

# Registrar un mensaje de advertencia
logging.warning('Este es un mensaje de advertencia')

# Registrar un mensaje de error
logging.error('Este es un mensaje de error')

# Registrar un mensaje crítico
logging.critical('Este es un mensaje crítico')

En este ejemplo, se establece el nivel de registro en logging.DEBUG, lo que significa que se registrarán mensajes de depuración y niveles superiores. Luego se registran mensajes de depuración, información, advertencia, error y crítico utilizando la función correspondiente en el módulo de registro. Si se quisiera cambiar el nivel de registro, simplemente se debería cambiar el argumento del nivel en la función basicConfig().

¿Que manejadores de registro existen en Python?

En el módulo de logging de Python, existen varios tipos de manejadores de registro que se pueden utilizar para escribir los mensajes de registro en diferentes destinos. Algunos de los manejadores de registro más comunes son:

  • StreamHandler: Este manejador de registro envía los mensajes de registro a una corriente de salida, como la consola o la salida estándar. Se utiliza comúnmente para la depuración durante el desarrollo.
  • FileHandler: Este manejador de registro escribe los mensajes de registro en un archivo en el sistema de archivos. Es útil para mantener un registro permanente de los mensajes de registro.
  • RotatingFileHandler: Este manejador de registro escribe los mensajes de registro en un archivo en el sistema de archivos y rota los archivos cuando alcanzan un tamaño máximo predefinido.
  • SMTPHandler: Este manejador de registro envía los mensajes de registro a través de correo electrónico.
  • NullHandler: Este manejador de registro no realiza ninguna acción y se utiliza comúnmente cuando se desea desactivar completamente el registro.

Para utilizar un manejador de registro en Python, se debe crear una instancia del manejador y agregarlo al objeto Logger utilizando el método addHandler().

A continuación encontrarás un ejemplo de cada uno de los manejadores de registro que te he indicado anteriormente, para que veas como funciona.

StreamHandler

Este ejemplo utiliza el manejador de registro FileHandler para escribir los mensajes de registro en un archivo en el sistema de archivos. El resto del código es similar al ejemplo anterior, pero en lugar de crear un manejador de registro StreamHandler, se crea un manejador de registro FileHandler.

import logging

# Configurar el registro
logger = logging.getLogger('MiApp')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler()

# Establecer el formato de los mensajes de registro
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# Agregar el manejador de registro al objeto Logger
logger.addHandler(handler)

# Registrar un mensaje de depuración
logger.debug('Este es un mensaje de depuración')

# Registrar un mensaje de información
logger.info('Este es un mensaje de información')

RotatingFileHandler

Este ejemplo utiliza el manejador de registro RotatingFileHandler para escribir los mensajes de registro en un archivo en el sistema de archivos y rotar el archivo cuando alcanza un tamaño máximo predefinido. En este ejemplo, el archivo de registro se rotará cuando alcance 1024 bytes y se conservarán hasta 3 archivos de registro antiguos. El resto del código es similar al ejemplo anterior, pero en lugar de crear un manejador de registro FileHandler, se crea un manejador de registro RotatingFileHandler.

import logging
import logging.handlers

# Configurar el registro
logger = logging.getLogger('MiApp')
logger.setLevel(logging.DEBUG)
handler = logging.handlers.RotatingFileHandler('miapp.log', maxBytes=1024, backupCount=3)

# Establecer el formato de los mensajes de registro
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# Agregar el manejador de registro al objeto Logger
logger.addHandler(handler)

# Registrar un mensaje de depuración
logger.debug('Este es un mensaje de depuración')

# Registrar un mensaje de información
logger.info('Este es un mensaje de información')

SMTPHandler

Este ejemplo utiliza el manejador de registro SMTPHandler para enviar los mensajes de registro por correo electrónico. En este caso, los mensajes se envían a la misma dirección de correo electrónico que se utiliza como remitente. El resto del código es similar al ejemplo anterior, pero en lugar de crear un manejador de registro FileHandler, se crea un manejador de registro SMTPHandler.

import logging
import logging.handlers

# Configurar el registro
logger = logging.getLogger('MiApp')
logger.setLevel(logging.DEBUG)
handler = logging.handlers.SMTPHandler(mailhost='smtp.gmail.com', fromaddr='miemail@gmail.com', toaddrs='miemail@gmail.com', subject='Mensaje de registro')

# Establecer el formato de los mensajes de registro
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)

# Agregar el manejador de registro al objeto Logger
logger.addHandler(handler)

# Registrar un mensaje de depuración
logger.debug('Este es un mensaje de depuración')

# Registrar un mensaje de información
logger.info('Este es un mensaje de información')

NullHandler

Este manejador simplemente descarta todos los mensajes de registro que recibe. Es útil cuando se desea deshabilitar temporalmente los mensajes de registro sin tener que modificar el código de registro existente.

En este caso, simplemente agregamos el manejador NullHandler al objeto Logger, lo que significa que cualquier mensaje que se registre con el objeto Logger será descartado sin generar ningún tipo de salida o registro.

import logging

# Configurar el registro
logger = logging.getLogger('MiApp')
logger.setLevel(logging.DEBUG)
handler = logging.NullHandler()

# Agregar el manejador de registro al objeto Logger
logger.addHandler(handler)

# Registrar un mensaje de depuración
logger.debug('Este es un mensaje de depuración')

# Registrar un mensaje de información
logger.info('Este es un mensaje de información')

Combinando varios manejadores en Python

Es posible combinar varios manejadores en Python. Esto permite que los mensajes de registro se envíen a múltiples destinos al mismo tiempo. Para hacerlo, se puede crear un manejador de registro que tenga otros manejadores como argumentos. Este manejador de registro personalizado puede procesar los registros y enviarlos a varios destinos al mismo tiempo. Por ejemplo,

import logging

# Configurar el registro
logger = logging.getLogger('MiApp')
logger.setLevel(logging.DEBUG)

# Crear dos manejadores de registro
file_handler = logging.FileHandler('registro.log')
stream_handler = logging.StreamHandler()

# Establecer el nivel de registro para cada manejador
file_handler.setLevel(logging.ERROR)
stream_handler.setLevel(logging.INFO)

# Crear un manejador personalizado que combina ambos manejadores
custom_handler = logging.handlers.MemoryHandler(capacity=1000, target=file_handler)
custom_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
custom_handler.setLevel(logging.DEBUG)
custom_handler.addFilter(lambda record: record.levelno >= logging.WARNING or record.name.startswith('MiApp'))

# Agregar los manejadores al objeto Logger
logger.addHandler(custom_handler)
logger.addHandler(stream_handler)

# Registrar un mensaje de depuración
logger.debug('Este es un mensaje de depuración')

# Registrar un mensaje de información
logger.info('Este es un mensaje de información')

# Registrar un mensaje de advertencia
logger.warning('Este es un mensaje de advertencia')

# Registrar un mensaje de error
logger.error('Este es un mensaje de error')

En este ejemplo, se crean dos manejadores de registro: uno para enviar los mensajes de registro a un archivo y otro para enviarlos a la consola. Luego, se crea un manejador personalizado que combina ambos manejadores. Este manejador personalizado utiliza la clase MemoryHandler para almacenar los registros en memoria hasta que se alcanza un límite máximo (en este caso, 1000 registros) y luego los envía al manejador de registro de archivo. También utiliza un filtro para asegurarse de que solo los registros de advertencia o de nivel superior se envíen al manejador de registro de archivo.

Finalmente, se agrega el manejador personalizado y el manejador de consola al objeto Logger. Cada vez que se registra un mensaje de registro, se envía tanto al manejador personalizado como al manejador de consola, lo que permite ver los mensajes en ambos destinos.

Manejadores personalizados

Es posible definir manejadores de registro personalizados en Python. Esto permite crear un manejador que se adapte a las necesidades específicas de la aplicación. Para hacerlo, se puede definir una nueva clase que herede de la clase base logging.Handler y sobrescriba sus métodos. Un ejemplo sencillito sería el siguiente,

import logging

# Definir una nueva clase de manejador de registro
class MyHandler(logging.Handler):
    def emit(self, record):
        msg = self.format(record)
        # Aquí se puede hacer lo que se necesite con el mensaje
        print(f'Log: {msg}')

# Configurar el registro
logger = logging.getLogger('MiApp')
logger.setLevel(logging.DEBUG)

# Crear el manejador personalizado y agregarlo al objeto Logger
my_handler = MyHandler()
my_handler.setLevel(logging.DEBUG)
my_handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s'))
logger.addHandler(my_handler)

# Registrar un mensaje de depuración
logger.debug('Este es un mensaje de depuración')

# Registrar un mensaje de información
logger.info('Este es un mensaje de información')

En este ejemplo, definimos una nueva clase de manejador de registro llamada MyHandler que hereda de la clase base logging.Handler. Luego, sobrescribimos el método emit() para que imprima el mensaje de registro en la consola en lugar de enviarlo a un archivo o correo electrónico.

Después, creamos una instancia del nuevo manejador personalizado MyHandler y lo agregamos al objeto Logger. Finalmente, registramos dos mensajes de registro para demostrar que el nuevo manejador personalizado está funcionando.

Ten en cuenta que este es solo un ejemplo muy básico y que es posible personalizar el manejador de registro aún más agregando más métodos o utilizando bibliotecas externas para enviar mensajes a un servidor de registro centralizado, base de datos o cualquier otro destino personalizado.

Enviando nuestros logs a un ElasticSearch

Es posible enviar registros a ElasticSearch sin utilizar el módulo elasticsearch. Una forma común de hacer esto es a través del protocolo HTTP, utilizando la API RESTful de ElasticSearch. Así, por ejemplo utilizando la librería requests de Python para enviar los registros a través de HTTP,

import logging
import requests
import json

# Definir una nueva clase de manejador de registro
class ElasticSearchHandler(logging.Handler):
    def __init__(self, index_name, url='http://localhost:9200'):
        super().__init__()
        self.index_name = index_name
        self.url = url

    def emit(self, record):
        log_entry = {
            'timestamp': self.format(record.created),
            'level': record.levelname,
            'message': record.getMessage(),
            'logger_name': record.name,
            'func_name': record.funcName,
            'line_no': record.lineno,
            'thread_name': record.threadName
        }
        headers = {'Content-type': 'application/json'}
        try:
            res = requests.post(f'{self.url}/{self.index_name}/_doc', json=log_entry, headers=headers)
            res.raise_for_status()
        except requests.exceptions.RequestException as e:
            self.handleError(record)

# Configurar el registro
logger = logging.getLogger('MiApp')
logger.setLevel(logging.DEBUG)

# Crear el manejador personalizado y agregarlo al objeto Logger
es_handler = ElasticSearchHandler(index_name='my_index')
es_handler.setLevel(logging.DEBUG)
logger.addHandler(es_handler)

# Registrar un mensaje de depuración
logger.debug('Este es un mensaje de depuración')

# Registrar un mensaje de información
logger.info('Este es un mensaje de información')

En este ejemplo, definimos una nueva clase de manejador de registro llamada ElasticSearchHandler que también hereda de la clase base logging.Handler. En el constructor de la clase, pasamos el nombre del índice de ElasticSearch y la URL del cluster (por defecto http://localhost:9200).

Luego, sobrescribimos el método emit() para crear un objeto de registro y enviarlo a través de HTTP utilizando el método requests.post(). Finalmente, creamos una instancia del nuevo manejador personalizado ElasticSearchHandler y lo agregamos al objeto Logger.

Es importante tener en cuenta que este ejemplo no maneja errores ni proporciona opciones de configuración adicionales. En un entorno de producción, es posible que desee agregar más opciones de configuración, como la capacidad de agregar información adicional al objeto de registro, y también tener en cuenta la autenticación y la seguridad.

Enviando nuestros logs a Telegram

De la misma manera, también podemos hacer que se envíen los logs a Telegram, para lo que al igual que en el caso anterior, utilizaremos la librería requests de Python. Por ejemplo,

import logging
import requests

class TelegramHandler(logging.Handler):
    def __init__(self, bot_token, chat_id):
        super().__init__()
        self.bot_token = bot_token
        self.chat_id = chat_id

    def emit(self, record):
        message = self.format(record)
        payload = {
            'chat_id': self.chat_id,
            'text': message,
            'parse_mode': 'HTML'
        }
        headers = {'Content-type': 'application/json'}
        try:
            response = requests.post(f'https://api.telegram.org/bot{self.bot_token}/sendMessage', 
                                      json=payload, headers=headers)
            response.raise_for_status()
        except requests.exceptions.RequestException as e:
            self.handleError(record)

# Configuración del registro
logger = logging.getLogger('MiApp')
logger.setLevel(logging.DEBUG)

# Configuración del manejador de Telegram
bot_token = '<inserte el token de su bot de Telegram aquí>'
chat_id = '<inserte el chat ID de su chat de Telegram aquí>'
telegram_handler = TelegramHandler(bot_token=bot_token, chat_id=chat_id)
telegram_handler.setLevel(logging.ERROR)
logger.addHandler(telegram_handler)

# Ejemplo de registro
logger.info('Este mensaje no se enviará a Telegram')
logger.warning('Este mensaje no se enviará a Telegram')
logger.error('Este mensaje se enviará a Telegram')

En este ejemplo, creamos una clase TelegramHandler que hereda de la clase base logging.Handler. El constructor de esta clase toma el token del bot de Telegram y el ID del chat al que queremos enviar los registros. El método emit() es el método que envía los registros a Telegram.

Para enviar el mensaje, creamos un diccionario payload que contiene el mensaje a enviar, el chat ID y el formato HTML. Luego, enviamos el mensaje a través del método requests.post() con la URL de la API de Telegram y el token de nuestro bot.

Finalmente, configuramos el manejador personalizado y lo agregamos al objeto Logger, y probamos el registro.

Es importante tener en cuenta que en la implementación de este manejador personalizado, solo se está manejando la excepción requests.exceptions.RequestException, por lo que se pueden agregar más excepciones personalizadas y manejarlas en consecuencia. Además, el ejemplo está configurado para enviar solo registros con nivel ERROR o superior a Telegram, pero se puede cambiar según sea necesario.

Enviando algunas excepciones

Es posible enviar solo un tipo específico de excepción a un manejador en Python. Podemos lograr esto usando el atributo filters en un manejador. Los filtros son funciones que se utilizan para determinar si un registro debe o no ser manejado por un manejador determinado. Por ejemplo, si solo quisiéramos enviar las excepciones ValueError a un manejador podríamos hacerlo de la siguiente forma,

import logging

class CustomHandler(logging.Handler):
    def emit(self, record):
        print(f'CustomHandler: {record.levelname}: {record.msg}')

logger = logging.getLogger('MiApp')
logger.setLevel(logging.DEBUG)

custom_handler = CustomHandler()
custom_handler.setLevel(logging.ERROR)
custom_handler.addFilter(lambda record: isinstance(record.exc_info, tuple) and record.exc_info[0] == ValueError)

logger.addHandler(custom_handler)

try:
    raise ValueError('¡Este es un error de valor!')
except ValueError:
    logger.exception('Ocurrió un error de valor')

logger.info('Este mensaje no se enviará al manejador')

En este ejemplo, creamos una clase CustomHandler que hereda de la clase base logging.Handler y sobrescribe el método emit(). El manejador simplemente imprime el mensaje y el nivel de registro.

Luego, creamos el objeto Logger y configuramos el manejador personalizado con el nivel de registro de ERROR. También agregamos un filtro al manejador utilizando la función lambda. Este filtro solo permitirá que los registros que contengan excepciones ValueError sean manejados por el manejador personalizado.

Finalmente, generamos una excepción ValueError y la registramos con el método logger.exception(). El registro solo se enviará al manejador personalizado porque tiene un filtro que solo permite excepciones ValueError. También agregamos un registro de nivel INFO, que no se enviará al manejador porque no se ajusta al filtro.

El vídeo

Lo mismo que te he contado pero en formato vídeo


Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *