---
title: "OpenTelemetry em Kotlin: traces, métricas e logs com Spring e Ktor"
url: "https://kotlin.dev.br/blog/opentelemetry-kotlin-spring-ktor-2026/"
markdown_url: "https://kotlin.dev.br/blog/opentelemetry-kotlin-spring-ktor-2026.MD"
description: "Guia prático de OpenTelemetry em Kotlin: traces, métricas, logs, Spring Boot, Ktor, export OTLP, sampling, correlação e erros comuns em produção no Brasil."
date: "2026-07-28"
author: "Karina Melo"
---

# OpenTelemetry em Kotlin: traces, métricas e logs com Spring e Ktor

Guia prático de OpenTelemetry em Kotlin: traces, métricas, logs, Spring Boot, Ktor, export OTLP, sampling, correlação e erros comuns em produção no Brasil.


**Resposta rápida:** use **OpenTelemetry (OTel)** em Kotlin para unificar **traces**, **métricas** e **logs** com o mesmo modelo de contexto, em vez de misturar bibliotecas proprietárias por stack. No backend JVM, o caminho mais estável em 2026 é: instrumentação automática (Java Agent) + instrumentação manual nos pontos de negócio + exportação **OTLP** para um collector (Grafana Tempo/Mimir, Jaeger, Datadog, New Relic, Elastic, AWS X-Ray via bridge, etc.). Em **Spring Boot**, o Agent cobre HTTP, JDBC, Redis e mensageria com pouco código; em **Ktor**, combine Agent com spans manuais nas rotas e plugins. Correlacione `trace_id` nos logs, defina sampling consciente e instrumente o que o negócio pergunta — não só o que a lib captura por padrão.

Colocar a API no ar sem enxergar latência por etapa, taxa de erro por cliente e dependências lentas é operar no escuro. O post clássico de [observabilidade em Kotlin](/blog/kotlin-observabilidade/) cobre os três pilares com logs estruturados e métricas genéricas; este guia foca no **padrão de mercado** que a maioria das vagas backend e de plataforma já cita: OpenTelemetry. Para resiliência de cliente HTTP, combine com [Ktor Client resiliente](/blog/ktor-client-resiliente-timeout-retry-circuit-breaker-2026/); para fluxos de IA, veja também [Tracy e observabilidade de LLMs](/blog/tracy-observabilidade-ia-kotlin-2026/).

## Por que OpenTelemetry e não “só Micrometer + log”?

Micrometer, SLF4J e um APM comercial ainda funcionam. O problema aparece quando o time cresce:

- Spring, Ktor, gRPC, Kafka e workers usam instrumentações diferentes;
- o APM A não conversa com o backend B do outro squad;
- você troca de vendor e reescreve anotações;
- mobile, backend e jobs não compartilham o mesmo `trace_id`.

OpenTelemetry separa **API/SDK de instrumentação** do **backend de telemetria**. Você instrumenta uma vez e exporta para o collector que o time de plataforma escolher. Em 2026 isso é o padrão de facto em job descriptions de backend Kotlin no Brasil — inclusive nas [vagas Kotlin](/vagas/) de fintechs e marketplaces que pedem “observabilidade”, “tracing distribuído” ou “OpenTelemetry”.

| Abordagem | Quando funciona | Limite |
|---|---|---|
| Só logs | Debug local, monólito pequeno | Correlação fraca entre serviços |
| Só Micrometer/Actuator | Métricas de JVM e HTTP Spring | Pouco contexto de negócio cross-service |
| APM proprietário | Time único, budget fixo | Vendor lock-in e custo por host |
| **OpenTelemetry + OTLP** | Multi-serviço, multi-stack | Exige collector e disciplina de atributos |

## Conceitos que você precisa dominar

### Trace, span e contexto

Um **trace** representa uma requisição de ponta a ponta. Cada **span** é uma unidade de trabalho (handler HTTP, query SQL, publish Kafka, chamada a outro serviço). Spans formam uma árvore via `parent_span_id`.

O **contexto** carrega `trace_id` e `span_id` entre threads e processos. Em Kotlin, coroutines complicam isso: se você não propagar o contexto, o span “some” no meio de um `withContext(Dispatchers.IO)`.

### Métricas e logs no mesmo ecossistema

- **Métricas**: contadores, histogramas e gauges (latência HTTP, tamanho de fila, erros por tipo).
- **Logs**: eventos discretos; o valor sobe quando cada linha carrega `trace_id`.
- **Traces**: o “porquê” de uma latência alta.

Sem correlação, você tem três silos. Com OTel bem configurado, clica no span e chega no log da mesma requisição.

### Semantic conventions

Atributos padronizados evitam caos:

- `http.request.method`, `http.route`, `http.response.status_code`
- `db.system`, `db.statement` (com cuidado de PII)
- `messaging.system`, `messaging.destination.name`
- `service.name`, `service.version`, `deployment.environment`

Padronize `service.name` (`pedido-api`, `checkout-worker`) e ambiente (`prod`, `staging`). Sem isso, o dashboard vira sopa de nomes.

## Arquitetura de referência

```text
App Kotlin (Spring ou Ktor)
   │  traces / metrics / logs (OTLP)
   ▼
OpenTelemetry Collector
   │
   ├── Tempo / Jaeger / X-Ray   (traces)
   ├── Prometheus / Mimir       (métricas)
   └── Loki / Elastic / Cloud   (logs)
```

O **Collector** é o ponto de controle: sampling tail-based, filtros de PII, batch e roteamento. Não aponte o SDK de cada pod direto para cinco vendors sem necessidade — o collector reduz acoplamento e custo de rede.

## Dependências e Agent (caminho recomendado)

Para a maior parte dos backends JVM em Kotlin, comece com o **OpenTelemetry Java Agent**. Ele instrumenta servlet/WebFlux, JDBC, Lettuce, Kafka clients, gRPC e HTTP clients sem reescrever o domínio.

Exemplo de start (container ou systemd):

```bash
java \
  -javaagent:/otel/opentelemetry-javaagent.jar \
  -Dotel.service.name=pedido-api \
  -Dotel.resource.attributes=deployment.environment=prod,service.version=1.14.2 \
  -Dotel.exporter.otlp.endpoint=http://otel-collector:4318 \
  -Dotel.metrics.exporter=otlp \
  -Dotel.logs.exporter=otlp \
  -Dotel.traces.sampler=parentbased_traceidratio \
  -Dotel.traces.sampler.arg=0.15 \
  -jar pedido-api.jar
```

**Por que Agent primeiro?** Porque cobre 70–90% do caminho feliz. Instrumentação manual entra depois, nos spans de negócio: `pedido.checkout`, `pagamento.autorizar`, `antifraude.score`.

Se você precisa de SDK embutido (FaaS, constraints de classloader), use as dependências Maven/Gradle do OpenTelemetry Java e inicialize o `OpenTelemetrySdk` no bootstrap — mas aceite mais código operacional.

## Spring Boot com Kotlin

### O que o Agent já resolve

Em um serviço Spring Boot típico (Web MVC ou WebFlux + JDBC + Redis):

- cada request HTTP vira span de servidor;
- calls `RestClient`/`WebClient` viram spans de cliente;
- queries JDBC aparecem com `db.system`;
- Redis e Kafka entram quando as instrumentações estão ativas.

Isso combina bem com [Spring Security + JWT](/blog/spring-security-kotlin-jwt-oauth2-2026/), [rate limiting](/blog/rate-limiting-kotlin-spring-ktor-2026/) e [monólito modular](/blog/monolito-modular-kotlin-spring-2026/): você enxerga se a lentidão está no filtro de auth, no limiter ou no repositório.

### Span manual de negócio

```kotlin
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.api.trace.Tracer

class CheckoutService(
    private val tracer: Tracer = GlobalOpenTelemetry.getTracer("pedido-api"),
    private val pagamentos: PagamentoClient,
    private val pedidos: PedidoRepository,
) {
    suspend fun finalizar(pedidoId: String, clienteId: String): CheckoutResult {
        val span = tracer.spanBuilder("checkout.finalizar")
            .setAttribute("pedido.id", pedidoId)
            .setAttribute("cliente.id", clienteId)
            .startSpan()

        return try {
            span.makeCurrent().use {
                val pedido = pedidos.buscar(pedidoId)
                span.addEvent("pedido.carregado")

                val autorizacao = pagamentos.autorizar(pedido)
                span.setAttribute("pagamento.status", autorizacao.status)

                if (!autorizacao.aprovado) {
                    span.setStatus(StatusCode.ERROR, "pagamento_recusado")
                    return CheckoutResult.Recusado(autorizacao.motivo)
                }

                pedidos.marcarPago(pedidoId)
                span.setStatus(StatusCode.OK)
                CheckoutResult.Aprovado(pedidoId)
            }
        } catch (ex: Exception) {
            span.recordException(ex)
            span.setStatus(StatusCode.ERROR, ex.message ?: "erro_checkout")
            throw ex
        } finally {
            span.end()
        }
    }
}
```

### Métricas de negócio com API OTel

```kotlin
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.common.AttributeKey
import io.opentelemetry.api.common.Attributes

class CheckoutMetrics {
    private val meter = GlobalOpenTelemetry.getMeter("pedido-api")
    private val checkouts = meter.counterBuilder("checkout_total")
        .setDescription("Total de checkouts por resultado")
        .build()

    fun registrar(resultado: String) {
        checkouts.add(
            1,
            Attributes.of(AttributeKey.stringKey("resultado"), resultado),
        )
    }
}
```

Histogramas de latência de pagamento e contadores de recusa por motivo respondem perguntas de produto — não só “CPU a 70%”.

### Logs com trace_id

No Logback/Log4j2, inclua o MDC preenchido pelo Agent (`trace_id`, `span_id`) no encoder JSON. Em produção brasileira com LGPD, **não** logue CPF, cartão ou token completo. Correlacione identificadores internos opacos.

## Ktor com Kotlin

Ktor é enxuto; por isso o Agent + spans manuais brilha. Se você já monta APIs com o [guia de Ktor](/guias/guia-kotlin-backend-ktor/), [Authentication JWT](/blog/ktor-authentication-jwt-kotlin-2026/) e [OpenAPI/Swagger](/tutoriais/kotlin-ktor-openapi-swagger/), o próximo passo de maturidade é telemetria.

### Plugin simples de span por rota

```kotlin
import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.opentelemetry.api.GlobalOpenTelemetry
import io.opentelemetry.api.trace.StatusCode
import io.opentelemetry.api.trace.SpanKind

fun Application.configureTelemetry() {
    val tracer = GlobalOpenTelemetry.getTracer("checkout-ktor")

    intercept(ApplicationCallPipeline.Monitoring) {
        val route = call.request.local.uri
        val span = tracer.spanBuilder("HTTP ${call.request.local.method.value}")
            .setSpanKind(SpanKind.SERVER)
            .setAttribute("http.route", route)
            .startSpan()

        try {
            span.makeCurrent().use {
                proceed()
                span.setAttribute(
                    "http.response.status_code",
                    call.response.status()?.value ?: 0,
                )
            }
        } catch (ex: Exception) {
            span.recordException(ex)
            span.setStatus(StatusCode.ERROR)
            throw ex
        } finally {
            span.end()
        }
    }
}
```

Na prática, prefira combinar Agent (que já entende Netty/CIO em muitos setups) com **spans de domínio** dentro dos handlers — o interceptor acima é didático; o Agent evita reinventar a roda de propagação W3C Trace Context.

### Cliente Ktor e propagação

Quando o serviço A chama o serviço B, o header `traceparent` precisa atravessar. O Agent costuma instrumentar o engine HTTP; se você usa client customizado, valide com um trace real em staging. Timeouts, retries e circuit breaker (veja o guia de [cliente resiliente](/blog/ktor-client-resiliente-timeout-retry-circuit-breaker-2026/)) devem aparecer como eventos ou spans filhos — senão você vê só “500 no gateway” sem saber quantas tentativas ocorreram.

## Coroutines: o detalhe que quebra traces

Kotlin multiplica threads via `Dispatchers`. Regras práticas:

1. Prefira APIs OTel que respeitam o contexto atual (`makeCurrent` + `use`).
2. Evite capturar um `Span` e reutilizá-lo em outra coroutine sem contexto.
3. Em `async`/`await` paralelos, decida se cada branch é span filho ou evento.
4. Teste com carga: race em contexto é bug silencioso.

```kotlin
suspend fun carregarPainel(userId: String): Painel =
    coroutineScope {
        val tracer = GlobalOpenTelemetry.getTracer("app")
        val root = tracer.spanBuilder("painel.carregar").startSpan()
        try {
            root.makeCurrent().use {
                val perfil = async { perfilRepo.get(userId) }
                val pedidos = async { pedidoRepo.recentes(userId) }
                Painel(perfil.await(), pedidos.await())
            }
        } finally {
            root.end()
        }
    }
```

Se `perfilRepo` e `pedidoRepo` tiverem instrumentação JDBC do Agent, você verá dois spans filhos sob `painel.carregar` — desde que o contexto tenha sido propagado.

## Sampling sem surpresa na fatura

Exportar 100% dos traces em produção costuma ser caro e ruidoso.

| Estratégia | Uso típico |
|---|---|
| `parentbased_traceidratio` 5–20% | APIs de alto QPS |
| Always on em staging | Debug de integração |
| Tail-based no Collector | Guardar 100% dos erros + amostra de sucessos |
| Rules por rota | 100% em `/checkout`, 1% em `/health` |

Nunca faça sampling “no escuro” em rotas de pagamento sem combinar com o time de risco. Erros devem ser preservados com prioridade.

## O que instrumentar de propósito (checklist de produto)

Além do automático, marque:

- início e fim de saga/checkout;
- decisão de antifraude (resultado, não dados sensíveis);
- publicação e consumo de mensagens ([Kafka/RabbitMQ](/blog/kotlin-kafka-rabbitmq-mensageria-2026/));
- hits/misses de [cache Redis](/blog/kotlin-redis-cache-sessoes-2026/);
- chamadas gRPC entre serviços ([gRPC + coroutines](/blog/grpc-kotlin-coroutines-tutorial-2026/));
- jobs e outbox;
- feature flags relevantes ([feature flags Android/backend](/blog/feature-flags-kotlin-android-backend-2026/)).

Pergunta norteadora: **“se isso falhar às 3h da manhã, o on-call precisa ver no trace?”**

## Testes e qualidade da telemetria

Telemetria sem teste vira mentira confiável.

- Use o **OTLP mock** ou o Collector em docker-compose no CI.
- Em testes de integração com [Testcontainers](/blog/testcontainers-kotlin-postgresql-testes-integracao-2026/), valide que uma request gera pelo menos um span de servidor e um de DB.
- Assert de atributos críticos (`http.route`, `service.name`) evita regressão quando alguém renomeia path.
- Não faça assert frágil de duração — foque em topologia e status.

## Erros comuns (e caros)

### `service.name` genérico (`unknown_service`)

Quebra dashboards e alertas. Configure no Agent e no resource detector.

### Cardinalidade explosiva em métricas

Colocar `user_id` ou `pedido_id` como label de métrica derruba Prometheus. IDs vão para **atributos de span**, não para labels de série temporal.

### PII em atributos e statements SQL

Logar `WHERE cpf = ...` completo viola LGPD e política interna. Use hash, últimos dígitos ou ID interno. O mesmo vale para headers `Authorization`.

### Ignorar health/readiness no sampling errado

`/health` a cada 5s com trace 100% polui backend. Exclua ou sample perto de zero.

### Instrumentar só o monólito e esquecer workers

Filas e cron jobs são onde o dinheiro some em silêncio. Traces de consumer precisam do mesmo `service.version`.

### Misturar dois agents APM + OTel sem plano

Classloader e double-instrumentation geram spans duplicados e overhead. Escolha um caminho de auto-instrumentação por processo.

### Tratar OTel como “projeto de plataforma só”

Sem spans de negócio, o time de produto continua cego. Plataforma entrega o tubo; o domínio preenche o significado.

## Como isso aparece em carreira e vagas

Em processos seletivos de backend Kotlin pleno/sênior, é comum pedir:

- explicar diferença entre métrica, log e trace;
- desenhar propagação entre API e worker;
- citar OTLP e Collector;
- discutir sampling e custo;
- correlacionar incidente real (latência p95, erro em dependência).

Se você está montando portfólio, um serviço Ktor ou Spring com Agent, um `docker-compose` de Collector + Jaeger e um README em português já diferencia. Combine com o [roadmap backend Kotlin](/carreira/roadmap-dev-backend-kotlin/), [salário backend](/carreira/salario-dev-backend-kotlin/) e o [roadmap júnior](/carreira/kotlin-junior-roadmap-primeira-vaga-2026/) se ainda está na primeira vaga — observabilidade costuma ser diferencial, não requisito júnior puro.

## Perguntas frequentes

### OpenTelemetry substitui Prometheus e Grafana?

Não. OTel **gera e transporta** telemetria; Prometheus/Grafana (ou equivalentes) **armazenam e visualizam**. Você exporta métricas OTLP ou faz bridge para o formato que o time já usa.

### Preciso reescrever a app para adotar OTel?

Na maioria dos casos, não. O Java Agent cobre o esqueleto. Reescrita só entra se houver custom frameworks sem instrumentação ou se o time quiser spans de domínio ricos.

### Spring Boot 3 e Kotlin precisam de lib especial?

O Agent e as instrumentações JVM oficiais funcionam com Kotlin da mesma forma que com Java. O cuidado extra é **coroutines e contexto**, não a linguagem em si.

### Ktor tem suporte de primeira classe?

O ecossistema JVM do OTel cobre boa parte dos engines; ainda assim, valide spans de servidor/cliente no seu engine (CIO/Netty/Jetty) em staging. Spans manuais de domínio continuam recomendados.

### Como correlacionar mobile Android com backend?

Propague W3C Trace Context (ou B3, se legado) nas chamadas HTTP do app. No Android, instrumentação OTel mobile ainda exige disciplina maior que no backend JVM; comece correlacionando `X-Request-Id`/`traceparent` e logs de crash ([Firebase Crashlytics](/blog/firebase-crashlytics-anr-android-kotlin-2026/)) com o span do servidor.

### OTel serve para agentes de IA em Kotlin?

Serve como base de tracing genérico. Para fluxos de LLM/tools, combine com abordagens específicas como o [Tracy](/blog/tracy-observabilidade-ia-kotlin-2026/) quando o time precisar de spans de prompt/tool com semântica de IA.

### Quanto de sampling usar no começo?

Em staging, 100%. Em produção de API média, 10–20% com tail-based para erros é um ponto de partida razoável. Ajuste com custo do backend de traces e QPS real.

## Checklist de produção

- [ ] `service.name` e `service.version` corretos em todos os deploys;
- [ ] exporter OTLP apontando para o Collector (não para cinco vendors);
- [ ] traces + métricas + logs habilitados de forma coerente;
- [ ] sampling documentado (ratio + exceções de rota);
- [ ] `trace_id` nos logs JSON;
- [ ] spans de negócio nos fluxos de dinheiro e autenticação;
- [ ] PII removida de atributos e statements;
- [ ] dashboards de p95/p99 por rota crítica;
- [ ] alerta de taxa de erro e saturação de dependência;
- [ ] workers e consumers com a mesma padrão de telemetria;
- [ ] teste de fumaça pós-deploy que gera um trace verificável;
- [ ] runbook de on-call com “como achar o trace deste pedido”.

## Conclusão

OpenTelemetry em Kotlin não é moda de conference talk: é a forma mais portável de ligar **Spring**, **Ktor**, mensageria e workers no mesmo mapa de execução. Comece pelo Java Agent e OTLP, padronize `service.name`, correlacione logs e só então invista em spans de domínio. Evite cardinalidade tóxica, preserve privacidade e meça o que o negócio sente — checkout lento, pagamento recusado, fila atrasada.

Se a sua stack é Spring, aprofunde segurança e modularização nos guias já citados; se é Ktor, feche o ciclo com JWT, client resiliente e OpenAPI. Para o panorama geral dos pilares, releia [observabilidade em Kotlin](/blog/kotlin-observabilidade/); para o mercado, acompanhe [empresas que usam Kotlin](/empresas/) e as [vagas](/vagas/). Observabilidade boa não é gráfico bonito — é dormir melhor quando o p95 sobe e você sabe exatamente onde olhar.
