Cuando trabajamos en algún proyecto de software es importante siempre poder probar y testear diferentes situaciones que pueden ocurrir en diferentes entornos, uno de ellos es a nivel de funcionalidad de código, donde entre más situaciones o casos realistas logre crear para probar en el código podemos lograr darle manejo y captura a los errores que se puedan generar en producción. Cuando se está desarrollando y probando se tienen varias herramientas para hacer debug en donde ya hice un post donde menciono algunas, pero cuando sucede un error en producción esto se sale un poco de mi alcance, para ello debemos utilizar logs los cuales son registros que guardan información importante de diferentes tipos de eventos entre ellos los que fallan, y esta informacion es super importante por que contienen la información que permite a futuro recrear situaciones y ambientes claves para encontrar el porqué del fallo, esto se puede apreciar en la Figura 1 en búsqueda del bug

Figura 1: Proceso de seguimiento para encontrar el bug

Para conseguir registrar estos datos de los logs debo primero capturar esos errores, sin embargo existe un antipatrón que se le podría conocer como el Silence Exception, este antipatrón se puede apreciar a continuación

try:
    do_something()
except ValueError:
    pass

en donde se aprecia cómo si bien el error se está capturando no se está dejando registro del mismo, para mi tambien podria considerarse un anti patrón imprimir el error ya que esto da una salida por consola pero se pierde en la ejecución del proyecto, por lo tanto en este artículo me enfocaré en explicar algunos módulos que funcionan para la realización de captura de logs en python con algunas prácticas interesantes

Módulo Logging

El módulo logging es un módulo nativo de python el cual nos permite generar eventos de diferentes tipos, tanto de error, alerta, debug etc este módulo facilita la integración de diferentes componentes que pueden especializarse para mejorar su funcionamiento y haci los resultados obtenidos, a continuación veremos un ejemplo más o menos común de logging

import os
import logging
from typing import Dict, List
from dotenv import load_dotenv

load_dotenv()

logging.basicConfig(
    level=logging.INFO,
    filename=os.getenv("LOG_LOCATION"),
    format="%(asctime)s - %(levelname)s -  %(message)s",
)


def show_info(users: List[Dict]):
    try:
        for user in users:
            print(f"name: {user['name']} - age: {user['age']}")
            print("")
    except KeyError as error:
        logging.error(str(error), exc_info=True)


users = [{"name": "Elon", "age": 40}, {"name": "Steve", "agee": 50}]

show_info(users)

el módulo logging recibe una configuración, en este caso utilizando la función basicConfig() se le dice el nivel que se tendrá, el formato de la información y donde se guardara, en este caso será en un archivo .log se ejecuta el código y en el archivo .log se puede apreciar lo siguiente

Figura 2: Informacion del archivo .log

en este caso guarda el trace del error por que cuando se llama a logging se le pasó exc_info=True esto para que me indique en que linea del codigo ocurrió el error, este modulo será la base para cualquier otro formato y complemento que se desee agregar, ya que esta herramienta contiene un gran cantidad de elementos para su configuración, sin embargo una desventaja de usar solamente archivos escritos en disco es que estos archivos tienden a volverse muy pesados y los contenedores o aplicaciones vienen y van como para mantener estos archivos, una opción es utilizar entornos enfocados directamente a la recolección de información procesado y visualización en donde la información de logs si se escribe en disco pero es enviada continuamente a centros de datos como se ve en la Figura 3

Figura 3: Procesamiento de informacion de logs sacado de https://logz.io/blog/filebeat-vs-logstash/

Módulo Logging con Handler

El módulo de logging está compuesto por varios componentes como se puede apreciar en la Figura 4, uno de ellos es el componente handler que me permite indicarle al módulo logging donde quiero que ponga mi información, para este ejemplo utilizaremos el servicio de AWS CloudWatchLogs esta herramienta la podemos utilizar con boto3 pero en función de mostrar cómo trabaja el handler utilizare la librería watchtower, para instalarla usare poetry

poetry add watchtower
Figura 4: Componentes básicos de la clase logger

se debe tener instalado o configurado el aws cli con sus credenciales, con esto listo podemos pasar al ejemplo como se aprecia en el siguiente código

import os
import logging
from typing import Dict, List
from dotenv import load_dotenv
import watchtower

load_dotenv()

logging.basicConfig(
    level=logging.INFO,
    filename=os.getenv("LOG_LOCATION"),
    format="%(asctime)s - %(levelname)s -  %(message)s",
)
logger = logging.getLogger(__name__)
logger.addHandler(watchtower.CloudWatchLogHandler())


def show_info(users: List[Dict]):
    try:
        for user in users:
            print(f"name: {user['name']} - age: {user['age']}")
            print("")
    except KeyError as error:
        logger.error(str(error), exc_info=True)


users = [{"name": "Elon", "age": 40}, {"name": "Steve", "agee": 50}]

show_info(users)

hemos puesto casi las mismas configuraciones que ya se habían hecho para logging agregando un Handler a nuestro módulo logging tal como se explicó su arquitectura en la Figura 4, utilizando el método addHandler() este método lo que le dice a logging es donde y como debe guardar la información que se registre, en este caso la guarda en CloudWatchLogs , por lo tanto el código sigue siendo el mismo no tuve que cambiar nada más que el handler, ejecuto el código y podemos ver en AWS como se ve en la siguiente Figura 5

Figura 5: Información procesada de los filtros en AWS CloudWatch

CloudWatch tiene además un query para hacer consultas sobre los logs y realizar gráficos como se puede ver en la siguiente consola para realizar consultas

Figura 6: Consola para ejecutar queries sobre la información de los logs

y al ejecutar podemos ver en gráfico lo que ocurrió en la siguiente Figura 7

Figura 7: Gráfico generado a partir de la consola de consultas de AWS

Como pudimos observar este tipo de herramientas que tienen su handler hacen que a la hora de capturar errores en logs los pueda cambiar y utilizar sin que el código existente se modifique, pues solo se agregan estos a la clase y apartir de alli ya todo el trabajo es transparente, como una posible desventaja es que la informacion se envia por medio de peticiones HTTP y a esto se le debe prestar mucha atención

Módulos de terceros

Actualmente muchas plataformas siguen la tendencia de everything as a service esto claramente nos facilita la vida a los programadores por que se traduce en una gran tendencia de servicios sencillos de implementar y que nos permiten hacer mejor nuestro trabajo, por lo tanto existen algunas herramientas que se enfocan en cobrar por uso de sus servicio de monitoreo de código entre ellas sentry

Sentry

Esta plataforma se define como una "Plataforma para monitorear, que ayuda a todos los desarrolladores a solucionar problemas en el código y optimizarlo" ,  esta plataforma nos brinda un recurso bastante fácil de usar que funciona con diferentes lenguajes de programación en donde brinda una recolección y monitoreo de código como servicio su implementación es bastante sencilla como se muestra a continuación

import os
from typing import Dict, List
import sentry_sdk
from sentry_sdk import capture_exception, capture_message
from dotenv import load_dotenv

load_dotenv()
sentry_sdk.init(
    os.getenv("URL_SDK"),
    traces_sample_rate=1.0,
)


def show_info(users: List[Dict]):
    try:
        for user in users:
            print(f"name: {user['name']} - age: {user['age']}")
    except KeyError as error:
        capture_exception(error)


users = [{"name": "Elon", "age": 40}, {"name": "Steve", "agee": 50}]

show_info(users)

después de ejecutar el código anterior se puede ver en consola

Figura 8: Salida por consola después de ejecutar el ejemplo con sentry

como se apreció solamente se imprimió un resultado debido a que en el segundo resultado se genera la excepción, ahora en la plataforma de sentry podemos ver el error

Figura 9: Dashboard de la plataforma sentry para visualizar logs

y se puede entrar a ver el error, esto es lo más genial y lo que más me ha gustado, es poder ver la información que trae sobre el error

Figura 10: Visualización de la información de los logs utilizando sentry

como se puede observar esta herramienta nos muestra en qué archivo ocurrió el error, en que linea, con qué  lista de datos y el dato exacto con el cual ocurrió, esta herramienta esta muy genial y esto son solo herramientas del plan free, con los planes de pago tiene dashboards y otros elementos que están muy interesantes. Este tipo de herramientas tiene una gran ventaja la cual  permiten abstraer la complejidad al desarrollador utilizando una API para guardar continuamente la información y se evita el problema de tener archivos de logs pesados, sin embargo hay que prestar atención pues esta herramienta puede tener delays ya que se debe llamar a un tercero para guardar la data, por supuesto estos servicios tienen forma de optimizarse y además tienen soporte director para ASGI lo que quiere decir que se puede integrar muy bien con frameworks como FastAPI

Conclusiones

Capturar los errores es un paso fundamental cuando se desarrolla software, ya que en producción es importante saber el momento y la informacion con la que ocurrio un error, para poder con esta informacion valerme para recrear más adelante este mismo "ambiente" , sin embargo no solamente debo capturar el error si no que debo guardar esta información utilizando algún servicio o alguna herramienta que me facilite el proceso como desarrollador, de las herramientas que se mostraron todas son válidas y nos pueden ayudar a cumplir este objetivo de manera eficiente sin embargo en el mercado se pueden encontrar muchas mas ya que cada nube tiene su propia implementacion