OpenTelemetry em Go é uma das formas mais práticas de preparar uma API para produção sem casar o código com uma ferramenta específica de observabilidade. Em vez de espalhar integrações diretas com Datadog, New Relic, Grafana, Jaeger, Tempo ou outro backend, a aplicação emite telemetria em um padrão aberto. Depois, um collector ou exportador decide para onde enviar traces, métricas e logs. Se o time mantém serviços em múltiplas linguagens, vale comparar com OpenTelemetry com Python para padronizar atributos, traces e collector entre stacks.
Isso importa porque aplicações Go costumam nascer pequenas e ficar críticas rápido. Um serviço começa como uma API interna, depois vira dependência de checkout, autenticação, pagamentos, filas, relatórios, webhooks ou processamento assíncrono. Quando surge lentidão, erro intermitente ou incidente pós-deploy, logs soltos raramente bastam. Você precisa responder onde a requisição passou, quanto tempo gastou em cada dependência, qual rota falhou, qual versão estava rodando e se o problema está no banco, na fila, em uma API externa ou no próprio handler.
Este guia mostra como usar OpenTelemetry em Go com foco em APIs HTTP: configuração de resource, traces, spans manuais, instrumentação de net/http, métricas essenciais, correlação com logs estruturados, OTLP em produção e cuidados para não vazar dados sensíveis. Se você ainda está montando a base do serviço, leia também API REST em Go, slog em Go, context.Context em Go e rate limiting em Go.
O que o OpenTelemetry resolve
OpenTelemetry é um conjunto de especificações, SDKs e ferramentas para gerar telemetria. Em Go, ele aparece principalmente em três sinais:
- Traces: contam o caminho de uma requisição entre handlers, funções, banco, filas e serviços externos.
- Métricas: agregam números como taxa de erro, latência, volume, duração de jobs e tamanho de filas.
- Logs: registram eventos estruturados que explicam decisões e falhas específicas.
O ganho não é apenas ter mais dados. O ganho é conectar dados. Um log com trace_id aponta para o trace completo. Um trace lento mostra qual span consumiu tempo. Uma métrica de erro por rota aponta para os traces daquela rota. Essa conexão encurta investigação e evita o ritual de abrir cinco dashboards sem saber por onde começar.
Em Go, o OpenTelemetry combina bem com a biblioteca padrão porque você pode instrumentar net/http, criar spans explícitos em pontos importantes e manter o código de negócio relativamente limpo. Frameworks como Gin, Chi, Echo e Fiber também têm instrumentações ou wrappers, mas o princípio é o mesmo: criar uma fronteira consistente de telemetria em volta das operações relevantes.
Dependências básicas
Comece com um módulo simples:
go mod init exemplo.com/api-pedidos
go get go.opentelemetry.io/otel \
go.opentelemetry.io/otel/sdk \
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp \
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
Para métricas, adicione também:
go get go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
Em produção, prefira exportar para um OpenTelemetry Collector via OTLP. O collector recebe dados de vários serviços, aplica filtros, adiciona atributos de infraestrutura e encaminha para o backend final. Isso evita que cada serviço Go conheça detalhes de autenticação, endpoint e formato de cada ferramenta.
Configurando tracing
Crie um pacote de observabilidade, por exemplo internal/observability. A configuração deve definir service.name, ambiente, versão e exportador. Esses atributos parecem burocráticos, mas salvam tempo quando você precisa separar produção de staging ou comparar versões depois de um deploy.
package observability
import (
"context"
"fmt"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
func ConfigureTracing(ctx context.Context, serviceName, version, environment string) (func(context.Context) error, error) {
exporter, err := otlptracehttp.New(ctx)
if err != nil {
return nil, fmt.Errorf("criar exportador OTLP: %w", err)
}
res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(serviceName),
semconv.ServiceVersion(version),
semconv.DeploymentEnvironment(environment),
),
)
if err != nil {
return nil, fmt.Errorf("criar resource: %w", err)
}
provider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(provider)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
return provider.Shutdown, nil
}
O TextMapPropagator permite receber e enviar contexto de trace em headers HTTP. Sem isso, uma chamada entre serviços pode aparecer como traces separados, quebrando a história da requisição.
Instrumentando uma API HTTP
Com otelhttp, você envolve handlers HTTP e ganha spans automáticos para cada requisição. Um exemplo mínimo:
package main
import (
"context"
"log"
"net/http"
"os"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
ctx := context.Background()
shutdown, err := ConfigureTracing(ctx, "api-pedidos-go", "1.0.0", env("APP_ENV", "local"))
if err != nil {
log.Fatal(err)
}
defer func() { _ = shutdown(context.Background()) }()
mux := http.NewServeMux()
mux.HandleFunc("GET /pedidos/{id}", buscarPedido)
handler := otelhttp.NewHandler(mux, "http.server")
log.Println("ouvindo em :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}
func env(key, fallback string) string {
if value := os.Getenv(key); value != "" {
return value
}
return fallback
}
Em Go 1.22+, o ServeMux suporta padrões como GET /pedidos/{id}. Se seu projeto usa outra versão ou framework, adapte o roteamento, mas mantenha a instrumentação na borda HTTP.
Criando spans manuais
Spans automáticos mostram entrada e saída da requisição. Spans manuais explicam o trabalho interno: consulta ao banco, chamada a parceiro, cálculo de frete, validação antifraude, publicação em fila, renderização de PDF ou qualquer operação relevante.
package main
import (
"context"
"encoding/json"
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
)
var tracer = otel.Tracer("api-pedidos/handlers")
func buscarPedido(w http.ResponseWriter, r *http.Request) {
ctx, span := tracer.Start(r.Context(), "buscar_pedido")
defer span.End()
pedidoID := r.PathValue("id")
span.SetAttributes(attribute.String("pedido.id", pedidoID))
pedido, err := carregarPedido(ctx, pedidoID)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "pedido não encontrado")
http.Error(w, "pedido não encontrado", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(pedido)
}
func carregarPedido(ctx context.Context, id string) (map[string]any, error) {
ctx, span := tracer.Start(ctx, "carregar_pedido_no_banco")
defer span.End()
span.SetAttributes(attribute.String("db.system", "postgresql"))
time.Sleep(40 * time.Millisecond)
return map[string]any{"id": id, "status": "pago"}, nil
}
Nem todo método precisa de span. Crie spans onde eles ajudam investigação. Um span para cada getter interno vira ruído. Um span para consulta SQL crítica, chamada externa ou publicação de evento costuma valer a pena.
Atributos seguros e cardinalidade
OpenTelemetry fica perigoso quando o time registra atributos sem critério. Atributos bons ajudam filtros e agregações. Atributos ruins vazam dados ou explodem cardinalidade.
Bons atributos para APIs Go:
service.name,service.versionedeployment.environment.- Método HTTP, rota, status e nome lógico da operação.
- Nome da fila, tabela, integração ou dependência externa.
- Tipo de erro, categoria de retry e resultado da operação.
- Identificadores internos quando realmente necessários e não sensíveis.
Evite CPF, e-mail, telefone, endereço, token, payload completo, senha, cabeçalho Authorization, query string sensível e dados pessoais desnecessários. Para produtos brasileiros, esse cuidado conversa com LGPD e também com custo: atributos de alta cardinalidade tornam métricas mais caras e dashboards menos úteis.
Uma regra prática: se o atributo puder ter milhões de valores diferentes por dia, pense duas vezes. Para investigação pontual, prefira log estruturado com retenção adequada ou amostragem controlada.
Métricas que valem começar
Traces explicam casos individuais. Métricas mostram tendência. Para uma API Go, o conjunto inicial deve responder perguntas operacionais simples:
| Métrica | Por que importa |
|---|---|
| Requisições por rota e status | Mostra volume e taxa de erro |
| Latência p50, p95 e p99 | Média esconde cauda lenta |
| Duração de chamadas externas | Identifica dependências problemáticas |
| Tempo de jobs e workers | Mostra degradação fora do HTTP |
| Tamanho de filas | Revela gargalo assíncrono |
| Retries e DLQ | Indica instabilidade real |
Não transforme tudo em gráfico no primeiro dia. Comece com latência, erro e volume. Depois acrescente métricas de negócio e dependências. Métrica boa tem dono, pergunta e ação possível. Métrica que ninguém olha vira custo.
Logs correlacionados com trace_id
Se você já usa slog, inclua trace_id e span_id nos logs de eventos importantes. Assim, um erro encontrado no log aponta para o trace completo.
package observability
import (
"context"
"log/slog"
"go.opentelemetry.io/otel/trace"
)
func Info(ctx context.Context, logger *slog.Logger, msg string, attrs ...slog.Attr) {
span := trace.SpanFromContext(ctx)
sc := span.SpanContext()
if sc.IsValid() {
attrs = append(attrs,
slog.String("trace_id", sc.TraceID().String()),
slog.String("span_id", sc.SpanID().String()),
)
}
logger.LogAttrs(ctx, slog.LevelInfo, msg, attrs...)
}
Use isso para eventos de negócio relevantes: pedido aprovado, pagamento recusado, webhook rejeitado, retry agendado, job finalizado, cache miss crítico. Evite logar cada linha de execução. O objetivo é deixar trilhas legíveis para humanos e pesquisáveis por máquina.
OTLP em produção
Em produção, configure variáveis de ambiente em vez de hardcode:
export APP_ENV=production
export OTEL_SERVICE_NAME=api-pedidos-go
export OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
No Kubernetes, essas variáveis ficam no Deployment ou em valores Helm. Em Docker Compose, ficam no serviço da aplicação. Em Cloud Run, ECS, Fly.io ou outra plataforma, ficam na configuração de ambiente.
O padrão com collector deixa a arquitetura mais flexível. A aplicação envia OTLP para o collector. O collector decide se manda traces para Tempo, métricas para Prometheus, logs para Loki, ou tudo para uma plataforma SaaS. Ele também pode fazer sampling, remover atributos sensíveis e adicionar metadados de cluster.
Testando sem depender do backend final
Para desenvolvimento local, você não precisa começar com uma stack completa. Algumas opções úteis:
- Usar exporter de console em ambiente local.
- Rodar um OpenTelemetry Collector em Docker Compose.
- Enviar traces para Jaeger ou Grafana Tempo local.
- Criar testes que garantem que handlers preservam
context.Context.
O último ponto é importante em Go. Se uma função troca ctx por context.Background() sem motivo, ela quebra cancelamento, deadline e trace propagation. Esse bug aparece como requisições órfãs, spans incompletos e operações que continuam rodando depois que o cliente desistiu.
Erros comuns em Go
O primeiro erro é configurar OpenTelemetry no main e esquecer shutdown. Exportadores em batch precisam descarregar dados antes do processo encerrar. Em deploys rápidos ou jobs curtos, sem shutdown você perde spans.
O segundo erro é instrumentar apenas o HTTP de entrada. Em sistemas reais, a lentidão costuma morar em banco, Redis, fila, API externa ou worker. A borda HTTP diz que a requisição demorou 2 segundos; spans internos dizem onde os 2 segundos foram gastos.
O terceiro erro é usar nomes de spans dinâmicos demais, como buscar_pedido_123. Prefira buscar_pedido e coloque o ID em atributo quando for seguro. Nome de span deve agrupar operações, não criar uma série infinita.
O quarto erro é ignorar logs. Tracing mostra forma e tempo, mas nem sempre explica decisão. Um log estruturado com motivo="saldo_insuficiente", tentativa=3 ou retry_em="30s" complementa o trace.
O quinto erro é comparar local e produção no mesmo painel sem deployment.environment. Quando telemetria de teste mistura com telemetria real, o time perde confiança nos dados.
OpenTelemetry no portfólio e na carreira
Para quem busca vagas Go no Brasil, observabilidade é um sinal forte de maturidade. Muitas descrições de backend, SRE, plataforma e DevOps já citam logs, métricas, traces, Prometheus, Grafana, Datadog, OpenTelemetry, Kubernetes e incidentes. Um projeto com API, banco, testes e OpenTelemetry documentado comunica que você pensa além do endpoint feliz.
No README do projeto, explique:
- Como rodar a API localmente.
- Como ver traces e métricas.
- Quais variáveis
OTEL_*configurar. - Quais atributos são seguros.
- Quais dados nunca entram em log ou span.
- Um exemplo de investigação: rota lenta, erro 500 ou chamada externa falhando.
Se você trabalha em time com múltiplas linguagens, o valor aumenta. OpenTelemetry permite comparar serviços Go, Python, Java, Kotlin e Rust em uma mesma linguagem operacional. Para um contraste com outra stack comum no backend brasileiro, veja também o guia de OpenTelemetry com Python no Python Brasil.
Próximos passos
Implemente em camadas. Primeiro, configure service.name, ambiente, propagação e exportação OTLP. Depois, envolva a borda HTTP. Em seguida, adicione spans manuais para banco, filas e APIs externas. Só então avance para métricas mais específicas e alertas.
Uma boa meta inicial é simples: dado um erro 500 em produção, você consegue sair do alerta para a métrica, da métrica para alguns traces, do trace para logs correlacionados e dos logs para uma hipótese clara? Se a resposta for sim, sua aplicação Go deixou de ser apenas um binário rápido e passou a ser um serviço operável.
Observabilidade não elimina incidentes, mas reduz o tempo em que a equipe fica cega. Em Go, com OpenTelemetry, context.Context, slog e instrumentação cuidadosa, dá para criar essa base sem transformar o código em um emaranhado de SDKs proprietários. Esse é o ponto: menos magia, mais sinais úteis e um caminho claro para operar software real.