
Compartir:
Renato is a backend software developer and a father of two amazing kids who won’t let him sleep so that he can enjoy spending nights connecting APIs around.
Creación de una herramienta de alerta de errores en Python
Por mucho que nos preocupemos por la calidad y las pruebas, está casi garantizado que el software salga mal en algún momento. Por ello, es esencial disponer de registros de monitorización para controlar el estado de las aplicaciones.
Ciertamente, existen múltiples servicios y proyectos de código abierto que se encargan de monitorizar los registros de las aplicaciones. En mi experiencia, sin embargo, suelen ser o bien caros, o bien lentos de integrar, o bien hinchados con características que apenas voy a utilizar. Cuando despliego proyectos pequeños que no requieren una monitorización sofisticada, a veces me gustaría tener una solución nativa de Python para recibir alertas sencillas cuando algo va mal en mi código.
El propósito de este tutorial es precisamente satisfacer esta necesidad. Construiremos una herramienta de alertas de error Python simple y flexible que puede ser conectada a cualquier proyecto. Un objeto HTTP handler de registro enviará asincrónicamente alertas a través de la SMS API de Vonage a nuestros teléfonos cuando lleguen nuevos errores o advertencias, por ejemplo.
Requisitos
Utilizaremos Python 3.9.1 (la última versión estable) en el tutorial, pero el código también debería funcionar en Python 3.6+. Python está disponible en Linux, macOS y Windows. Para descargarlo e instalarlo, sigue las instrucciones del sitio web oficial.
También necesitarás una cuenta de Vonage para recibir alertas de error a través de SMS. Crea una Account si aún no estás registrado. Vonage ofrece a los nuevos suscriptores € 2.00 en créditos para probar las API gratis.
Vonage API Account
To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.
La clave y el secreto de la API de Vonage también serán necesarios; asegúrate de obtenerlos en los Configuración del panel:

PyPI http-logging se utilizará para el almacenamiento en caché de registros y la comunicación asíncrona con la API de Vonage. Evita que nuestra aplicación principal de Python se vea interrumpida por el mecanismo de alerta.
Preparación del entorno local
Virtualenv y dependencias
Cree un directorio para el proyecto:
Creación de un entorno virtual suele ser una buena práctica, así que hagámoslo primero:
En un ordenador con Windows, sustituya el comando source de la última línea por
Asegúrese de que el entorno funciona como se espera:
Ahora vamos a crear nuestro archivo de dependencias de Python:
Ábralo con su editor de texto preferido y añada las siguientes líneas:
http-logging
vonageCierre el archivo e instale las dependencias con el pip install comando:
Variables de entorno
Nuestra lógica de registro personalizada requerirá cierta información que será suministrada a través de variables de entorno.
La clave API de Vonage es necesaria para la autenticación con el servicio de SMS. También será necesario un número de teléfono para enviar mensajes SMS.
El comando export debería funcionar en Linux y macOS. En Windows, utilice set en su lugar. Si utiliza PowerShell este comando debería funcionar:
$Env: VONAGE_API_KEY = "abc123"
$Env: VONAGE_API_SECRET = "xyz123"
$Env: export ALERT_PHONE_NUMBER = "+1234567890" Gestor de registro HTTP
Como se ha mencionado anteriormente, nos basaremos en el http-logging para conectar nuestros registros a las API de Vonage.
Un gestor logging HTTP handler de la librería estándar de Python. Sin embargo, no vamos a usarlo porque genera peticiones HTTP bloqueantes, lo que puede afectar negativamente a la ejecución de nuestra aplicación Python principal.
La dirección http-logging se ejecuta silenciosamente en un subproceso en segundo plano y también es capaz de almacenar en caché los registros en una base de datos SQLite local para reducir el número de peticiones de red. Por estas razones, será mucho menos intrusiva que un gestor HTTP nativo.
La librería está basada en la librería Python Logstash Asyncpero generalizada para trabajar con cualquier backend aparte de Logstash (en nuestro tutorial, usaremos Vonage). Lee más sobre ella en la wiki de documentación del proyecto.
Transporte HTTP de Vonage
Lo primero que necesitamos es crear una clase de transporte HTTP personalizada. Esta es la que lleva las instrucciones sobre cómo enviar registros a la API de Vonage.
Antes de sumergirnos en ello, vamos a crear un nuevo archivo Python para contener nuestro código de registro personalizado:
Ahora abre este archivo: ¡es hora de divertirse con Python!
Nuestra propia clase HTTP Transport heredará de la clase [http_logging.AsyncHttpTransport](https://github.com/hacktlib/py-async-http-logging/wiki/3.-HTTP-Transport-Class) de la anterior. En primer lugar, importar las bibliotecas necesarias en la parte superior del archivo y luego declarar una nueva clase como se demuestra a continuación:
import logging
import os
from vonage import Sms
from http_logging import HttpHost, SupportClass
from http_logging.handler import AsyncHttpHandler
from http_logging.transport import AsyncHttpTransport
class VonageHttpTransport(http_logging.transport.AsyncHttpTransport):
passAhora mismo, esta clase se comportará exactamente igual que la original. Vamos a añadirle alguna funcionalidad personalizada. El AsyncHttpTransport implementa un método send que se encarga de enviar los registros a un host remoto. Inicialmente, utiliza el método peticiones para ello. En nuestro caso, tenemos el Vonage SDKque nos facilita mucho la vida y elimina el aburrimiento del protocolo HTTP.
Vale, basta de hablar. Comencemos a codificar con el SDK de Vonage declarando un nuevo método send método:
class VonageHttpTransport(AsyncHttpTransport):
def send(self, events: dict, **kwargs) -> None:
batches = self._HttpTransport__batches(events)
sms_logs = ', '.join([
f"{log['level']['name']}: {log['message']}"
for batch in batches
for log in batch
])
sms_message = f'[Python Logger {self.logger_name}] {sms_logs}'
sms_client = Sms(
key=self.vonage_api_key,
secret=self.vonage_api_secret,
)
response = sms_client.send_message({
'from': f'Python Logger {self.logger_name}',
'to': self.alert_phone_number,
'text': sms_message,
})
if not response['messages'][0]['status'] == 0:
raise ConnectionError(response["messages"][0].get("error-text"))
El método send toma un argumento events argumento; una lista que se convierte en un lote de registros mediante el método HttpTransport.__batches método. A continuación, los lotes se procesan para extraer los puntos de datos básicos en una cadena de registro.
Cada cadena de registro contiene únicamente el nombre del nivel de registro (por ejemplo, "Advertencia" o "Error") y un mensaje de registro. SMS significa Servicio de Mensajes Cortos, por lo que queremos que nuestro mensaje de alerta sea breve. Nuestro objetivo principal es alertar, no apoyar, la depuración completa a través de SMS. Se envía información mínima para proporcionar contexto y ayudar al desarrollador a iniciar el proceso de depuración.
A continuación, los registros se concatenan mediante el método string.join y se les antepone el nombre del registrador para proporcionar información sobre el contexto de la aplicación (esto puede ser útil en caso de que varios proyectos utilicen esta herramienta de alerta).
Por último, instanciamos un vonage.Sms cliente del SDK de Vonage y lo utilizamos para enviar el mensaje SMS a nuestro teléfono. Se comprueba el estado de la respuesta y, si no es "OK", lanzamos un mensaje ConnectionError. Este error planteado asegura que el mecanismo de alerta de registro se reintente más tarde y no interrumpirá nuestra aplicación Python principal, ya que la clase VonageHttpTransport se ejecutará en un subproceso en segundo plano.
Observe que estamos utilizando algunos atributos de clase en el nuevo método send nuevo: logger_name, vonage_api_key, vonage_api_secret, alert_phone_number. Reemplacemos el método __init__ para asegurarnos de que se establecen correctamente en la instanciación de la clase:
class VonageHttpTransport(AsyncHttpTransport):
def __init__(
self,
logger_name: str,
vonage_api_key: str,
vonage_api_secret: str,
alert_phone_number: str,
*args,
**kwargs,
) -> None:
self.logger_name = logger_name
self.vonage_api_key = vonage_api_key
self.vonage_api_secret = vonage_api_secret
self.alert_phone_number = alert_phone_number
super().__init__(*args, **kwargs)
Nuestra nueva clase Transporte HTTP ya está lista. Pero antes de pasar a la acción de registro real, primero tenemos que crear la lógica que instanciará un Logger usando la nueva clase VonageHttpTransport nueva.
Vonage Log Handler
La clase VonageHttpTransport tiene buena pinta, pero no puede ir a la batalla por sí sola. En realidad no somos capaces de usarla para registrar nada en nuestras aplicaciones, así que demos un paso más y hagámosla lista para el combate.
La pieza que falta en nuestro rompecabezas es una clase HTTP Handler real. Debería ser una clase http_logging.AsyncHttpHandlerpero, seguramente, instanciada con la clase VonageHttpTransport.
Vamos a crear una getLogger dentro de logging_vonage.pypara imitar el comportamiento nativo de Python logging.getLogger de Python:
def getLogger(name: str) -> logging.Logger:
pass
Como la función getLogger la nuestra toma una cadena de nombre como argumento y devuelve una instancia de la clase logging.Logger clase. A continuación, vamos a construir la funcionalidad de esta función paso a paso.
Empezaremos por crear una instancia HttpHost. Esto no es realmente necesario para el VonageHttpTransportya que estamos delegando las solicitudes HTTP al SDK de Vonage, pero es una parte necesaria de la firma de la API de la biblioteca de registro http:
def getLogger(name: str) -> logging.Logger:
host = HttpHost(name='vonage.com')
A continuación necesitamos un SupportClass que contenga nuestro objeto HTTP Transport:
support_class = SupportClass(
http_host=host,
_transport=VonageHttpTransport(
http_host=host,
logger_name=name,
vonage_api_key=os.environ.get('VONAGE_API_KEY'),
vonage_api_secret=os.environ.get('VONAGE_API_SECRET'),
alert_phone_number=os.environ.get('ALERT_PHONE_NUMBER'),
),
)Este objeto SupportClass se utiliza para instanciar nuestro AsyncHttpHandler:
vonage_handler = AsyncHttpHandler(
http_host=host,
support_class=support_class,
)Por último, instanciamos un objeto logging.Logger objeto, añadimos el vonage_handler como su manejador y lo devolvemos:
logger = logging.getLogger(name)
logger.addHandler(vonage_handler)
return loggerAl final, nuestra getLogger debería tener el siguiente aspecto:
def getLogger(name: str) -> logging.Logger:
host = HttpHost(name='vonage.com')
support_class = SupportClass(
http_host=host,
_transport=VonageHttpTransport(
http_host=host,
logger_name=name,
vonage_api_key=os.environ.get('VONAGE_API_KEY'),
vonage_api_secret=os.environ.get('VONAGE_API_SECRET'),
alert_phone_number=os.environ.get('ALERT_PHONE_NUMBER'),
),
)
vonage_handler = AsyncHttpHandler(
http_host=host,
support_class=support_class,
)
logger = logging.getLogger(name)
logger.addHandler(vonage_handler)
return logger
Observa que la clave, el secreto y el número de teléfono de la API se obtienen de las variables de entorno que establecimos al principio de este tutorial. Esto proporciona flexibilidad en caso de que queramos utilizar este código en varios proyectos, y también evita hardcoding secretos de la API, que por lo general no es una buena idea. ;)
Múltiples manipuladores
La maquinaria de registro de Python es muy potente, y el objeto logging.Logger es lo suficientemente flexible como para extenderlo con múltiples manejadores.
Como ya se ha explicado, la clase VonageHttpTransport enviará información mínima sobre los registros debido a las limitaciones inherentes a la longitud del texto del sistema SMS. No obstante, en caso de que se produzca un error que requiera una depuración más profunda, sin duda querremos obtener el seguimiento completo de la pila, información sobre qué línea de código falló, marcas de tiempo exactas, etc.
Podemos satisfacer esa demanda de registro detallado utilizando la función Logger.addHandler y añadiendo uno o más gestores adicionales al objeto Vonage Logger de Vonage.
Por ejemplo, para enviar logs no sólo a nuestro teléfono sino también a la consola, podemos utilizar el comando logging.StreamHandlercomo se muestra a continuación:
import logging
import logging_vonage
logger = logging_vonage.getLogger('')
logger.addHandler(logging.StreamHandler())Todo lo que se registre con el objeto logger se imprimirá en la consola y se enviará a nuestro teléfono a través de la SMS API de Vonage.
A logging.FileHandler puede usarse para almacenar registros en el sistema de archivos local si eso tiene sentido en una implementación. También se podría utilizar el mismo http_logging.AsyncHttpHandler de nuevo, pero en este caso, enviando los registros a un host backend diferente aparte de la API de Vonage. Pruebas con una aplicación de ejemplo Muy bien, es hora de ver algo de acción en el mundo real con campanas y silbatos. Es broma, estamos a punto de hacer sonar nuestros teléfonos con la SMS API de Vonage :D
Cree un nuevo archivo en el directorio del proyecto llamado sample_app:
Ábrelo y añade el siguiente contenido:
import logging
import logging_vonage
logger = logging_vonage.getLogger('sampleapp')
logger.addHandler(logging.StreamHandler())
logger.debug('Debugging...')
logger.warning('You\'ve been warned!')
logger.error('This is a test error')
try:
1/0
except ArithmeticError as exc:
logger.exception(exc)Observa que estamos instanciando un objeto logger del módulo logging_vonage que construimos anteriormente. El logging.StreamHandler() también se está utilizando para que las trazas completas se registren en nuestra consola, y no sólo se envíen a nuestro teléfono.
En la consola, ejecute este script con:
La siguiente salida debería imprimirse en la consola:
You've been warned!
This is a test error
division by zero
Traceback (most recent call last):
File "/home/vonage-alerts/sample_app.py", line 14
1/0
ZeroDivisionError: division by zeroCon suerte, si tienes todo configurado correctamente (una Account de Vonage y clave/secreto de API), deberías recibir un mensaje SMS en breve con el siguiente texto:
[Python Logger sampleapp] WARNING: You've been warned!, ERROR: This is a test error, ERROR: division by zero
Observe que el mensaje de depuración 'Debugging...' no se imprime en la consola ni se concatena en el mensaje SMS. Esto se debe a que el nivel de registro predeterminado en la biblioteca de registro de Python es WARNING. El nivel DEBUG se considera inferior a WARNING y, por tanto, se descarta.
Si desea que el DEBUG mensaje, ajuste el nivel en consecuencia, como se muestra a continuación:
Ejecute el sample_app.py y debería ver el mensaje de depuración impreso en la consola y también concatenado en el mensaje SMS.
Obsérvese que, a pesar de logger dependemos de un Handler (http_logging.AsyncHttpHandler) y una clase Transport (logging_vonage.VonageHttpTransport), se comporta como cualquier otro objeto Python Logger de Python. Esto hace que sea totalmente compatible con cualquier proyecto Python que tengas actualmente, en caso de que quieras integrar el mecanismo de alerta SMS que acabamos de desarrollar en tu pila y en cualquier proyecto futuro.
Conclusión
Ya está. Ahora tenemos una herramienta de alerta Python sencilla y no intrusiva para estar al tanto de lo que ocurre con las aplicaciones que hemos desplegado. Extiende las características nativas básicas de Python logging para utilizar la misma API a la que estamos acostumbrados, y se ejecuta en cualquier lugar donde se ejecuten nuestras aplicaciones Python. Desde el punto de vista económico, no tiene costes fijos y su mantenimiento es relativamente barato (sólo gastos de envío de SMS).
La biblioteca http-logging mantiene una caché local de registros, de modo que en caso de que la API de Vonage o el proveedor de telefonía celular experimenten algún tiempo de inactividad o inestabilidad de la red, nuestro registrador puede volver a intentar enviar las alertas por SMS algún tiempo después.
