OpenTelemetry: padrão unificado para trace, log e métrica

1. Visão geral

Quando um sistema se divide em múltiplos serviços, uma requisição pode passar por cinco, dez processos diferentes. Quando há um erro, você precisa saber por onde a requisição foi, em qual passo ela ficou lenta e quais logs pertencem àquela requisição. OpenTelemetry (OTel) é um padrão aberto que resolve exatamente isso. Ele unifica a forma de coletar traces, métricas e logs, depois os envia para qualquer backend.

Ao terminar de ler este artigo, você entenderá os conceitos essenciais do OTel e por que está recebendo tanta atenção. Você também saberá como integrar OTel em uma aplicação Spring Boot 3, executar um Collector, escrever testes para a parte de instrumentação e reconhecer quando não deve usá-lo.

2. O que é OpenTelemetry?

OpenTelemetry é um projeto de código aberto sob o CNCF. Ele define como criar, descrever e transmitir dados de telemetria. OTel não é um local de armazenamento nem uma interface de visualização. Essa parte fica a cargo de backends como Jaeger, Prometheus, Grafana Tempo/Loki ou serviços comerciais.

2.1. Três tipos de sinais

  • Trace: a jornada de uma requisição. Cada trace é composto de múltiplos spans, sendo cada span uma unidade de trabalho, como “chamar PaymentService” ou “consultar tabela orders”. Um span tem hora de início, hora de término e atributos.
  • Métrica: números agregados ao longo do tempo, como requisições por segundo, latência p99 ou número de pedidos falhados.
  • Log: registros de eventos discretos. OTel adiciona traceId e spanId ao log, permitindo que você pule de uma linha de log para o trace exato dela.

O maior valor está na correlação. Todos os três sinais compartilham um contexto comum e uma descrição comum de recursos, como service.name.

2.2. Componentes principais

Componente Função
API Interface para que o código crie spans e métricas, independente de vendor
SDK Implementação da API: sampling, batching, export
OTLP Protocolo de transmissão de dados (gRPC ou HTTP/protobuf)
Collector Processo independente que recebe, processa e encaminha telemetria
Semantic conventions Convenções de nomes para atributos como http.request.method, db.system…
Instrumentação Bibliotecas ou agent que geram automaticamente telemetria para frameworks populares

2.3. Context propagation

Para que spans em múltiplos serviços se juntem em um único trace, o contexto deve acompanhar a requisição. Com HTTP, OTel usa por padrão o padrão W3C Trace Context pelo header traceparent:

traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01

Esse header tem quatro partes: versão, traceId, spanId do span pai e flag de sampling. O serviço que recebe a requisição lê esse header e cria um span filho pertencendo ao mesmo trace.

3. Por que OpenTelemetry está recebendo atenção?

  • Neutral com vendor: você instrumenta o código uma vez. Se quiser trocar backend, só muda a configuração do exporter ou do Collector, sem alterar o código.
  • Unifica padrões antigos: OTel nasceu da fusão de OpenTracing e OpenCensus. A comunidade não precisa mais escolher entre dois caminhos.
  • Um padrão para os três sinais: anteriormente, cada tipo de dado tinha seu próprio agent e formato.
  • Ecossistema amplo: muitos frameworks, cloud providers e ferramentas de observability suportam OTLP. Spring Boot 3 também se integra via Micrometer.
  • Collector flexível: você filtra dados sensíveis, coleta amostras e roteia para múltiplos backends na camada de infraestrutura, sem tocar na aplicação.

4. Aplicando OpenTelemetry em Spring Boot 3

Spring Boot 3 usa Micrometer Observation como camada de abstração. Você escreve uma observation, Micrometer gera métrica (timer) e span. A ponte para OTel cuida da exportação.

4.1. Dependência

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- Ponte Micrometer Tracing -> OpenTelemetry -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-tracing-bridge-otel</artifactId>
    </dependency>
    <!-- Exportar trace via OTLP -->
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
    <!-- Exportar métrica via OTLP -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-registry-otlp</artifactId>
    </dependency>
    <!-- Para usar em testes -->
    <dependency>
        <groupId>io.micrometer</groupId>
        <artifactId>micrometer-observation-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Você não precisa especificar versões. Spring Boot gerencia as versões das bibliotecas Micrometer e OpenTelemetry através de um BOM.

4.2. Configuração

spring:
  application:
    name: order-service

management:
  tracing:
    sampling:
      probability: 1.0   # dev: amostrar 100%; produção deve ser reduzido
  otlp:
    tracing:
      endpoint: http://localhost:4318/v1/traces
    metrics:
      export:
        url: http://localhost:4318/v1/metrics
        step: 30s

logging:
  pattern:
    level: "%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]"

O significado de cada grupo de configuração:

  • spring.application.name é usado como service.name. Este é o atributo mais importante para o backend diferenciar os serviços.
  • management.tracing.sampling.probability define a proporção de traces que são registrados.
  • Os dois endpoints otlp apontam para o Collector via OTLP/HTTP, a porta padrão é 4318.
  • logging.pattern.level insere traceId e spanId do MDC em cada linha de log. Essa é a forma mais simples de correlacionar logs com traces.

4.3. Instrumentar regras de negócio usando a API Observation

Requisições HTTP que chegam ao controller e chamadas via RestClient/WebClient já são instrumentadas automaticamente pelo Spring. A parte que você precisa adicionar são os passos de negócio importantes:

public record Order(String id, String paymentMethod, long amount) {}
@FunctionalInterface
public interface PaymentService {
    String charge(Order order);
}
@Service
public class CheckoutService {

    private static final Logger log = LoggerFactory.getLogger(CheckoutService.class);

    private final ObservationRegistry registry;
    private final PaymentService paymentService;

    public CheckoutService(ObservationRegistry registry, PaymentService paymentService) {
        this.registry = registry;
        this.paymentService = paymentService;
    }

    public String checkout(Order order) {
        return Observation.createNotStarted("order.checkout", registry)
                .lowCardinalityKeyValue("payment.method", order.paymentMethod())
                .highCardinalityKeyValue("order.id", order.id())
                .observe(() -> {
                    log.info("Processando pedido {}", order.id());
                    return paymentService.charge(order);
                });
    }
}

Pontos importantes no código acima:

  • Observation.createNotStarted("order.checkout", registry) cria uma observation chamada order.checkout.
  • lowCardinalityKeyValue adiciona um atributo com poucos valores (CARD, CASH, WALLET). Esse atributo aparece tanto em métrica quanto em span.
  • highCardinalityKeyValue adiciona um atributo com muitos valores como ID do pedido. Ele apenas é anexado ao span, não inflaciona a métrica.
  • observe(...) abre o span, mede o tempo, registra exceção se houver e fecha o span quando o supplier retorna.

Ao executar, o log ficará parecido com:

 INFO [order-service,4bf92f3577b34da6a3ce929d0e0e4736,a1b2c3d4e5f60718] ... Processando pedido A-1001

No lado do backend você recebe dois dados:

  • Um span order.checkout que fica sob o span HTTP da requisição.
  • Uma métrica de timer order.checkout, dividida por payment.method. Dessa métrica você calcula número de chamadas, tempo total e tempo máximo.

Nota: Prefira a API Observation em vez de chamar diretamente io.opentelemetry.api.trace.Tracer no código Spring. Seu código não ficará dependente de uma biblioteca de tracing específica. Uma única medição também gera métrica e trace.

4.4. Alternativa: Java agent

Se não quiser modificar código nem adicionar dependências, você pode usar o OpenTelemetry Java agent. O agent instrumenta automaticamente muitas bibliotecas populares como JDBC, HTTP client e Kafka:

export OTEL_SERVICE_NAME=order-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
export OTEL_TRACES_SAMPLER=parentbased_traceidratio
export OTEL_TRACES_SAMPLER_ARG=0.1

java -javaagent:./opentelemetry-javaagent.jar -jar order-service.jar

O agent é adequado para aplicações legadas ou quando o time de operações quer centralizar o controle da instrumentação. O tradeoff é que você tem menos controle fino, o tempo de startup aumenta e o agent funciona como uma “caixa preta” interferindo no bytecode.

Nota: Não execute o Java agent e o bridge Micrometer simultaneamente para o mesmo tipo de sinal. Você receberá spans duplicados ou contexto corrompido.

5. Executando OpenTelemetry Collector

A aplicação deve enviar dados para o Collector em vez de enviá-los diretamente ao backend. O Collector é onde você faz batch, filtra e roteia dados. Abaixo está uma configuração mínima:

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch: {}
  attributes/strip-pii:
    actions:
      - key: customer.email
        action: delete

exporters:
  debug:
    verbosity: detailed

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [attributes/strip-pii, batch]
      exporters: [debug]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [debug]

Essa configuração tem quatro partes:

  • receivers.otlp recebe dados via gRPC (4317) e HTTP (4318).
  • attributes/strip-pii remove o atributo customer.email antes dos dados saírem da sua infraestrutura.
  • batch agrupa dados antes de enviar para reduzir o número de requisições.
  • O exporter debug imprime dados no console para você verificar. Em produção, você o substitui pelo exporter do seu backend. Versões antigas do Collector usavam logging como nome desse exporter.

Inicie o Collector com Docker:

docker run --rm -p 4317:4317 -p 4318:4318 \
  -v "$(pwd)/otel-collector.yaml:/etc/otelcol-contrib/config.yaml" \
  otel/opentelemetry-collector-contrib

Faça uma chamada de teste para a API de checkout e verifique o console do Collector. Você verá spans assim:

Span #0
    Trace ID       : 4bf92f3577b34da6a3ce929d0e0e4736
    Parent ID      : 9a8b7c6d5e4f3a2b
    Name           : order.checkout
    Attributes:
         -> payment.method: Str(CARD)
         -> order.id: Str(A-1001)

6. Testando a instrumentação

Instrumentação também é código, então também precisa ser testada. TestObservationRegistry permite que você verifique observations sem iniciar o contexto do Spring ou o Collector:

import io.micrometer.observation.tck.TestObservationRegistry;
import io.micrometer.observation.tck.TestObservationRegistryAssert;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

class CheckoutServiceTest {

    @Test
    void whenCheckout_thenObservationIsRecordedWithPaymentMethod() {
        TestObservationRegistry registry = TestObservationRegistry.create();
        PaymentService paymentService = order -> "PAID-" + order.id();
        CheckoutService service = new CheckoutService(registry, paymentService);

        String result = service.checkout(new Order("A-1001", "CARD", 250_000));

        assertThat(result).isEqualTo("PAID-A-1001");
        TestObservationRegistryAssert.assertThat(registry)
                .hasObservationWithNameEqualTo("order.checkout")
                .that()
                .hasLowCardinalityKeyValue("payment.method", "CARD")
                .hasHighCardinalityKeyValue("order.id", "A-1001")
                .hasBeenStarted()
                .hasBeenStopped();
    }
}

Esse teste confirma três coisas:

  • O resultado de negócio está correto.
  • A observation order.checkout foi criada com os atributos corretos.
  • A observation foi iniciada e encerrada. Um span que esquecer de encerrar é um erro muito difícil de detectar em produção.

7. Erros comuns

  • Explosão de cardinality: colocar userId, orderId ou URL com parâmetros em tags de métrica faz o número de séries de métricas aumentar exponencialmente e os custos de armazenamento disparam. Esses valores devem ir apenas em spans.
  • Amostragem 100% em produção: volume de dados e custos aumentam linearmente com o tráfego. Reduza probability ou use tail sampling no Collector para manter traces de erros ou traces lentos.
  • Perda de contexto ao executar de forma assíncrona: com @Async, CompletableFuture ou thread pools criados manualmente, o contexto não segue automaticamente para a nova thread. Você precisa envolver o executor com um mecanismo de propagação de contexto, como ContextExecutorService da biblioteca context-propagation, ou usar um executor gerenciado pelo Spring que já está pré-configurado.
  • Vazamento de dados sensíveis: emails, números de cartão ou tokens podem vazar em atributos ou logs. Filtre no código e adicione uma camada de filtragem no Collector.
  • Nomes arbitrários: não seguir as semantic conventions torna dashboards e alertas difíceis de reutilizar entre serviços.
  • Ignorar o Collector: enviar direto da aplicação para o backend significa que toda vez que trocar backend precisa fazer deploy de todos os serviços.

8. Quando NÃO usar (ou ainda não precisa)

  • Aplicação pequena, uma instância, poucas dependências: logs estruturados com algumas métricas do Actuator geralmente são suficientes. Distributed tracing quase não traz valor quando não há nada “distribuído”.
  • Time ainda sem backend e pessoa de operações: OTel é apenas um duto. Se ninguém for olhar os dados, você só gasta CPU, rede e dinheiro com armazenamento.
  • Sistema extremamente sensível a latência ou recursos: como aplicações embarcadas ou hot paths medidos em microsegundos. Meça o overhead antes de ativar instrumentação em larga escala.
  • Esperar que OTel substitua todas as ferramentas existentes de uma vez: o nível de maturidade de cada sinal e de cada linguagem pode variar. Verifique o status de suporte do que você pretende usar e faça a migração gradualmente, começando com tracing.

9. Conclusão

OpenTelemetry unifica como criamos e transmitimos traces, métricas e logs através de uma API comum, protocolo OTLP e Collector. Isso permite separar a instrumentação no código da escolha de backend.

Com Spring Boot 3, o caminho mais simples tem quatro passos. Primeiro, use Micrometer Observation para uma única medição gerar métrica e span. Segundo, exporte via OTLP para o Collector. Terceiro, coloque traceId nos logs. Quarto, escreva testes para as observations importantes. Paralelamente, controle cardinality, sampling e dados sensíveis desde o início.

Se seu sistema ainda é pequeno, não se apresse. Quando o número de serviços aumentar e a depuração começar a consumir horas, OTel é uma base que vale a pena investir.

opentelemetryobservabilityspring-boottracingtech-trends