Go + Observabilidade
Métricas com o Prometheus — Artigo 10
Postman collection, código do artigo, diff com o artigo 9.
Sumário: Desenvolvimento de APIs em Go
Próximo Artigo: Grafana + Prometheus — Visualizando as Métricas de Nossa API
Artigo Anterior: Go + Integração Contínua — Github Actions, Golangci-lint e Codecov
Introdução
A observabilidade é um dos tópicos mais importantes ao se trabalhar com o desenvolvimento de APIs e a integração de vários microsserviços. Ela proporciona a capacidade de compreender e entender o comportamento interno das aplicações com base em informações coletadas, permitindo que os desenvolvedores obtenham insights sobre o desempenho, saúde e estado do sistema em tempo real. Isso inclui a detecção de problemas, identificação de gargalos, análise de tendências, monitoramento de latência, taxa de erros e muito mais.
Um ponto crucial para alcançar a observabilidade é a instrumentação adequada do sistema, que envolve a incorporação de pontos de coleta de dados no código e na infraestrutura. Isso possibilita a captura de informações relevantes e sua disponibilização para análise posterior.
O foco deste artigo será a coleta de métricas técnicas relacionadas às requisições de todos os endpoints de uma API. Será utilizado a abordagem de metrificar apenas os pontos de entrada e saída dos endpoints, evitando a dispersão do código de métricas em todas as camadas da aplicação. Neste momento, não abordaremos a criação de métricas de negócio.
No final desse e do próximo artigo, teremos uma API que enviará métricas para o Prometheus e conseguiremos criar os seguintes dashboards no Grafana.
No dashboard acima, na primeira linha, temos uma primeira série temporal (em azul) que mostra a taxa de requests do endpoint. Na segunda série temporal (em amarelo), temos a latência, e na terceira série (em vermelho), temos a taxa de erros. Na segunda linha, no primeiro gráfico donut, temos uma visão dos erros de disponibilidade da API; no segundo, uma visão dos erros de resiliência; e no terceiro, uma visão dos códigos de erro HTTP mais frequentes. Os três pequenos quadrados no canto inferior direito mostram o valor consolidado de RPS, taxa de erros e latência média do endpoint.
No dashboard acima, temos quatro colunas em cores diferentes, são elas da esquerda para direita: taxa de requests, taxa de erros total, taxa de erros de disponibilidade e taxa de erros de resiliência. Na primeira linha de cada coluna temos os valores absolutos e na segunda os valores relativos a quantidade total de requests.
Fiz o upload desse vídeo para demonstrar o funcionamento dos dashboards em tempo real.
Neste artigo, estamos abordando apenas o pilar de métricas da observabilidade. No entanto, existem outros pontos importantes aos quais devemos prestar atenção. Vamos explorar melhor essas questões.
Os Três Pilares da Observabilidade
Para que um sistema seja observável, existem três tipos de dados que devemos coletar: métricas, logs e tracing.
- Logs: são registros detalhados das atividades e eventos que ocorrem no sistema. Eles fornecem informações sobre situações que ocorrem no sistema, como: erros, exceções, transações e outras atividades relevantes.
- Métricas: são medidas que fornecem informações sobre o desempenho e o estado do sistema. Elas podem incluir dados como utilização de recursos, tempo de resposta, taxa de erros, throughput, latência, entre outros. As métricas são coletadas em intervalos regulares e ajudam a identificar tendências, anomalias e possíveis problemas em tempo real.
- Tracing: é uma técnica para monitorar o fluxo de uma solicitação específica ao longo de vários componentes do sistema. Ele captura informações sobre as etapas, tempos de resposta, erros encontrados e os componentes envolvidos no processamento da solicitação. O rastreamento de solicitações é útil para entender a causa-raiz de problemas, otimizar o desempenho e visualizar a jornada de uma solicitação por diferentes partes do sistema.
Esses três pilares da observabilidade, quando combinados, fornecem uma visão abrangente do sistema, permitindo uma compreensão detalhada de seu comportamento e facilitando a detecção e solução de problemas. Nesta série de artigos, vamos implementar os três pilares. No entanto, neste artigo, abordaremos apenas a parte de métricas. Iniciaremos nesse artigo a integração com o Prometheus e no próximo será a realizado a integração com o Grafana.
Integrando com o Prometheus
O Prometheus é um sistema de monitoramento e registro de métricas de código aberto, amplamente utilizado na comunidade de desenvolvimento. Ele foi projetado para coletar, armazenar e consultar métricas de diferentes sistemas e serviços em tempo real. O Prometheus segue o modelo de coleta de métricas por scraping, no qual os agentes do Prometheus são implantados em cada serviço ou aplicativo para expor métricas específicas.
Para definir um agente em nossa API, foi criado um pacote denominado (endpointmetrics) que utiliza a própria biblioteca do Prometheus para expor o endpoint de métricas e definir quais dados serão enviados. O pacote (endpointmetrics) possui duas funções principais: (Start) e (Send).
Note que, além das funções (Start) e (Send), definimos várias labels. Elas serão importantes para permitir filtros e agregações na plataforma web do Prometheus e Grafana quando começarmos a criar os gráficos. A função (Start) serve para habilitar o endpoint (/metrics) para que o Prometheus realize o scrap das métricas da nossa API. O endpoint habilitado retorna as métricas registradas em um texto plano, conforme exemplificado a seguir.
A função (Send) recebe as métricas que serão adicionadas no endpoint (/metrics) da aplicação. É importante destacar que o método (Send) utiliza outros dois pacotes: (histogrammetrics) e (countermetrics), os quais foram criados por mim para facilitar o tratamento de dados. A seguir, vamos explicar a função (Increment) do pacote (countermetrics).
O primeiro ponto que vale ressaltar é a utilização de uma goroutine para o envio dos dados. Optei por seguir dessa forma para evitar bloquear a execução da API toda vez que uma métrica fosse enviada para o Prometheus.
Outro ponto importante é a utilização do padrão singleton ao registrar as métricas. Esse padrão se mostrou necessário, uma vez que o Prometheus não suporta a criação de múltiplas métricas com o mesmo nome. Com esse padrão, garantimos que quando uma métrica é enviada, somente criamos ela na primeira vez, caso a mesma métrica venha a ser enviada novamente no futuro, será utilizada a métrica já criada do objeto (createdMetrics).
Para prosseguir com a integração entre nossa API e o Prometheus, precisei realizar duas alterações no arquivo docker-compose.yml.
As alterações foram somente essas duas:
- Adicionamos um nó de network e definimos duas redes: (transactional) e (metrics). A primeira será utilizada pelas imagens (product-api) e (product-db) com a finalidade de realizar operações transacionais, enquanto a segunda será usada exclusivamente para o tratamento das métricas.
- Adicionamos a imagem do prometheus e definimos que será utilizado a rede (metrics) para troca de dados entre o (prometheus) e a (product-api).
Abstraindo O Conceito de Endpoint
Para facilitar a tarefa de adicionar métricas à nossa API, realizamos uma mudança significativa no registro de endpoints. Anteriormente, tínhamos um único objeto chamado ProductHttp, que era responsável por carregar todos os endpoints. Agora, refatoramos o código para utilizar uma abstração de HttpHandler, e cada endpoint implementará essa nova interface. Isso nos permite adicionar as métricas de forma mais modular e flexível.
Essa abstração possui um método chamado (Handle), que recebe (*http.Request) e pode retornar dois objetos criados para o contexto de I/O da API: (ApiResponse) e (ApiError). Além do método principal (Handle), existem os métodos auxiliares (Name), (Verb) e (Pattern), que são utilizados para registrar os endpoints no arquivo principal. Com essa abstração criada, cada endpoint terá sua própria implementação de HttpHandler. No exemplo abaixo, é demonstrada a implementação do endpoint CreateProduct.
A utilização dessa nova abstração HttpHandler tem dois principais objetivos:
- Definir uma forma padrão e única de registrar os endpoints na API. À medida que a API cresce, é comum adicionar cada vez mais endpoints. Para lidar com isso, decidimos criar arquivos separados em vez de manter um único objeto (ProductHttp) extenso. Essa abordagem modular nos permite gerenciar melhor os endpoints e facilita a manutenção da API.
- Seguir o princípio do Open/Closed do SOLID, permitindo a adição de comportamentos nos endpoints sem a necessidade de alterar o código existente. Essa abordagem facilita a extração de métricas de todos os endpoints e a injeção de erros e de latência, visando validar a integração com o Prometheus.
Para enviar as métricas utilizando o pacote mencionado anteriormente (endpointmetrics), implementamos um objeto chamado (MetricsHandlerAdapter). Esse adapter permite centralizar o código que computa as métricas em um único ponto, facilitando a coleta e o envio das métricas para o Prometheus.
Com essa mudança na abordagem de registro de endpoints, o código da função principal (main) fica um pouco diferente. Agora, chamamos a função (endpointmetrics.Start) para iniciar a coleta de métricas. Em seguida, construímos um array de HttpHandlers e iteramos nesse array para compor os endpoints utilizando o (MetricsHandlerAdapter). Por fim, registramos os endpoints para que estejam prontos para receber as solicitações. Essa nova abordagem nos permite incorporar facilmente o monitoramento de métricas em todos os endpoints da aplicação.
Experimentos e Métricas
Para coletar as métricas da API, criei um pequeno script em Go. Esse script consiste em uma máquina de estados que realiza chamadas à API de maneira aleatória, seguindo uma lógica de transição entre as chamadas para simular um cenário caótico próximo à realidade. Para coletar os dados, executei o script por algum tempo e obtivemos aproximadamente 18.000 chamadas HTTP em endpoints diferentes. Esse processo nos permitiu obter uma amostra significativa de métricas para análise e monitoramento do desempenho da API.
Durante a execução do script, um decorator de HttpHandler foi habilitado para injetar erros e latência, a fim de validar a integração com o Prometheus. Com as métricas coletadas, conseguimos gerar alguns gráficos utilizando a PromQL, que é a linguagem de consulta do Prometheus.
No exemplo acima, utilizamos as labels (failed) e (endpoint), definidas anteriormente no pacote (endpointmetrics), para filtrar a quantidade de erros do endpoint (create_product).
Conclusões e Próximas Etapas
Neste artigo, apresentamos de forma breve o conceito de observabilidade, destacando sua importância, bem como os três pilares fundamentais desse conceito. Dado o trabalho envolvido para tornar uma API observável, focamos especificamente no pilar das métricas neste artigo, mostrando uma integração com o Prometheus. Vale ressaltar que a observabilidade é um processo contínuo e complexo, e outros pilares, como logs e tracing, também são essenciais para uma visão abrangente do sistema. Esses pilares serão abordados em futuros artigos, fornecendo uma compreensão completa e aprofundada sobre como tornar sua API totalmente observável.
Apesar de conseguirmos gerar gráficos na plataforma web do Prometheus, ainda não chegamos ao ápice da leitura das métricas coletadas dessa forma. Para alcançar esse objetivo, faremos a integração do Prometheus com o Grafana no próximo artigo dessa série. Isso nos permitirá criar painéis de controle como os exemplificados no início do artigo e até mesmo trabalhar com monitoramento mais avançado.