https://a.storyblok.com/f/270183/42525/f13e61dcd0/python_error-alert_1200x600.png

Criação de uma ferramenta de alerta de erros em Python

Publicado em March 30, 2021

Tempo de leitura: 20 minutos

Por mais que nos preocupemos com a qualidade e os testes, é quase certo que o software apresentará algum problema em algum momento. Por isso, é essencial monitorar os logs para acompanhar o estado de funcionamento do aplicativo.

Certamente existem diversos serviços e projetos de código aberto dedicados ao monitoramento de logs de aplicativos. Na minha experiência, porém, eles costumam ser caros, demorados de integrar ou repletos de recursos que eu dificilmente vou usar. Quando estou implantando pequenos projetos que não exigem um monitoramento sofisticado, às vezes gostaria de ter uma solução nativa em Python para receber alertas simples quando algo der errado no meu código.

O objetivo deste tutorial é justamente atender a essa necessidade. Vamos criar uma ferramenta simples e flexível em Python para alertas de erros, que possa ser integrada a qualquer projeto. Um objeto manipulador HTTP de registro de eventos enviará alertas de forma assíncrona por meio da SMS API da Vonage para nossos celulares quando novos erros ou avisos, por exemplo, forem detectados.

Requisitos

Usaremos o Python 3.9.1 (a versão estável mais recente) neste tutorial, mas o código também deve funcionar no Python 3.6 ou superior. O Python está disponível no Linux, macOS e Windows. Para baixar e instalar, siga as instruções no site oficial.

Você também precisará de uma conta na Vonage para receber alertas de erro por SMS. Crie um Account se ainda não estiver cadastrado. A Vonage oferece aos novos assinantes € 2,00 em créditos para testar as APIs gratuitamente.

A chave e o segredo da API da Vonage também serão necessários; certifique-se de obtê-los nas configurações do painel de controle:

Vonage Dashboard

PyPI http-logging será utilizada para o armazenamento em cache de logs e a comunicação assíncrona com a API da Vonage. Isso evita que nosso aplicativo principal em Python seja interrompido pelo mecanismo de alertas.

Preparação do ambiente local

Virtualenv e dependências

Crie um diretório para o projeto:

~$ mkdir vonage-alerts ~$ cd vonage-alerts

Criação de um ambiente virtual costuma ser uma boa prática, então vamos fazer isso primeiro:

~/vonage-alerts$ python3.9 -m venv .env ~/vonage-alerts$ source .env/bin/activate

Em um computador com Windows, substitua o source comando na última linha acima por:

~/vonage-alerts$ .venv\Scripts\activate

Certifique-se de que o ambiente esteja funcionando conforme o esperado:

(.env) ~/vonage-alerts$ python --version Python 3.9.1

Agora, vamos criar nosso arquivo de dependências do Python:

(.env) ~/vonage-alerts$ touch requirements.txt

Abra o arquivo com seu editor de texto preferido e adicione as seguintes linhas:

http-logging
vonage

Feche o arquivo e instale as dependências com o pip install comando:

(.env) ~/vonage-alerts$ pip install -r requirements.txt

Variáveis de ambiente

Nossa lógica de registro personalizada exigirá algumas informações que serão fornecidas por meio de variáveis de ambiente.

A chave de API da Vonage é necessária para a autenticação no serviço de SMS. Também será necessário um número de telefone para enviar mensagens SMS.

(.env) ~/vonage-alerts$ export VONAGE_API_KEY="abc123" (.env) ~/vonage-alerts$ export VONAGE_API_SECRET="xyz123" (.env) ~/vonage-alerts$ export ALERT_PHONE_NUMBER="+1234567890"

O export deve funcionar no Linux e no macOS. No Windows, use set em vez disso. Se você estiver usando o PowerShell , então este comando deve resolver:

$Env: VONAGE_API_KEY = "abc123"
$Env: VONAGE_API_SECRET = "xyz123"
$Env: export ALERT_PHONE_NUMBER = "+1234567890"

Manipulador de registro de log HTTP

Como mencionado anteriormente, vamos utilizar o biblioteca http-logging para conectar nossos logs às APIs da Vonage.

Um manipulador HTTP de registro da biblioteca padrão do Python também daria conta do recado. No entanto, não vamos usá-lo porque ele gera solicitações HTTP bloqueantes, o que pode afetar negativamente a execução do nosso aplicativo principal em Python.

O http-logging funciona silenciosamente em uma thread em segundo plano e também é capaz de armazenar logs em cache em um banco de dados SQLite local para reduzir o número de solicitações de rede. Por esses motivos, ela será muito menos intrusiva do que um manipulador HTTP nativo.

A biblioteca é baseada no Python Logstash Async, mas foi generalizada para funcionar com qualquer backend além do Logstash (em nosso tutorial, usaremos o Vonage). Saiba mais sobre isso na wiki de documentação do projeto.

Transporte HTTP da Vonage

A primeira coisa que precisamos fazer é criar uma classe de transporte HTTP personalizada. É essa classe que contém as instruções sobre como enviar logs para a API da Vonage.

Antes de entrarmos no assunto, vamos criar um novo arquivo em Python para conter nosso código personalizado de registro de logs:

(.env) ~/vonage-alerts$ touch logging_vonage.py

Agora abra este arquivo — é hora de se divertir um pouco com Python!

Nossa própria classe `HTTP Transport` herdará da [http_logging.AsyncHttpTransport](https://github.com/hacktlib/py-async-http-logging/wiki/3.-HTTP-Transport-Class) . Primeiro, importe as bibliotecas necessárias no início do arquivo e, em seguida, declare uma nova classe conforme demonstrado abaixo:

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):
    pass

No momento, essa classe se comportará exatamente como a original. Vamos adicionar algumas funcionalidades personalizadas a ela. A AsyncHttpTransport implementa um send método, que é responsável por enviar registros de log para um host remoto. Inicialmente, ela usa o biblioteca para isso. No nosso caso, temos o SDK da Vonage, o que facilita muito a nossa vida e elimina o trabalho chato com o protocolo HTTP.

Bom, chega de conversa. Vamos começar a programar com o SDK da Vonage , declarando um novo 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"))

O send método recebe um events argumento: uma lista que é convertida em um lote de logs usando o HttpTransport.__batches método. Os lotes são então processados para extrair pontos de dados básicos e transformá-los em uma string de log.

Cada string de log contém apenas o nome do nível de log (por exemplo, “Aviso” ou “Erro”) e uma mensagem de log. SMS significa SMS (Serviço de Mensagens Curtas), portanto, queremos manter nossa mensagem de alerta curta. Nosso objetivo principal é alertar, e não oferecer suporte à depuração completa por meio do SMS. São enviadas informações mínimas para fornecer contexto e ajudar o desenvolvedor a iniciar o processo de depuração.

Os logs são então concatenados usando o string.join método e recebem como prefixo o nome do logger para fornecer informações sobre o contexto do aplicativo (isso deve ser útil caso vários projetos estejam utilizando essa ferramenta de alertas).

Por fim, instanciamos um vonage.Sms cliente a partir do SDK da Vonage e o usamos para enviar a mensagem SMS para o nosso celular. O status da resposta é verificado e, caso não seja “OK”, geramos um ConnectionError. Esse erro lançado garante que o mecanismo de alerta de log seja repetido posteriormente e não interrompa nosso aplicativo Python principal, já que a VonageHttpTransport classe estará sendo executada em uma thread em segundo plano.

Observe que estamos usando alguns atributos de classe no novo send : logger_name, vonage_api_key, vonage_api_secret, alert_phone_number. Vamos sobrescrever o __init__ método para garantir que esses parâmetros sejam definidos corretamente na instanciação da classe:

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)

Nossa nova classe `HTTP Transport` já está pronta. Mas, antes de passarmos à prática do registro de logs, precisamos primeiro criar a lógica que irá instanciar um Logger usando a nova VonageHttpTransport classe.

Gerenciador de Logs da Vonage

A VonageHttpTransport classe parece boa, mas não dá para usá-la sozinha. Na verdade, ainda não conseguimos usá-la para registrar nada em nossas Applications, então vamos dar mais um passo e deixá-la pronta para a ação.

A peça que falta no nosso quebra-cabeça é uma classe HTTP Handler propriamente dita. Ela deve ser uma http_logging.AsyncHttpHandler, mas, certamente, instanciada com o VonageHttpTransport.

Vamos criar uma getLogger função dentro logging_vonage.py, para imitar o logging.getLogger :

def getLogger(name: str) -> logging.Logger:
    pass

Assim como a função nativa do Python getLogger , a nossa recebe uma string de nome como argumento e retorna uma instância da logging.Logger classe. A seguir, vamos construir a funcionalidade dessa função passo a passo.

Começamos instanciando um HttpHost. Isso não é realmente necessário para o VonageHttpTransport, já que estamos delegando as solicitações HTTP ao SDK da Vonage, mas é uma parte obrigatória da assinatura da API da biblioteca http-logging:

def getLogger(name: str) -> logging.Logger:
    host = HttpHost(name='vonage.com')

Em seguida, precisamos de um SupportClass que contenha nosso objeto de transporte HTTP:

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'),
        ),
    )

Esse SupportClass objeto é então usado para instanciar nosso AsyncHttpHandler:

vonage_handler = AsyncHttpHandler(
        http_host=host,
        support_class=support_class,
    )

Por fim, instanciamos um logging.Logger objeto, adicionamos o vonage_handler como seu manipulador e o retornamos:

logger = logging.getLogger(name)
    logger.addHandler(vonage_handler)

    return logger

No final, nossa getLogger função deve ficar assim:

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

Observe que a chave e o segredo da API, bem como o número de telefone, estão sendo obtidos a partir das variáveis de ambiente que definimos no início deste tutorial. Isso oferece flexibilidade caso queiramos usar esse código em vários projetos e também evita a codificação direta de segredos da API, o que geralmente não é uma boa ideia. ;)

Vários manipuladores

O mecanismo de registro de log do Python é muito poderoso, e o logging.Logger objeto é flexível o suficiente para ser ampliado com vários manipuladores.

Conforme explicado acima, a VonageHttpTransport classe enviará informações mínimas sobre os logs devido às limitações inerentes ao comprimento do texto do sistema SMS. No entanto, no caso de um erro que exija uma depuração mais aprofundada, certamente vamos querer obter o rastreamento completo da pilha, informações sobre em qual linha de código ocorreu a falha, carimbos de data e hora exatos etc.

Podemos atender a essa exigência de registro detalhado utilizando o Logger.addHandler e adicionando um ou mais manipuladores adicionais ao objeto Vonage Logger .

Por exemplo, para enviar registros não apenas para o nosso celular, mas também para o console, podemos usar o logging.StreamHandler, conforme demonstrado abaixo:

import logging
import logging_vonage

logger = logging_vonage.getLogger('')
logger.addHandler(logging.StreamHandler())

Tudo o que for registrado com o objeto acima logger será exibido no console e enviado para o nosso celular por meio da SMS API da Vonage.

Um logging.FileHandler pode ser usado para armazenar logs no sistema de arquivos local, se isso fizer sentido em uma implementação. Você também poderia usar o mesmo http_logging.AsyncHttpHandler novamente, mas, nesse caso, enviando logs para um host de backend diferente da API da Vonage. Testando com um aplicativo de exemplo Muito bem, hora de ver um pouco de ação no mundo real com todos os recursos possíveis. Brincadeira, estamos apenas prestes a fazer nossos celulares apitarem com a SMS API da Vonage. :D

Crie um novo arquivo no diretório do projeto chamado sample_app:

(.env) ~/vonage-alerts$ touch sample_app.py

Abra o arquivo e insira o seguinte conteúdo:

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)

Observe que estamos instanciando um logger objeto a partir do logging_vonage módulo que criamos anteriormente. O logging.StreamHandler() também está sendo usado para que os rastreamentos completos sejam registrados em nosso console, e não apenas enviados para o nosso celular.

No console, execute este script com:

(.env) ~/vonage-alerts$ python sample_app.py

A saída a seguir deve ser exibida no console:

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 zero

Se tudo estiver configurado corretamente (uma conta na Vonage e a chave/segredo da API), você deverá receber em breve uma mensagem SMS com o seguinte texto:

[Python Logger sampleapp] WARNING: You've been warned!, ERROR: This is a test error, ERROR: division by zero

Debug message

Observe que a mensagem de depuração 'Debugging...' não foi exibida no console nem concatenada à mensagem SMS. Isso ocorre porque o nível de log padrão na biblioteca de log do Python é WARNING. O DEBUG nível é considerado inferior a WARNING e, portanto, descartado.

Se você quiser que a DEBUG mensagem seja capturada, defina o nível conforme mostrado abaixo:

logger = logging_vonage.getLogger('sampleapp') logger.addHandler(logging.StreamHandler()) logger.setLevel(logging.DEBUG)

Execute o sample_app.py script novamente e você deverá ver a mensagem de depuração exibida no console e também concatenada à mensagem de SMS.

Observe que, apesar de nossa logger dependência de um Handler personalizado (http_logging.AsyncHttpHandler) e de uma classe Transport personalizada (logging_vonage.VonageHttpTransport), ele se comporta exatamente como qualquer outro objeto Python Logger . Isso o torna totalmente compatível como um substituto direto para qualquer projeto em Python que você tenha atualmente, caso queira integrar o mecanismo de alertas por SMS que acabamos de desenvolver em toda a sua pilha de tecnologia e em qualquer projeto futuro.

Conclusão

Pronto! Agora temos uma ferramenta de alerta em Python simples e não intrusiva para acompanhar o que está acontecendo com as Applications que implantamos. Ela amplia os recursos básicos nativos do Python logging para usar a mesma API com a qual estamos acostumados e roda em qualquer lugar onde nossas aplicações em Python sejam executadas. Financeiramente, não tem custos fixos e é relativamente barato de manter (apenas as tarifas das mensagens SMS).

A biblioteca http-logging mantém um cache local de registros; assim, caso a API da Vonage ou a operadora de celular enfrentem alguma interrupção no serviço ou instabilidade na rede, nosso registrador pode tentar novamente enviar os alertas por SMS algum tempo depois.

Compartilhar:

https://a.storyblok.com/f/270183/400x373/ed2dc20b00/renato-byrro.png
Renato Byrro

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.