Monitoramento de uma aplicação em Spring Boot com os três pilares da observabilidade no Grafana:
-
Rastreamento com Grafana Tempo e OpenTelemetry para Java
-
Métricas com Prometheus, Spring Boot Actuator e Micrometer
-
Centralização de logs com Grafana Loki e Logback
Errata: a rota de extração de métricas de todas as aplicações é a
/actuator/prometheus
-
Instale o driver do Loki para extração de logs dos containers
docker plugin install grafana/loki-docker-driver:latest --alias loki --grant-all-permissions
Atenção: atente-se a versão instalada pois ela deverá ser a mesma do container do Grafana Loki
-
Inicialize todos os contêineres
docker compose up -d
-
Execute scripts simulando interações nas aplicações
sh scripts/tracing.sh && sh scripts/requests.sh
Obrigatório: é necessário instalar o pacote siege
Dica: se preferir, remova comentários na parte de configuração do container do Grafana K6 no
docker-compose.yml
ou através do Swagger UI disponível na rota/swagger-ui/index.html
-
Acesse o Grafana através do endereço localhost:3000 e vá até o painel
Aplicação
Credenciais: o login é
admin-a
e senha égrafana
Captura de tela exemplar:
Este painel predefinido para aplicações em Spring Boot está disponível no Marketplace de dashboards do Grafana.
Grafana fornece uma grande solução, no qual podemos monitorar ações específicas em um serviço entre rastreamentos, métricas e logs através do ID de rastreamento.
Fonte da imagem: Grafana
Obtenha o ID de rastreamento de um exemplo nas métricas, e então busque-a no Grafana Tempo.
Pesquise por histogram_quantile(.99,sum(rate(http_server_requests_seconds_bucket{application="app-a", uri!="/actuator/prometheus"}[1m])) by(uri, le))
e converta em um exemplar nas opções.
Obtenha o ID de rastreamento e tags (que é compose.service
) definida na fonte de dados do Grafana Tempo, e então faça a busca no Grafana Loki.
Obtenha o ID de rastreamento no log (RegExp estão habilitadas a partir da fonte de dados do Grafana Loki), e então pesquise através do Grafana Tempo.
Para um cenário mais complexo, usaremos estas aplicações em Spring Boot com o mesmo código nesta demonstração. Há uma ação de múltiplos serviços na rota /chain
, na qual fornece um bom exemplo de como a instrumentação do OpenTelemtry funciona e como o Grafana apresenta essa informação para rastreamento.
OpenTelemetry para Java possui um SDK para instrumentação automática:
java -javaagent:localização/do/opentelemetry-agent.jar -jar [nome da aplicação].jar
O agente suporta diversas bibliotecas, incluindo o Spring Web MVC. Segundo o documento traduzido do inglês:
Pode ser usado para capturar dados de telemetria nos "limites" de uma aplicação ou serviço, tais como requisições de entrada, chamadas de saídas HTTP, chamadas ao banco de dados e muito mais
Configuração do OpenTelemetry
// Fornecedor automatizado de logs, métricas e traces do OpenTelemetry
package com.example.app;
import io.prometheus.client.exemplars.tracer.otel_agent.OpenTelemetryAgentSpanContextSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class OpenTelemetryConfig {
@Bean
public OpenTelemetryAgentSpanContextSupplier OTelAgentSupplier() {
return new OpenTelemetryAgentSpanContextSupplier();
}
}
Então não precisamos modificar qualquer linha de código de nosso repositório. O agente se encarregará de tudo automaticamente. Neste projeto, temos três tipos de ações que podem ser capturadas pelo agente:
-
Requisições HTTP: captura informações HTTP tais como método da requisição, status e entre outros.
-
Ações do PostgreSQL (POST
/peanuts
e a primeira requisição do GET/peanuts/{id}
): captura informação do banco de dados como a declaração em SQL, nome da tabela e muito mais. -
Comandos do Redis(Da segunda requisição GET
/peanuts/{id}
): captura informação do Redis como comandos, chaves e outros detalhes.
As confurações como as do exportador, são listadas no documento, e são utilizadas pelo agente a partir de um ou mais fontes seguintes (ordenadas da alta até a menor prioridade):
- Propriedades do sistema
- Variáveis de ambiente
- O arquivo de configuração
- O SPI do ConfigPropertySource
Neste projeto de apresentação, usaremos variáveis de ambiente para definir as configurações do agente:
app-a:
build: ./app/
environment:
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tempo:4317
- OTEL_SERVICE_NAME=app-a
- OTEL_RESOURCE_ATTRIBUTES=compose_service=app-a
- OTEL_METRICS_EXPORTER=none
ports:
- 8080:8080
Ou usando um arquivo de configuração, que é uma outra alternativa comum para configurar o agente:
# otel.properties
otel.exporter.otlp.endpoint=http://tempo:4317
otel.service.name=app-a
otel.resource.attributes=compose_service=app-a
otel.metrics.exporter=none
# Defina o "otel.javaagent.configuration-file" com as propriedades do sistema
java -javaagent:path/to/opentelemetry-javaagent.jar \
-Dotel.javaagent.configuration-file=path/to/otel.properties \
-jar myapp.jar
Para mais detalhes de configuração, consulte a documentação oficial do OpenTelemetry.
O agente do OpenTelemetry adicionará informação automaticamente para cada log.
Agente do OpenTelemtry injeta a informação atual em cada evento de log da cópia do MDC (Mapped Diagnostic Context):
trace_id
- o atual ID de rastreamento (equivalente ao Span.current().getSpanContext().getTraceId());span_id
- o atual ID (equivalente aSpan.current().getSpanContext().getSpanId());
)trace_flags
- flags de rastreamento atual, formatada de acordo com as regras de traceflags da W3C (equivalente aSpan.current().getSpanContext().getTraceFlags().asHex()
).
Referências técnicas: Instrumentação automática com Logger MDC
Neste projeto demonstrativo, trocamos o logging.pattern.level
no arquivo application.yaml
:
logging:
pattern:
level: "trace_id=%mdc{trace_id} span_id=%mdc{span_id} trace_flags=%mdc{trace_flags} %p"
O log terá sua saída neste formato:
2022-10-10 15:18:54.893 trace_id=605c7adf03bdb2f2917206de1eae8f72 span_id=c581b882e2d252c2 trace_flags=01 ERROR 1 --- [nio-8080-exec-6] com.example.app.AppApplication : Olá Java!
Como mencionado anteriromente, o agente do OpenTelemtry captura dados nos "limites" de uma aplicação ou serviço, como requisições de entradas e saída de chamadas HTTP. Não necessitamos de adicionar nada no código. Para demonstrar, providenciamos a rota /chain
na aplicação a seguir:
@GetMapping("/chain")
public String chain() throws InterruptedException, IOException {
String TARGET_ONE_HOST = System.getenv().getOrDefault("TARGET_ONE_HOST", "localhost");
String TARGET_TWO_HOST = System.getenv().getOrDefault("TARGET_TWO_HOST", "localhost");
logger.debug("Cadeia inicializando ...");
Request.Get("http://localhost:8080/").execute().returnContent();
Request.Get(String.format("http://%s:8080/io_task", TARGET_ONE_HOST)).execute().returnContent();
Request.Get(String.format("http://%s:8080/cpu_task", TARGET_TWO_HOST)).execute().returnContent();
logger.debug("Cadeia encerrada");
return "chain";
}
Ao acessar a rota chain
do container app-a
, irá enviar requisições para sua própria rota base (/
) e outros dois serviços como o io_task
e cpu_task
em ordem. No processo todo, não digitamos nenhuma linha de código do OpenTelemetry, rastreamento ou até mesmo operação da aplicação. Mas o log mostra todas as requisições recebidas e saídas de chamada HTTP adicionadas a informação de operação a seguir:
# Start from app-a chain
2022-10-10 15:57:12.828 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=6cc84ac5ed4cf68c trace_flags=01 DEBUG 1 --- [nio-8080-exec-6] com.example.app.AppApplication : Cadeia inicializando ...
# In app-a root
2022-10-10 15:57:13.106 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=4745d1a1f588a949 trace_flags=01 ERROR 1 --- [nio-8080-exec-7] com.example.app.AppApplication : [traceparent:"00-743ae05db90d00fd65998ff30cf7094d-d72a1422522ce837-01", host:"localhost:8080", connection:"Keep-Alive", user-agent:"Apache-HttpClient/4.5.13 (Java/1.8.0_342)", accept-encoding:"gzip,deflate"]
2022-10-10 15:57:13.106 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=4745d1a1f588a949 trace_flags=01 ERROR 1 --- [nio-8080-exec-7] com.example.app.AppApplication : Olá Java!
2022-10-10 15:57:13.106 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=4745d1a1f588a949 trace_flags=01 DEBUG 1 --- [nio-8080-exec-7] com.example.app.AppApplication : Log de depuração
2022-10-10 15:57:13.106 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=4745d1a1f588a949 trace_flags=01 INFO 1 --- [nio-8080-exec-7] com.example.app.AppApplication : Log de informação
2022-10-10 15:57:13.106 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=4745d1a1f588a949 trace_flags=01 WARN 1 --- [nio-8080-exec-7] com.example.app.AppApplication : Log de aviso!
2022-10-10 15:57:13.106 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=4745d1a1f588a949 trace_flags=01 ERROR 1 --- [nio-8080-exec-7] com.example.app.AppApplication : Log de erro
# In app-b io_task
2022-10-10 15:57:14.141 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=b97df0b1834ab84a trace_flags=01 INFO 1 --- [nio-8080-exec-4] com.example.app.AppApplication : Tarefa I/O
# In app-c cpu_task
2022-10-10 15:57:14.191 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=7fd693eefc0d3387 trace_flags=01 INFO 1 --- [nio-8080-exec-4] com.example.app.AppApplication : Tarefa da CPU
# Back to app-a chain
2022-10-10 15:57:14.199 trace_id=743ae05db90d00fd65998ff30cf7094d span_id=6cc84ac5ed4cf68c trace_flags=01 DEBUG 1 --- [nio-8080-exec-6] com.example.app.AppApplication : Cadeia encerrada
Cada rota possui a mesma propriedade trace_id como 743ae05db90d00fd65998ff30cf7094d
inicializando a partir do container app-a
na rota chain
. A auto-injeção do traceparent
(pode ser vista na primeira linha nos logs do container app-a
) é como o agnete do OpenTelemetry transmite através destes serviços
Each endpoint got the same trace_id 743ae05db90d00fd65998ff30cf7094d
start from app-a chain
. The auto-injected traceparent
(could saw in the first line in app-a root log) is how OpenTelemetry Agent passed through all these services.
Para obter as métricas do Prometheus vindas da aplicação, precisamos de duas dependências:
- Spring Boot Actuator: O Actuator fornece muitas funcionalidades para monitoramente por HTTP ou rotas JMX para aplicações em Spring Boot.
- Micrometer: Micrometer fornece uma API geral para coletar métricas e transforma o formato para diferentes sistemas de monitoramento, incluindo o Prometheus.
Adicione estas duas dependências para o arquivo pom.xml
e configuração para o application.yaml
conforme o exemplo abaixo:
<!-- pom.xml -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
# application.yaml
management:
endpoints:
web:
exposure:
include: prometheus # Apenas para exposição da rota "/actuator/prometheus"
metrics:
tags:
application: app # Adicione uma tag para cada métrica do Prometheus
Métricas do Prometheus parecerão como isto na rota /actuator/prometheus
:
# HELP executor_active_threads The approximate number of threads that are actively executing tasks
# TYPE executor_active_threads gauge
executor_active_threads{application="app",name="applicationTaskExecutor",} 0.0
[...]
HELP http_server_requests_seconds Duration of HTTP server request handling
# TYPE http_server_requests_seconds summary
http_server_requests_seconds_count{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",} 1.0
http_server_requests_seconds_sum{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",} 0.047062542
http_server_requests_seconds_count{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/prometheus",} 2.0
http_server_requests_seconds_sum{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/prometheus",} 0.053801375
# HELP http_server_requests_seconds_max Duration of HTTP server request handling
# TYPE http_server_requests_seconds_max gauge
http_server_requests_seconds_max{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",} 0.047062542
http_server_requests_seconds_max{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/actuator/prometheus",} 0.045745625
[...]
Exemplar é um novo tipo de dado proposto no OpenMetrics. Para habilitar a funcionalidade do Exemplar, há alguns requisitos de dependências:
- Spring Boot em versão igual ou superior a
2.7.0
: Spring Boot suporta o Exemplar do Prometheus desde a versão v2.7.0-RC1. - Micrometer em versão igual ou superior a
1.10.0
: Micrometer suporta Exemplar para o Histogramas e Contadores do Prometheus desde a versão 1.9.0 e usandoio.prometheus.simpleclient_common
em versão 0.16.0 desde a versão 1.10.0.
Além disso, necessitaremos de adicionar uma amostra do Exemplar Exemplar Sampler (Fonte: qaware/cloud-observability-grafana-spring-boot) como mostra a seguir:
package com.example.app;
import io.prometheus.client.exemplars.tracer.otel_agent.OpenTelemetryAgentSpanContextSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class PrometheusExemplarSamplerConfiguration {
@Bean
public OpenTelemetryAgentSpanContextSupplier openTelemetryAgentSpanContextSupplier() {
return new OpenTelemetryAgentSpanContextSupplier();
}
}
A dicussão sobre uma amostra do Exemplar está em Suporte de Exemplares para o Histograma do Prometheus #2812 no repositório do Micrometer.
Quando todas as dependências estiverem adicionadas, podemos adicionar distribuição de métrica para o Prometheus no arquivo application.yaml
.
management:
metrics:
distribution:
percentiles-histogram:
http:
server:
requests: 'true'
Verifique por mais opções de distribuição de métricas na documentação oficial do Spring Boot.
Como mencionado anteriormente, Exemplar é um novo tipo de dado proposto no OpenMetrics e o padrão da rota /actuator/prometheus
para fornecer métricas com o formato do Prometheus. Então necessitamos adicionar alguns cabeçalhos para obter as métricas com o formato do OpenMetrics como mostrar a seguir:
curl 'http://localhost:8080/actuator/prometheus' -i -X GET \
-H 'Accept: application/openmetrics-text; version=1.0.0; charset=utf-8'
As métricas do histograma com ID de rastreamento (que começam com #
), irão parecer como isto:
# TYPE http_server_requests_seconds histogram
# HELP http_server_requests_seconds Duration of HTTP server request handling
http_server_requests_seconds_bucket{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",le="0.001"} 0.0
http_server_requests_seconds_bucket{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",le="0.001048576"} 0.0
http_server_requests_seconds_bucket{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",le="0.001398101"} 0.0
http_server_requests_seconds_bucket{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",le="0.001747626"} 0.0
http_server_requests_seconds_bucket{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",le="0.002097151"} 0.0
http_server_requests_seconds_bucket{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",le="0.002446676"} 0.0
http_server_requests_seconds_bucket{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",le="0.002796201"} 1.0 # {span_id="55255da260e873d9",trace_id="21933703cb442151b1cef583714eb42e"} 0.002745959 1665676383.654
http_server_requests_seconds_bucket{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",le="0.003145726"} 1.0
http_server_requests_seconds_bucket{application="app",exception="None",method="GET",outcome="SUCCESS",status="200",uri="/",le="0.003495251"} 2.0 # {span_id="81222a08c4f050fe",trace_id="eddcd9569d20b4aa48c06d3b905f32ea"} 0.003224625 1665676382.620
Coleta métricas das aplicações e expõe em uma rota
Defina todo o rascunho/scrape de métricas de todas as aplicações no arquivo localizado em ./configs/prometheus/prometheus.yml
.
Prometheus irá extrair automaticamente o formato de métricas do OpenMetrics, e não há necessidade de adicionar configuração especifica de cabeçalhos quando extraindo da rota /actuator/prometheus
.
# [...]
scrape_configs:
- job_name: 'app-a'
scrape_interval: 5s
metrics_path: "/actuator/prometheus"
static_configs:
- targets: ['app-a:8080']
- job_name: 'app-b'
scrape_interval: 5s
metrics_path: "/actuator/prometheus"
static_configs:
- targets: ['app-b:8080']
- job_name: 'app-c'
scrape_interval: 5s
metrics_path: "/actuator/prometheus"
static_configs:
- targets: ['app-c:8080']
Adicione um Exemplar no qual usa o valor do rótulo TraceID
para vincular ao Grafana Tempo.
Exemplo de configuração de transmissão de dados ao Grafana:
Exemplo de configuração predefinida de fonte de dados do Grafana:
name: Prometheus
type: prometheus
typeName: Prometheus
access: proxy
url: http://prometheus:9090
password: ''
user: ''
database: ''
basicAuth: false
isDefault: true
jsonData:
exemplarTraceIdDestinations:
- datasourceUid: tempo
name: TraceID
httpMethod: POST
readOnly: false
editable: true
Receptor de operações das aplicações
Configuração de rastreamento em logs:
-
Fonte de dados: fonte do log alvo.
-
Tags: Chave de tags ou atributos em nível de processo vindo do rastreamento, no qual irá ser o critério de busca de log se a chave existir no rastreamento.
-
Mapeamento de nomes de tag: converte uma chave existente de tags ou atributos em nível de processo do rastreamento para outra chave, e então usado como critério de busca de log. Use esta funcionalidade quando os valores de tag de rastreamento e rótulo do log são idênticos mas com chaves diferentes.
Exemplo de configuração predefinida de fonte de dados para o Grafana:
Exemplo de configuração de fonte de dados do Grafana:
name: Tempo
type: tempo
typeName: Tempo
access: proxy
url: http://tempo
password: ''
user: ''
database: ''
basicAuth: false
isDefault: false
jsonData:
nodeGraph:
enabled: true
tracesToLogs:
datasourceUid: loki
filterBySpanID: false
filterByTraceID: true
mapTagNamesEnabled: false
tags:
- compose_service
readOnly: false
editable: true
Centralizador de logs com o plugin do Docker de todos os serviços
- Use a funcionalidade de ancoramento e apelidação no YAML para definir opções de log em cada serviço.
- Defina as opções do driver do Grafana Loki no Docker
- loki-url: endereço do Grafana Loki
- loki-pipeline-stages: processa múltiplas linhas de log da aplicação com estágios de múltiplas linhas de RegExp (referência)
version: '3.9'
x-logging: &default-logging # ancôra(&): 'default-logging' que define um pedaço/parte (chunk) da configuração
driver: loki
options:
loki-url: http://loki:3100/api/prom/push
loki-pipeline-stages: |
- multiline:
firstline: '^\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}:\d{2}'
max_wait_time: 3s
- regex:
expression: '^(?P<time>\d{4}-\d{2}-\d{2} \d{1,2}:\d{2}:\d{2},d{3}) (?P<message>(?s:.*))$$'
# Use os caracteres $$ (símbolo do dólar) quando sua configuração necessita de um símbolo literal do dólar.
services:
container-1:
image: nginx:latest
logging: *default-logging # apelido(*): refere-se a parte chamada 'default-logging'
Adiciona um ID de rastreamento derivado do campo para extrair o ID e vincular ao Grafana Tempo a partir deste mesmo ID.
Configuração exemplar de fonte de dados do Grafana Tempo:
Exemplo de configuração predefinida do Grafana Tempo:
name: Loki
type: loki
typeName: Loki
access: proxy
url: http://loki:3100
password: ''
user: ''
database: ''
basicAuth: false
isDefault: false
jsonData:
derivedFields:
- datasourceUid: tempo
matcherRegex: (?:trace_id)=(\w+)
name: TraceID
url: $${__value.raw}
# Use os caracteres $$ (símbolo do dólar) quando sua configuração necessita de um símbolo literal do dólar.
readOnly: false
editable: true
-
Adicione o Prometheus, Tempo e Loki para a fonte de dados com o arquivo de configuração no caminho
./configs/grafana/datasource.yml
. -
Carregue um painel predefinido a partir dos arquivos localizados em
.configs/grafana/dashboards.yaml
e também./configs/grafana/dashboards/spring-boot-observability.json
# Configuração individual do container do Grafana no docker-compose.yml
grafana:
container_name: grafana
build: ./configs/grafana/
user: root
ports:
- 3000:3000
volumes:
- ./configs/grafana/datasource.yml:/usr/share/grafana/conf/provisioning/datasources/datasources.yml # Fonte de dados
- ./configs/grafana/dashboards.yml:/usr/share/grafana/conf/provisioning/dashboards/dashboards.yml # Configurações do painel
- ./configs/grafana/dashboards:/usr/share/grafana/dashboards # Diretório de arquivos JSON de painéis