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 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; para fluxos de IA, veja também Tracy e observabilidade de LLMs.

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 de fintechs e marketplaces que pedem “observabilidade”, “tracing distribuído” ou “OpenTelemetry”.

AbordagemQuando funcionaLimite
Só logsDebug local, monólito pequenoCorrelação fraca entre serviços
Só Micrometer/ActuatorMétricas de JVM e HTTP SpringPouco contexto de negócio cross-service
APM proprietárioTime único, budget fixoVendor lock-in e custo por host
OpenTelemetry + OTLPMulti-serviço, multi-stackExige 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

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):

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, rate limiting e monólito modular: você enxerga se a lentidão está no filtro de auth, no limiter ou no repositório.

Span manual de negócio

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

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, Authentication JWT e OpenAPI/Swagger, o próximo passo de maturidade é telemetria.

Plugin simples de span por rota

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) 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.
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égiaUso típico
parentbased_traceidratio 5–20%APIs de alto QPS
Always on em stagingDebug de integração
Tail-based no CollectorGuardar 100% dos erros + amostra de sucessos
Rules por rota100% 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:

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, 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, salário backend e o roadmap júnior 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) 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 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; para o mercado, acompanhe empresas que usam Kotlin e as vagas. Observabilidade boa não é gráfico bonito — é dormir melhor quando o p95 sobe e você sabe exatamente onde olhar.