a. Visão geral
Objetivo
Adicionar instrumentação OpenTelemetry a todos os serviços Spring Boot para que uma única requisição de usuário — passando por Gateway → OrderService → ProductService — gere um trace distribuído unificado, visível no Jaeger, com os trace IDs injetados automaticamente nas linhas de log.
Você já tem o Prometheus coletando métricas e o Grafana exibindo dashboards. Métricas respondem "quanto" e "com que frequência". Elas não conseguem dizer qual serviço, dentro de uma cadeia de requisições com vários saltos, é o responsável por uma latência de cauda de 2 segundos. O rastreamento distribuído responde a essa pergunta.
Slides
Clique nos slides e use ← → para navegar; F abre em tela cheia e P abre a visão do apresentador, com as notas. Abrir os slides em uma nova aba
O problema que isso resolve
Considere uma resposta lenta de /orders. O Prometheus mostra http_server_requests_seconds elevado no gateway, mas o gateway delega ao OrderService, que por sua vez chama o ProductService e o banco de dados. A métrica não consegue dizer para onde foi o tempo.
Sem rastreamento, a única opção é ficar olhando os logs de quatro serviços separados e chutar.
sequenceDiagram
participant C as Cliente
participant G as Gateway
participant O as OrderService
participant P as ProductService
participant D as Banco de dados
C->>G: POST /orders (? ms)
G->>O: POST /orders (? ms)
O->>P: GET /products/{id} (? ms)
P->>D: SELECT ... (? ms)
D-->>P: linha
P-->>O: ProductDTO
O-->>G: OrderDTO
G-->>C: 200 OK (total: 2 340 ms) Com rastreamento, cada salto é medido como um span, e todos os spans de uma requisição compartilham um trace ID. A UI do Jaeger desenha o grafo de chamadas completo como um flame chart, para que você veja o span lento imediatamente.
sequenceDiagram
participant C as Cliente
participant G as Gateway
participant O as OrderService
participant P as ProductService
participant D as Banco de dados
C->>G: POST /orders<br/>traceparent: 00-4bf9...36-01
note over G: span: gateway.POST /orders (45 ms)
G->>O: POST /orders<br/>traceparent: 00-4bf9...36-01
note over O: span: order.POST /orders (2 250 ms)
O->>P: GET /products/7<br/>traceparent: 00-4bf9...36-01
note over P: span: product.GET /products/7 (2 190 ms)
P->>D: SELECT ...
note over D: span: jdbc.query (2 180 ms)
D-->>P: linha
P-->>O: ProductDTO
O-->>G: OrderDTO
G-->>C: 200 OK trace_id=4bf9...36 O gargalo fica visível na hora: a query JDBC dentro do ProductService responde por quase toda a latência.
Arquitetura
Este hands-on adiciona o Jaeger à camada de observabilidade e conecta o agente Java do OpenTelemetry ao container de cada serviço:
flowchart LR
subgraph obs [Camada de observabilidade]
prometheus@{ shape: cyl, label: "Prometheus\n:9090" }
grafana[Grafana\n:3000]
jaeger["Jaeger\n:16686 UI\n:4317 OTLP"]:::highlighted
prometheus --> grafana
end
subgraph api [Camada confiável]
loadbalancer --> gateway
gateway --> order
gateway --> account
order --> product
order --> db@{ shape: cyl, label: "PostgreSQL" }
end
gateway & order & account & product e1@-->|"traces OTLP"| jaeger
prometheus e2@-.->|"scrape /actuator/prometheus"| gateway & order & account & product
internet e0@==>|":80"| loadbalancer
e0@{ animate: true }
e1@{ animate: true }
e2@{ animate: true }
classDef highlighted fill:#fcc Adicionando o Jaeger ao Docker Compose
O Jaeger é distribuído como um único container all-in-one que inclui o coletor, o mecanismo de consulta e a UI. Em produção você rodaria esses componentes separadamente, mas para o desenvolvimento local a imagem all-in-one é suficiente.
Adicione o seguinte serviço ao compose.yaml:
jaeger:
image: jaegertracing/all-in-one:1.57
container_name: jaeger
environment:
COLLECTOR_OTLP_ENABLED: "true"
ports:
- "16686:16686" # Jaeger UI
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP HTTP receiver
networks:
- store-net
Porta 4317 vs 4318
O agente Java do OTel usa por padrão gRPC na porta 4317. A porta 4318 é para HTTP/protobuf. As duas transportam o mesmo protocolo OTLP — use a 4317, a menos que um firewall obrigue o uso de HTTP.
Agente Java do OpenTelemetry
O agente Java do OpenTelemetry é um agente de instrumentação da JVM — um único JAR que você acopla com -javaagent. Ele usa a API de instrumentação de bytecode do Java para interceptar Spring MVC, Spring WebClient, RestTemplate, drivers JDBC e dezenas de outras bibliotecas sem nenhuma alteração de código.
Baixe o agente
Adicione uma etapa de download ao Dockerfile de cada serviço:
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v2.4.0/opentelemetry-javaagent.jar \
/app/opentelemetry-javaagent.jar
COPY target/*.jar app.jar
ENTRYPOINT ["java", \
"-javaagent:/app/opentelemetry-javaagent.jar", \
"-jar", "/app/app.jar"]
Fixe a versão do agente
A versão do agente OTel e a versão do Spring Boot precisam ser compatíveis. A v2.4.0 funciona com o Spring Boot 3.x. Consulte a matriz de compatibilidade antes de atualizar qualquer uma das duas dependências.
Configure cada serviço no Docker Compose
O agente é configurado inteiramente por variáveis de ambiente. Adicione o seguinte bloco environment a cada serviço no compose.yaml:
Por que OTEL_METRICS_EXPORTER: none?
Por padrão, o agente OTel também tenta exportar métricas via OTLP. Você já coleta métricas com Prometheus e Micrometer — exportá-las uma segunda vez para o Jaeger criaria duplicatas e desperdiçaria recursos. Definir none desativa o pipeline de métricas do OTel, mantendo a exportação de traces totalmente ativa.
Propagação de traces — como funciona
Quando o agente intercepta uma chamada HTTP de saída, ele injeta o cabeçalho W3C traceparent na requisição. O agente do serviço que a recebe lê esse cabeçalho e cria um span filho no mesmo trace.
O formato do traceparent é:
00-{traceId}-{parentSpanId}-{flags}
│ │ │ │
│ │ │ └── 01 = sampled, 00 = not sampled
│ │ └── 16-char parent span ID (hex)
│ └── 32-char trace ID (hex) — identical on every hop
└── version byte (always 00)
Exemplo de valor visto nos cabeçalhos HTTP:
O trace ID 4bf92f3577b34da6a3ce929d0e0e4736 está presente em todos os serviços da cadeia. Todos os spans que carregam esse ID são agrupados em um único trace no Jaeger.
sequenceDiagram
participant G as Agente do Gateway
participant O as Agente do Order
participant P as Agente do Product
G->>G: inicia o span raiz<br/>traceId=4bf9... spanId=aaa1
G->>O: POST /orders<br/>traceparent: 00-4bf9...-aaa1-01
O->>O: inicia span filho<br/>traceId=4bf9... spanId=bbb2 parent=aaa1
O->>P: GET /products/7<br/>traceparent: 00-4bf9...-bbb2-01
P->>P: inicia span filho<br/>traceId=4bf9... spanId=ccc3 parent=bbb2
P-->>O: 200
O-->>G: 200
G->>G: encerra o span raiz → exporta para o Jaeger
O->>O: encerra o span filho → exporta para o Jaeger
P->>P: encerra o span filho → exporta para o Jaeger Amostragem
O byte de flags 01 significa que o trace foi amostrado e será exportado. O sampler padrão exporta 100% dos traces. Em produção, troque por um sampler com limitação de taxa: defina OTEL_TRACES_SAMPLER=parentbased_traceidratio e OTEL_TRACES_SAMPLER_ARG=0.1 para amostrar 10% das requisições.
Correlacionando traces com logs
Um trace ID no Jaeger só é útil se você conseguir encontrar as linhas de log correspondentes. O agente OTel faz a ponte entre os dois mundos: ele injeta o trace ID e o span ID ativos no MDC do SLF4J, de modo que toda instrução de log emitida durante uma requisição rastreada carrega automaticamente o contexto do trace.
Adicione a dependência da ponte com o logback
No pom.xml de cada serviço:
<dependency>
<groupId>io.opentelemetry.instrumentation</groupId>
<artifactId>opentelemetry-logback-appender-1.0</artifactId>
<version>2.4.0-alpha</version>
<scope>runtime</scope>
</dependency>
Configure o logback-spring.xml
Crie src/main/resources/logback-spring.xml em cada serviço:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36}
[trace_id=%X{trace_id} span_id=%X{span_id}] - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
Nomes das chaves no MDC
O agente OTel grava o contexto do trace nas chaves trace_id e span_id do MDC do SLF4J. Elas seguem a convenção de nomes do OTLP/W3C e podem ser pesquisadas diretamente no Loki, no CloudWatch Insights, no Splunk e na maioria dos outros agregadores de logs.
Com essa configuração, cada linha de log durante uma requisição rastreada vai ficar assim:
14:23:01.442 [http-nio-8080-exec-3] INFO c.example.order.OrderService
[trace_id=4bf92f3577b34da6a3ce929d0e0e4736 span_id=00f067aa0ba902b7] - creating order for account 42
14:23:01.501 [http-nio-8080-exec-3] INFO c.example.order.OrderService
[trace_id=4bf92f3577b34da6a3ce929d0e0e4736 span_id=00f067aa0ba902b7] - calling ProductService for product id 7
Fluxo de trabalho: abra o trace lento no Jaeger → clique no span problemático → copie o valor de trace_id → cole-o na barra de busca do seu agregador de logs → todas as linhas de log daquela requisição exata aparecem juntas.
Visualizando traces na UI do Jaeger
Refaça o build e suba a stack completa:
Verifique se o Jaeger subiu:
Saída esperada:
NAME IMAGE STATUS PORTS
jaeger jaegertracing/all-in-one:1.57 running 0.0.0.0:4317->4317/tcp, 0.0.0.0:16686->16686/tcp
Gere algum tráfego para que haja traces para explorar:
curl -s -X POST http://localhost:8080/orders \
-H "Content-Type: application/json" \
-d '{"accountId": 1, "productId": 7, "quantity": 2}' | jq
Abra http://localhost:16686 no navegador.
Encontrando um trace
- No menu Service, selecione
gateway-service. - Deixe Operation como
alle clique em Find Traces. - Os resultados listam os traces recentes ordenados por timestamp. Cada linha mostra o nome da operação raiz, a duração total e o número de spans.
- Clique em qualquer trace para abrir a visualização em flame chart.
Lendo o flame chart
O flame chart desenha cada span como uma barra horizontal. O span raiz (gateway) fica no topo. Os spans filhos aparecem recuados abaixo do pai. A largura da barra é proporcional à duração — uma barra larga é uma operação lenta.
O que procurar
- Um span quase tão largo quanto o span raiz é o principal responsável pela latência.
- Spans rotulados
SELECToujdbc.querycom longa duração indicam queries lentas no banco de dados. - Spans rotulados
HTTP GETouHTTP POSTsão chamadas HTTP de saída capturadas pelo agente. - Um span com um ícone
!vermelho registrou uma exceção — clique nele para ver o stack trace completo anexado como um evento do span.
Atributos do span
Clicar em um span expande suas tags. O agente registra estas automaticamente:
| Tag | Valor de exemplo |
|---|---|
http.method | POST |
http.url | http://order:8080/orders |
http.status_code | 200 |
db.system | postgresql |
db.statement | SELECT * FROM products WHERE id = ? |
net.peer.name | db |
Adicionando spans personalizados
O agente Java instrumenta o código dos frameworks automaticamente, mas não sabe nada sobre a sua lógica de negócio. Quando uma validação, um cálculo de preço ou qualquer etapa de processamento relevante precisa ser rastreada individualmente, adicione um span manual.
Adicione a dependência da API do OpenTelemetry
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-api</artifactId>
</dependency>
O gerenciamento de dependências do Spring Boot inclui o opentelemetry-bom, então não é preciso informar a versão aqui.
Instrumente a lógica de negócio
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class OrderService {
@Autowired
private Tracer tracer;
public OrderOut create(OrderIn orderIn) {
Span span = tracer.spanBuilder("validateOrder").startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("order.account_id", orderIn.getAccountId());
span.setAttribute("order.product_id", orderIn.getProductId());
validateAccount(orderIn.getAccountId());
checkInventory(orderIn.getProductId(), orderIn.getQuantity());
} catch (Exception e) {
span.recordException(e);
throw e;
} finally {
span.end();
}
return persist(orderIn);
}
}
Sempre chame span.end() em um bloco finally
Se span.end() nunca for chamado, o span nunca é exportado para o Jaeger. O padrão try-finally garante a finalização mesmo quando uma exceção é lançada. O agente cuida disso automaticamente no código dos frameworks; os spans manuais são responsabilidade sua.
Quando adicionar spans manuais
Adicione spans manuais quando: (1) um método leva mais de ~50 ms e você quer medi-lo de forma independente, (2) você precisa anexar atributos de negócio (order.id, account.tier) que não são capturados pela instrumentação dos frameworks, ou (3) você precisa registrar uma exceção capturada como um evento do span. Não adicione spans manuais a getters triviais ou atribuições de campos — o ruído esconde os dados úteis.
Checklist
Antes de seguir em frente, verifique:
-
docker compose psmostra ojaegerrodando com as portas 16686 e 4317 expostas -
http://localhost:16686carrega a UI do Jaeger sem erros - Depois de enviar uma requisição,
gateway-serviceaparece no menu de serviços do Jaeger - Um trace de
POST /orderscontém pelo menos 3 spans: gateway, order-service, product-service - Cada span tem as tags
http.methodehttp.status_codedefinidas automaticamente pelo agente - A saída de log inclui campos
trace_id=que correspondem ao trace ID mostrado no Jaeger