Observabilidade: Monitorando sua aplicação a partir de métricas

Guilherme Biff Zarelli
luizalabs
Published in
8 min readJan 28, 2023

--

O objetivo desse artigo é mostrar como adicionar métricas em suas aplicações, suas vantagens e como montar uma stack com o Prometheus e Grafana para um monitoramento de qualidade — com exemplos em Java.

Ilustração: Micrometer, Grafana e Prometheus

Métricas são indicadores numéricos utilizados para medir desempenho, disponibilidade, negócio e outros aspectos de um sistema ou processo. Elas podem ser coletadas automaticamente através de ferramentas como o Prometheus, e com elas é possível acompanhar o desempenho de um sistema por sistemas de visualização como o Grafana a fim de identificar problemas e tomar decisões informadas, assim conseguimos entender melhor como um sistema está se comportando e tomar ações para melhorá-lo.

“Metrics are the foundation of observability. They give us the data we need to understand what’s happening in our systems.” — John Allspaw, CTO at Stitch Fix

Por que não usar os Logs?

Gerar métricas a partir de logs é uma prática comum entre desenvolvedores, no entanto, existem várias razões pelas quais essa prática não é recomendada. Algumas delas incluem:

  1. Escalabilidade: coletar e armazenar métricas geralmente é mais escalável do que coletar e armazenar logs. Métricas geralmente são números agregados, enquanto logs podem ser grandes arquivos de texto.
  2. Custo: armazenar, pesquisar e analisar esses grandes volumes de dados de log pode consumir muitos recursos e ser caro.
  3. Tempo de busca: métricas geralmente são armazenadas em bancos de dados time-series, o que permite uma busca mais rápida e eficiente do que buscar em arquivos de log.
  4. Análise: métricas são mais fáceis de analisar do que logs, pois elas fornecem dados numéricos estruturados e normalizados, enquanto logs são geralmente texto não estruturado.
  5. Amostragem: métricas são coletadas em intervalos regulares, o que permite uma amostragem consistente do estado do sistema, enquanto logs são gerados somente quando algo acontece.

As métricas podem ser usadas para monitorar o desempenho, reconhecer eventos importantes e facilitar a previsão de falhas futuras e criação de alertas. Os logs geralmente são usados para troubleshooting, e também para analisar o comportamento do usuário/sistema. Ambos criam uma base de observabilidade complementar.

Imagem do artigo Logs vs Metrics do splunk.com

Como instrumentar?

Existem atualmente diversas libs para instrumentar sua aplicação por métricas. Nesse artigo, vamos usar o Micrometer, sendo o mais comum e recomendado no ecossistema Java, além de ter uma auto-instrumentação bem completa que nos fornece métricas sobre class loader, garbage collection, processor utilization, thread pools, entre outras.

Para instrumentação em outras linguagens existem clients do próprio Prometheus na qual pode ser utilizado de forma bem simples, veja: https://prometheus.io/docs/instrumenting/clientlibs/

Os tipos mais comuns de métricas para instrumentar o código são:

  • Contadores: usados para contar o número de vezes que um evento ocorreu.
  • Temporizadores: usados para medir o tempo que leva para que um evento ocorra.
  • Gauges: usados para medir um valor instantâneo.

Depois de instrumentar temos que disponibilizar essas métricas, normalmente via API para que o Prometheus as colete. Os Frameworks atualmente disponibilizam esse recurso via libs, por exemplo em Java com o Spring Boot, basta adicionarmos o starter-web e o actuator, no Quarkus o resteasy já é o suficiente, ambos com a biblioteca do micrometer. Caso queira fazer na mão, sem a dependência de um framework também é possível, na própria documentação do Prometheus temos exemplos: https://micrometer.io/docs/registry/prometheus

Instrumentando a aplicação

Para demostração, utilizarei apenas o Java com o Quarkus, porém os steps devem ser bem similares para outros ambientes. No repositório desse artigo adicionarei outro exemplo com o Spring Boot.

1 — Dependências para a instrumentação / exportação das métricas (exemplo das dependências no pom.xml):

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>

2 — Instrumentando um endpoint. No exemplo, estamos criando um endpoint no Path /hello com um Query Param Stringname’ .

Usando a classe MeterRegistry do micrometer vamos gerar uma métrica de duração de uma função específica (hello_get_do_process_timer) e outra métrica contadora(hello_get_name_length_limit), na qual só será incrementada se o tamanho do ‘name’ recebido for maior que o limite configurado.

Caso não queira injetar o MeterRegistry, as annotations são bem fáceis e úteis de serem inseridas. Para exemplificar, na assinatura do método coloquei uma annotation para gerar uma métrica de tempo da função como um todo (hello_get_method_timer).

package br.com.helpdev;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import java.util.concurrent.ThreadLocalRandom;

import io.micrometer.core.annotation.Timed;
import io.micrometer.core.instrument.MeterRegistry;

@Path("/")
public class HelloController {

private static final int LENGTH_LIMIT = 10;

@Inject
MeterRegistry registry;

@GET
@Path("/hello")
@Produces(MediaType.TEXT_PLAIN)
// Exemplo de instrumentação de tempo por annotations
@Timed("hello_get_method_timer")
public String getString(@QueryParam("name") final String name) {

// Exemplo para registrar uma metrica de duração
registry.timer("hello_get_do_process_timer").record(() -> {
doProcess();
});

// Exemplo de metrica de contagem; Quando o tamanho do nome for maior que o limite, incrementa
if (name.length() > LENGTH_LIMIT)
registry.counter("hello_get_name_length_limit", "length", String.valueOf(name.length()))
.increment();

return "Hello " + name;
}

// simula um processamento
private void doProcess() {
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(1000));
} catch (InterruptedException e) {
}
}
}

3 — Execute a aplicação, faça as devidas chamadas no endpoint /hello?name={any name} e confira as métricas geradas pelo endpoint /q/metrics ; Como dito anteriormente, o Micrometer já faz uma auto-instrumentação de sua aplicação, e mostrará diversas métricas da JVM e outras da aplicação como as do resteasy (dependendo da lib que for usar, ela já pode ser auto-instrumentada e se necessário precisaria habilitar no application.properties , veja sempre a documentação).

[...]
# Exemplo de métricas auto-instrumentada pelo resteasy:
http_server_requests_seconds_count{method="GET",outcome="SUCCESS",status="200",uri="/hello",} 4.0
http_server_requests_seconds_sum{method="GET",outcome="SUCCESS",status="200",uri="/hello",} 1.618068076
http_server_requests_seconds_count{method="GET",outcome="CLIENT_ERROR",status="404",uri="NOT_FOUND",} 1.0
http_server_requests_seconds_sum{method="GET",outcome="CLIENT_ERROR",status="404",uri="NOT_FOUND",} 0.023389064
[...]
# Exemplo de métricas auto-instrumentada pelo micrometer com dados da jvm:
jvm_memory_committed_bytes{area="heap",id="G1 Survivor Space",} 1.6777216E7
jvm_memory_committed_bytes{area="heap",id="G1 Old Gen",} 1.48897792E8
jvm_memory_committed_bytes{area="nonheap",id="Metaspace",} 5.3280768E7
jvm_memory_committed_bytes{area="nonheap",id="CodeCache",} 1.9267584E7
jvm_memory_committed_bytes{area="heap",id="G1 Eden Space",} 1.44703488E8
jvm_memory_committed_bytes{area="nonheap",id="Compressed Class Space",} 7667712.0
[...]

# Métricas que instrumentamos na aplicação
hello_get_method_timer_seconds_count{class="br.com.helpdev.HelloController",exception="none",method="getString",} 4.0
hello_get_method_timer_seconds_sum{class="br.com.helpdev.HelloController",exception="none",method="getString",} 1.598810142
hello_get_method_timer_seconds_max{class="br.com.helpdev.HelloController",exception="none",method="getString",} 0.817407475

hello_get_do_process_timer_seconds_count 4.0
hello_get_do_process_timer_seconds_sum 1.903437705
hello_get_do_process_timer_seconds_max 0.95616954

hello_get_name_length_limit_total{length="23",} 2.0
hello_get_name_length_limit_total{length="11",} 2.0

Agora que a aplicação está devidamente instrumentada, podemos configurar nossa stack de observabilidade para utilizar o Grafana, configurado com o datasource do Prometheus, para criar nossos dashboards de monitoramento.

Configurando a Stack

Arquitetura de uma Stack com o Prometheus e Grafana

Vamos usar o docker-compose para criarmos nossa stack, nele, vamos configurar o Prometheus para raspar as métricas da aplicação e o Grafana já com o datasource e uma dashboard default para monitorar uma aplicação Java. A stack será configurada com o network mode: host, assim, conseguimos criar um ambiente local para o desenvolvedor executar a aplicação sem precisar buildar sua imagem docker.

version: "3"
services:

prometheus:
image: prom/prometheus:v2.26.0
container_name: prometheus
restart: always
network_mode: host
expose:
- 9090
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml

grafana:
image: grafana/grafana:9.3.4
container_name: grafana
network_mode: host
expose:
- 3000
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
depends_on:
- prometheus

Agora vamos configurar os volumes mapeados:

1./prometheus/prometheus.yml Esse arquivo contém as configurações do Prometheus, que iremos configurar em quais serviços ele irá raspar as métricas e seu intervalo.

global:
scrape_interval: 5s
evaluation_interval: 5s

scrape_configs:
- job_name: 'java-quarkus'
metrics_path: '/q/metrics'
static_configs:
- targets: ['localhost:8080']
labels:
application: java-quarkus-metrics

2./grafana/provisioning/datasources Diretório para configuração dos datasources do Grafana; Arquivo datasource.yml :

apiVersion: 1

datasources:
- name: Prometheus
type: prometheus
access: proxy
orgId: 1
url: http://localhost:9090
basicAuth: false
isDefault: true
editable: true

3./grafana/provisioning/dashboards Diretório para configuração dos dashboards do Grafana; Podemos adicionar nossos dashboards defaults aqui para subir já com a infra, muito útil para manter as dashboards da aplicação compartilhadas junto com a stack no repositório do projeto.

Nesse diretório precisamos ter um arquivo provisioning.yml com a seguinte configuração:

apiVersion: 1

providers:
- name: 'default'
type: file
disableDeletion: false
updateIntervalSeconds: 60
options:
path: /etc/grafana/provisioning/dashboards

Ele faz com que o Grafana carregue os arquivos *.json armazenados nesse diretório, nesse exemplo, vamos adicionar a dashboard jvm-micrometer_rev9.json (veja aqui) para já termos um monitoramento default da JVM para nossa aplicação.

Agora, basta rodar sua aplicação e executar o docker-compose da stack.

Para acessar a interface gráfica do Prometheus, basta acessar http://localhost:9090 e consultar suas métricas.

Exemplo da interface gráfica do Prometheus

Caso queira aprender mais sobre querys no Prometheus, acesse a doc oficial deles (aqui). Será bem útil para elaborar suas dashs no Grafana.

Para acessar o Grafana, vamos acessar o endereço http://localhost:3000, utilize o usuário e senha: admin. No primeiro acesso irá solicitar a alteração de senha, agora, já podemos consultar em http://localhost:3000/dashboards a dashboard da JVM pré carregada que configuramos no docker-compose.

Dashboard para monitoramento da JVM da aplicação

Com a stack pronta, coletando as métricas de nossa aplicação, é hora de criar uma dashboard para monitorarmos a nossa aplicação.

“Without monitoring, you don’t know if you have a problem. Without observability, you don’t know what the problem is.” — “Observability in Microservices”

Monitoramento: Criando dashboards com o Grafana

Para criar uma dashboard, basta acessarmos o painel e clicarmos em ‘New

Agora adicionaremos um Painel e criaremos um ‘Pie chart’ para ilustrar as chamadas do endpoint /hello que excedeu o limite de caracteres do Query Param name, agrupando pela quantidade de caracteres dessa ocorrência:

A ‘raw query’ para consultar no Prometheus ficaria dessa forma:

sum by(length) (hello_get_name_length_limit_total{application=”java-quarkus-metrics”})

Podemos também criar um Paineltime series’ para mostrar em um rate de 1min o tempo de execução do método doProcess :

rate(hello_get_do_process_timer_seconds_sum{application=”java-quarkus-metrics”}[1m])

Agora já temos uma dashboard inicial para monitorar nossa aplicação 👏:

Exemplo de dashboard com métricas de negócio

Uma dashboard de monitoramento no Grafana é uma ferramenta poderosa para garantir a disponibilidade e desempenho de um software. Ela permite visualizar de forma clara e intuitiva diversos indicadores-chave de negócio e desempenho, como taxas de erro, tempo de resposta, uso de recursos e outros. Isso facilita a criação de alertas e na identificação de problemas e tendências, permitindo que os administradores e desenvolvedores tomem medidas para melhorar a qualidade do software. Além disso, a dashboard do Grafana é altamente personalizável, permitindo que os usuários criem relatórios e gráficos personalizados para atender às suas necessidades específicas. Ter uma dashboard de monitoramento no Grafana é uma vantagem valiosa para garantir que um software esteja sempre disponível e performando de forma adequada, melhorando a experiência do usuário e a confiabilidade do sistema.

Repositório do artigo

Referências

--

--