Barte/ dev

Plataforma

Pavimentando a observabilidade: do Grafana ao Datadog

Como centralizamos logs, métricas, traces e alertas no Datadog usando OpenTelemetry, sidecar no ECS, retenção em camadas e monitores como código.

por Lucas Leitão··13 min de leitura

Todo incidente tem dois problemas: a causa raiz e o caminho até entendê-la. Durante muito tempo, o segundo era desproporcionalmente caro aqui na Barte. Não porque faltasse ferramenta, mas porque tínhamos ferramentas demais, cada uma cobrindo uma parte do cenário.

Quatro ferramentas, um incidente

Nossa primeira solução completa de observabilidade era uma composição: Grafana self-hosted como camada de dashboards, puxando de várias fontes (métricas e logs do CloudWatch, banco de dados, métricas customizadas); New Relic para APM (o monitoramento de performance da aplicação, requisição a requisição) e health checks sintéticos; CloudWatch como destino dos logs de todos os serviços no ECS; e o Opsgenie como intermediário dos alertas, nascidos no Grafana ou no New Relic, antes de chegarem ao Slack.

Cada peça dessas foi uma escolha razoável no momento em que entrou. O problema não era nenhuma ferramenta isolada, era a composição. Investigar um incidente significava abrir o dashboard no Grafana, pular para o APM no New Relic e buscar logs no console do CloudWatch, fazendo a correlação entre as três visões manualmente. O alerta que disparava tudo isso chegava ao Slack depois de duas etapas e com pouco contexto: dizia que algo estava errado, mas raramente indicava por onde começar. Manter tudo isso de pé também tinha preço: o Grafana self-hosted era mais um sistema nosso para operar, com infraestrutura, atualização e controle de acesso por nossa conta.

Havia ainda um custo mais silencioso: cada serviço novo repetia as mesmas decisões. Como logar? O que medir? Em que threshold alertar? Para qual canal? Cada time respondia do seu jeito, e as respostas divergiam com o tempo.

Observabilidade também precisa de paved road

Se você leu o post sobre paved road, já reconheceu o padrão: energia gasta em decisões repetidas é energia que não vira produto. Monitoramento era exatamente um desses casos.

Foi assim que encaramos a migração para o Datadog: não como uma troca de ferramenta, mas como a construção de um paved road de observabilidade. Centralizar logs, métricas, traces (o rastro que uma requisição deixa ao atravessar os serviços) e alertas em um só lugar era a metade visível do trabalho. A outra metade era transformar as boas práticas de observabilidade no caminho mais fácil, para que o jeito certo de monitorar um serviço fosse também o jeito mais rápido.

Vale descrever o contexto em que essa estrada seria construída. Nossa stack core é Kotlin com Spring Boot, rodando em containers no ECS Fargate, na AWS. A comunicação assíncrona entre serviços passa por SQS e SNS, a infraestrutura é declarada em Terraform e o CI/CD roda no GitHub Actions. É nesse cenário que a telemetria nasce, e foi pela instrumentação que a pavimentação começou.

Trocar o backend, não a instrumentação

A migração de traces poderia ter sido a parte mais cara. Reinstrumentar vários serviços é o tipo de projeto que atravessa trimestres. No nosso caso não foi, por uma decisão tomada bem antes: os serviços já eram instrumentados com o agente OpenTelemetry, exportando para um backend de tracing que rodava internamente (o New Relic ainda cobria o APM de parte dos serviços, mas o caminho novo já era OTel).

Como o OpenTelemetry é um padrão aberto, migrar significou apontar o mesmo agente para outro destino. O -javaagent continuou o mesmo; mudou apenas o endereço de entrega:

environment = [
# o mesmo agente OTel de antes, mudou apenas o destino
{ name = "OTEL_EXPORTER_OTLP_ENDPOINT", value = "http://localhost:4318" },
{ name = "OTEL_EXPORTER_OTLP_PROTOCOL", value = "http/protobuf" },
{ name = "OTEL_SERVICE_NAME", value = "meu-servico" },
]

O localhost:4318 é o detalhe central: quem recebe os traces agora é um sidecar do Datadog Agent rodando na mesma task do ECS, com o receiver OTLP (o protocolo de exportação do OpenTelemetry) habilitado. A aplicação não sabe que o backend mudou. Se um dia quisermos trocar de novo, o custo volta a ser uma variável de ambiente.

Essa instrumentação traz outro benefício que sustenta todo o resto: a propagação de contexto. O agente injeta e lê os headers de trace nas chamadas entre serviços, então o mesmo trace_id acompanha a requisição de ponta a ponta. Um fluxo que nasce em um serviço, passa por uma fila e termina em outro aparece como um único trace distribuído, e os logs de todos os serviços envolvidos carregam o mesmo trace_id. A correlação que antes era feita manualmente, ferramenta a ferramenta, virou um clique.

A lição que levamos: instrumentação baseada em padrão aberto é liberdade comprada antecipadamente. O custo é pequeno no início, e o valor aparece justamente no dia em que uma migração dessas se torna necessária.

O antes e o depois na task definition

Na prática, a virada aconteceu na task definition do ECS (o arquivo que declara quais containers rodam juntos para um serviço). A antiga tinha um container: a aplicação, com o log driver awslogs enviando tudo para o CloudWatch.

flowchart LR
  A1[app] -- awslogs --> CW1[CloudWatch Logs]
  A1 -- OTLP --> T[Backend de tracing interno]

A nova tem três containers:

flowchart LR
  A2[app] -- OTLP localhost:4318 --> DA[datadog-agent]
  A2 -- stdout JSON --> FB[log_router / Fluent Bit]
  FB -- FireLens --> DD[Datadog Logs]
  FB -- cópia --> CW2[CloudWatch Logs]
  DA -- traces + métricas --> DD2[Datadog]

1. A aplicação, agora enviando logs para o Datadog via FireLens (o mecanismo do ECS para rotear logs para destinos fora do CloudWatch):

logConfiguration = {
logDriver = "awsfirelens"
options = {
Name = "datadog"
Host = "http-intake.logs.datadoghq.com" # ajuste para o site da sua conta
dd_service = "meu-servico"
dd_source = "java"
dd_tags = "env:prod,team:minha-equipe"
TLS = "on"
provider = "ecs"
}
secretOptions = [
{ name = "apikey", valueFrom = "<arn-do-secret-no-secrets-manager>" }
]
}

2. O sidecar datadog-agent, que recebe traces e métricas OTLP da aplicação e cuida do APM:

{
name = "datadog-agent"
image = "public.ecr.aws/datadog/agent:7.76.1"
essential = false
environment = [
{ name = "ECS_FARGATE", value = "true" },
{ name = "DD_SITE", value = "datadoghq.com" }, # idem: site da sua conta
{ name = "DD_APM_ENABLED", value = "true" },
{ name = "DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT", value = "0.0.0.0:4318" },
{ name = "DD_ENV", value = "prod" },
{ name = "DD_SERVICE", value = "meu-servico" },
# descarta o trace inteiro quando o span raiz casa com o padrão (health checks)
{ name = "DD_APM_IGNORE_RESOURCES", value = "GET /actuator.*" },
]
secrets = [
{ name = "DD_API_KEY", valueFrom = "<arn-do-secret-no-secrets-manager>" }
]
}

3. O log_router, um Fluent Bit (coletor leve de logs) gerenciado pelo FireLens: um container com firelensConfiguration = { type = "fluentbit" } e a imagem aws-for-fluent-bit, que roteia o stdout da aplicação. Ele existe porque, no Fargate, o Datadog Agent não enxerga o stdout dos outros containers; log em Fargate sai via FireLens ou direto pela API. Nossos logs já saíam em JSON estruturado, o que simplificou a chegada ao Datadog: cada campo do log vira um filtro de busca (as facetas), e como toda linha já carrega o trace_id, o Datadog liga log e trace automaticamente (o remapeamento padrão reconhece o formato do OTel).

Tudo isso é Terraform no próprio repositório do serviço, com a API key do Datadog vinda do Secrets Manager, sem credencial em texto plano na task definition. O pipeline de deploy (GitHub Actions) atualiza apenas a imagem: o Terraform é dono da estrutura da task, e o deploy é dono da versão que roda.

Um destino para investigar, outro para armazenar

O diagrama acima tem um detalhe que costuma gerar dúvida: os logs vão para o Datadog e continuam indo para o CloudWatch. Não é indecisão, é uma arquitetura de retenção em camadas.

Logs têm dois consumidores com necessidades opostas. Quem investiga um incidente quer busca rápida, facetas e correlação, e quase sempre olha para os últimos dias. Quem audita (e uma fintech convive com auditoria) precisa de anos de histórico, consultados raramente. Indexar anos de log em uma ferramenta de investigação custa caro; investigar um incidente em armazenamento frio é lento demais.

A divisão ficou assim: o Datadog indexa a janela quente de 15 dias, onde acontece a investigação; o CloudWatch guarda a cauda longa de 5 anos, a um custo muito menor do que seria indexá-la. O Fluent Bit resolve isso com um arquivo de configuração adicional (um [OUTPUT] extra apontando para o CloudWatch, que a imagem init do aws-for-fluent-bit carrega do S3). A mesma linha de log sai uma única vez da aplicação e chega aos dois destinos, cada um cumprindo um papel diferente.

Monitores como código

Centralizar a telemetria resolvia só parte do problema. Faltava estruturar o que fazer com ela, e aqui entrou a peça mais importante da pavimentação: um módulo Terraform interno de observabilidade.

Em vez de cada time criar monitores manualmente na interface, o serviço declara o que tem, e o módulo gera a suíte de monitores correspondente:

module "observability" {
source = "git::https://github.com/<org>/terraform-modules//terraform-datadog-observability?ref=<versao>"
service_name = "meu-servico"
team = "minha-equipe"
notification_slack_channel = "@slack-alertas-do-time"
sqs_queues = [
{ name = "eventos-do-dominio", dlq_name = "eventos-do-dominio-dlq" },
]
log_monitors = [
{
name = "fluxo-escalado-para-manual"
headline = "processamento escalado para intervenção manual"
query_filter = "@flow:meu_fluxo MANUAL_INTERVENTION_REQUIRED"
reason = "o fluxo esgotou os retries e precisa de ação humana"
threshold = 0
window = "30m"
},
]
rds_instances = [{ identifier = "minha-instancia" }]
dependencies = [
{ name = "minha-dependencia-externa", host = "api.dependencia.example" },
]
}

Declarou uma fila SQS? O módulo cria monitor de DLQ (dead-letter queue, a fila para onde vão as mensagens que falharam) e de mensagem envelhecendo. Declarou uma dependência HTTP? Cria monitores de taxa de 4xx/5xx e de latência p95 (o tempo dentro do qual 95% das requisições respondem), a partir de métricas que o Datadog deriva automaticamente dos traces. Declarou um padrão de log crítico? O módulo gera um log alert, e o campo reason entra na mensagem do alerta como um mini guia de ação (runbook).

O módulo carrega as convenções que antes eram decididas caso a caso: nomenclatura ([env][serviço] recurso condição), severidade mapeada em prioridade, tags padronizadas (service, team, sli) e canal de notificação. E trouxe um efeito colateral valioso: alerta agora passa por code review. Criar um monitor é abrir um PR, com histórico, discussão e rollback, como qualquer outra mudança de produção.

É o mesmo movimento que descrevemos em princípios de engenharia: decidir uma vez, na convenção, o que antes se decidia toda semana, em cada time.

O caminho do alerta encurtou

No desenho antigo, um alerta nascia no Grafana, virava ocorrência no Opsgenie e só então aparecia no Slack. Duas etapas, pouco contexto.

Hoje o monitor notifica o Slack diretamente, e a mensagem chega pronta para a ação: o que disparou, em qual serviço e ambiente, por que isso importa (o reason declarado no módulo) e um link para a query já filtrada na janela do problema. A pergunta no canal deixou de ser “alguém sabe o que é isso?” e passou a ser “quem pega?”.

O que estava oculto apareceu

Talvez a mudança mais sutil tenha sido de postura. No desenho antigo, a atuação em incidentes era essencialmente reativa: a gente enxergava o que alguém tinha construído dashboard ou alerta para enxergar, e o resto só aparecia quando virava incidente.

Aqui entra algo que vale destacar: boa parte da visibilidade nova não exigiu construção nenhuma. Com os serviços instrumentados e a integração com a AWS ligada, o Datadog gera sozinho um conjunto de visões que antes dependiam de alguém criar dashboard: o mapa de dependências entre serviços, latência e taxa de erro por endpoint e por integração externa (derivadas dos próprios traces), o agrupamento de exceções por padrão com o volume de cada uma, dashboards prontos para RDS, SQS e Lambda. Tudo isso vem de graça com a instrumentação, e para todos os serviços de uma vez, o time tendo pedido ou não.

Foi por essas visões que problemas ocultos vieram à superfície: erros recorrentes que ninguém acompanhava, filas acumulando em horários específicos, integrações degradando aos poucos. O que antes era ruído disperso virou uma lista priorizável.

Essa visibilidade também expôs uma dívida nossa: a classificação dos logs. Mensagens marcadas como erro que não pedem ação nenhuma, avisos importantes escondidos em nível de info, exceções genéricas demais para agrupar bem. Estamos com um trabalho em andamento de reavaliar níveis de log e classificação de erros, e ele ficou muito mais fácil agora que dá para ver, por padrão e por volume, o que cada linha realmente representa.

O que ficou para trás, de propósito

Nenhuma migração termina 100%, e vale registrar as pendências:

  • Uma métrica customizada ainda é emitida pelos dois caminhos. O antigo, via CloudWatch, e o novo, via OpenTelemetry, coexistem até a última consulta migrar.
  • Um serviço já instrumentado ainda não declarou seus monitores no módulo. O paved road está pronto, mas nem todo mundo terminou de entrar nele.

Saber exatamente o que ficou para trás, e por quê, vale mais do que uma migração aparentemente perfeita.

A telemetria virou contexto para IA

Um efeito da centralização que não estava no plano original: ela preparou o terreno para a IA. Com logs, métricas, traces e monitores em um lugar só, com tags e convenções padronizadas, a telemetria ficou consumível não só por pessoas, mas também por agentes.

Hoje usamos bastante o MCP do Datadog: agentes de IA investigam um alerta consultando logs, traces e métricas diretamente, seguindo a mesma trilha que um engenheiro seguiria. E o acesso deixou de ser exclusividade da engenharia: pessoas de outras áreas conseguem fazer perguntas sobre o comportamento do sistema em linguagem natural, sem precisar aprender a linguagem de consulta de cada ferramenta.

Já tínhamos escrito no post sobre paved road que padrões e guardrails são a base que torna seguro automatizar com IA. A observabilidade centralizada é mais um exemplo disso: a IA só consegue investigar bem porque o caminho está pavimentado.

O que mudou no dia a dia

No fim, o valor da migração se mede na rotina:

  • Investigação em um lugar só. Do alerta ao log, do log ao trace, do trace à métrica, sem trocar de ferramenta e sem correlacionar nada de cabeça.
  • O mesmo trace_id de ponta a ponta. Um fluxo que atravessa vários serviços é um único trace, e os logs de todos os serviços se encontram por esse identificador.
  • Alerta que chega pronto para a ação, com contexto e link para a janela exata do problema, direto no Slack.
  • Serviço novo já nasce monitorado. Declarar filas, dependências e padrões de log no módulo é parte do setup, e os monitores passam por review como qualquer código.
  • Telemetria acessível além da engenharia, por pessoas e por agentes de IA, via MCP.
  • Ninguém mais mantém ferramenta de observabilidade de pé. O Grafana self-hosted era um sistema nosso para operar; o Datadog é gerenciado, e a energia que ia para a ferramenta agora vai para os monitores.

Detecção, porém, é só o começo de um incidente. Quem assume? Como a comunicação flui? O que vira aprendizado depois? Para esse capítulo, uma nova peça entrou no nosso ecossistema: o incident.io, integrado a tudo que descrevemos aqui. Este foi o primeiro estudo de caso da série que prometemos ao lançar o blog; como incidente virou processo, com dono, timeline e post-mortem, é assunto para o próximo post.