Go
Instrumentação Go com Elven Observability
Sumário
- Visão geral
- Pré-requisitos
- Exemplo de referência
- Configuração de variáveis de ambiente
- Bootstrap OpenTelemetry
- Instrumentando o HTTP server
- Instrumentando o HTTP client
- Instrumentando PostgreSQL
- Instrumentando Redis
- Spans manuais
- Métricas customizadas de negócio
- Golden Signals mapeados
- Worker, retry e DLQ
- Estrutura de projeto recomendada
- Como rodar localmente
- Como validar no collector da Elven
- Como adaptar para a sua aplicação
- Testes
- Troubleshooting
- Checklist de deploy
Visão geral
A instrumentação Go da Elven usa o SDK oficial do OpenTelemetry para Go com exportação OTLP HTTP. Ela cobre instrumentação automática de HTTP server e client, PostgreSQL e Redis, além de instrumentação manual de spans e métricas de negócio (golden signals).
O Collector OTLP fica sempre no ambiente do cliente. A aplicação envia traces e métricas para esse Collector local/do cliente, e o Collector encaminha os dados para Tempo e Mimir conforme a arquitetura contratada.
- Grafana Tempo para traces
- Grafana Mimir para métricas
- Grafana para consulta, correlação, painéis e alertas
| Componente | O que faz |
|---|---|
otelhttp |
Instrumentação automática de HTTP server e client. Gera spans e métricas HTTP. |
otelsql |
Instrumentação de database/sql com spans de queries e métricas de pool. |
redisotel |
Instrumentação de Redis com spans de operações e métricas de cache/fila. |
otlptracehttp |
Exporter OTLP HTTP para traces. |
otlpmetrichttp |
Exporter OTLP HTTP para métricas. |
otelprom |
Exporter Prometheus para expor métricas localmente em /metrics. |
Pré-requisitos
- Go 1.21+ instalado
- Docker Desktop ou Docker Engine funcionando
- Acesso de rede ao collector da Elven
- Collector OTLP HTTP aceitando
POST /v1/tracesePOST /v1/metrics
Baseline técnico
| Item | Versão |
|---|---|
| Go | 1.21+ |
go.opentelemetry.io/otel |
v1.42.0 |
go.opentelemetry.io/contrib |
v1.42.0 |
otelhttp |
v0.67.0 |
otlptracehttp |
v1.42.0 |
otlpmetrichttp |
v1.42.0 |
otelprom |
v0.64.0 |
chi |
v5.2.5 |
pgx/v5 |
v5.8.0 |
go-redis/v9 |
v9.18.0 |
redisotel/v9 |
v9.18.0 |
otelsql |
v0.41.0 |
Checklist do collector
Antes de subir a aplicação, confirme:
- O endpoint de traces está correto
- O endpoint de métricas está correto
- O header de autenticação está definido (se necessário)
- O collector aceita
http/protobuf
Atenção: se o collector exigir token, o valor em
OTEL_EXPORTER_OTLP_HEADERSdeve estar URL-encoded. Exemplo:
OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer%20SEU_TOKEN
Exemplo de referência
A Elven disponibiliza uma aplicação demo completa em Go que demonstra todos os conceitos desta documentação:
👉 elven-observability/go-otel-app
Configuração de variáveis de ambiente
Copie o arquivo de exemplo e preencha com os dados do seu collector:
cp .env.example .env
Configuração mínima funcional:
APP_NAME=go-otel-app
APP_ENV=local
APP_VERSION=1.0.0
HTTP_ADDR=:8080
PUBLIC_BASE_URL=http://localhost:8080
DATABASE_URL=postgres://postgres:postgres@localhost:5432/mydb?sslmode=disable
REDIS_ADDR=localhost:6379
OTEL_SERVICE_NAME=go-otel-app
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=local,service.version=1.0.0
OTEL_TRACES_EXPORTER=otlp
OTEL_METRICS_EXPORTER=otlp
OTEL_LOGS_EXPORTER=none
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://SEU-COLLECTOR:4318/v1/traces
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://SEU-COLLECTOR:4318/v1/metrics
OTEL_EXPORTER_OTLP_HEADERS=authorization=Bearer%20SEU_TOKEN
OTEL_SEMCONV_STABILITY_OPT_IN=*
Sobre
OTEL_SEMCONV_STABILITY_OPT_IN: definir comodatabase(ou*) ativa a semântica de banco de dados mais recente, incluindo a métricadb.client.operation.durationem segundos. Sem isso, algumas instrumentações podem expor métricas legadas em milissegundos.
Bootstrap OpenTelemetry
O ponto central da instrumentação é criar um TracerProvider e um MeterProvider com dois destinos para métricas: o collector OTLP e o endpoint /metrics local (Prometheus).
func SetupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
res, err := newResource()
if err != nil {
return nil, err
}
traceProvider, err := newTraceProvider(ctx, res)
if err != nil {
return nil, err
}
otel.SetTracerProvider(traceProvider)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
meterProvider, err := newMeterProvider(ctx, res)
if err != nil {
return nil, err
}
otel.SetMeterProvider(meterProvider)
shutdown = func(ctx context.Context) error {
var errs []error
if err := traceProvider.Shutdown(ctx); err != nil {
errs = append(errs, err)
}
if err := meterProvider.Shutdown(ctx); err != nil {
errs = append(errs, err)
}
return errors.Join(errs...)
}
return shutdown, nil
}
Instrumentando o HTTP server
Use o middleware otelhttp.NewHandler para instrumentar automaticamente todas as rotas:
router := chi.NewRouter()
router.Use(middleware.RequestID)
router.Use(middleware.RealIP)
// Wrap com otelhttp para instrumentação automática
handler := otelhttp.NewHandler(router, "server",
otelhttp.WithMessageEvents(otelhttp.ReadEvents, otelhttp.WriteEvents),
)
server := &http.Server{
Addr: cfg.HTTPAddr,
Handler: handler,
}
Isso gera automaticamente:
- Spans para cada request com atributos HTTP (método, rota, status code)
- Métricas
http.server.request.durationehttp.server.active_requests
Instrumentando o HTTP client
httpClient := &http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
Timeout: 10 * time.Second,
}
Isso propaga automaticamente o traceparent header para serviços downstream.
Instrumentando PostgreSQL
db, err := otelsql.Open("pgx", cfg.DatabaseURL,
otelsql.WithAttributes(semconv.DBSystemPostgreSQL),
otelsql.WithDBName("mydb"),
)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// Registra métricas de pool de conexões
if err := otelsql.RegisterDBStatsMetrics(db,
otelsql.WithAttributes(semconv.DBSystemPostgreSQL),
); err != nil {
return nil, fmt.Errorf("failed to register db stats: %w", err)
}
Métricas geradas automaticamente:
db.client.operation.duration— latência de queriesdb.client.connection.count— pool de conexõesdb.client.connection.idle— conexões ociosas
Instrumentando Redis
rdb := redis.NewClient(&redis.Options{
Addr: cfg.RedisAddr,
})
if err := redisotel.InstrumentTracing(rdb); err != nil {
return nil, fmt.Errorf("failed to instrument redis tracing: %w", err)
}
if err := redisotel.InstrumentMetrics(rdb); err != nil {
return nil, fmt.Errorf("failed to instrument redis metrics: %w", err)
}
Spans manuais
Para instrumentar operações de negócio específicas:
func (s *OrderService) ProcessOrder(ctx context.Context, order Order) error {
ctx, span := otel.Tracer("order-service").Start(ctx, "ProcessOrder",
trace.WithAttributes(
attribute.String("order.id", order.ID),
attribute.String("order.customer_id", order.CustomerID),
attribute.Float64("order.total", order.Total),
),
)
defer span.End()
if err := s.validateOrder(ctx, order); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "order processed successfully")
return nil
}
Métricas customizadas de negócio
meter := otel.Meter("go-otel-app")
// Contador de pedidos
orderCounter, _ := meter.Int64Counter("orders.total",
metric.WithDescription("Total number of orders processed"),
metric.WithUnit("{order}"),
)
// Histograma de valor dos pedidos
orderValue, _ := meter.Float64Histogram("orders.value",
metric.WithDescription("Value of orders processed"),
metric.WithUnit("USD"),
)
// Uso
orderCounter.Add(ctx, 1, metric.WithAttributes(
attribute.String("status", "success"),
attribute.String("payment_method", "credit_card"),
))
orderValue.Record(ctx, order.Total)
Golden Signals mapeados
| Signal | Métrica OTel | Como usar |
|---|---|---|
| Latência | http.server.request.duration |
Histograma gerado pelo otelhttp |
| Tráfego | http.server.request.body.size |
Gerado automaticamente |
| Erros | http.server.request.duration{error.type=*} |
Status codes 4xx/5xx |
| Saturação | db.client.connection.count |
Pool de conexões do banco |
Worker, retry e DLQ
Para workers que processam filas com retry e Dead Letter Queue (DLQ):
func (w *Worker) ProcessMessage(ctx context.Context, msg Message) error {
ctx, span := otel.Tracer("worker").Start(ctx, "ProcessMessage",
trace.WithAttributes(
attribute.String("messaging.system", "redis"),
attribute.String("messaging.destination", msg.Queue),
attribute.Int("messaging.retry_count", msg.RetryCount),
),
)
defer span.End()
if err := w.handler(ctx, msg); err != nil {
span.RecordError(err)
if msg.RetryCount >= w.maxRetries {
// Envia para DLQ
w.dlq.Push(ctx, msg)
span.SetAttributes(attribute.Bool("messaging.dead_letter", true))
}
return err
}
return nil
}
Estrutura de projeto recomendada
go-otel-app/
├── cmd/
│ └── api/
│ └── main.go # Entrypoint, wiring de dependências
├── internal/
│ ├── config/ # Leitura de variáveis de ambiente
│ ├── otel/ # Bootstrap do SDK OTel
│ ├── server/ # HTTP server e rotas
│ ├── handler/ # Handlers HTTP
│ ├── service/ # Regras de negócio
│ ├── repository/ # Acesso ao banco
│ └── worker/ # Workers de fila
├── .env.example
├── docker-compose.yml
└── go.mod
Como rodar localmente
# 1. Clone o repositório de exemplo
git clone https://github.com/elven-observability/go-otel-app
cd go-otel-app
# 2. Configure as variáveis de ambiente
cp .env.example .env
# Edite .env com os endpoints do seu collector
# 3. Suba as dependências (Postgres, Redis)
docker compose up -d postgres redis
# 4. Rode a aplicação
go run ./cmd/api
Como validar no collector da Elven
Após subir a aplicação, valide que os dados estão chegando:
- Acesse o Grafana Explore e selecione o datasource Tempo
- Busque por
service.name = "go-otel-app" - Verifique que os traces aparecem com spans de HTTP, banco e Redis
- Troque para o datasource Mimir e busque a métrica
http_server_request_duration_seconds_count
Como adaptar para a sua aplicação
- Substitua
go-otel-apppelo nome do seu serviço emOTEL_SERVICE_NAMEe nos atributos do resource - Remova os handlers e services de exemplo, mantenha apenas o bootstrap OTel em
internal/otel/ - Adicione
otelhttp.NewHandlerno seu HTTP server - Adicione
otelhttp.NewTransportnos seus HTTP clients - Instrumente o banco e Redis conforme as seções acima
- Adicione spans manuais nas operações críticas de negócio
Testes
Para testar a instrumentação sem um collector real, use o InMemoryExporter:
func TestOrderService(t *testing.T) {
// Setup in-memory exporter
exp := tracetest.NewInMemoryExporter()
tp := sdktrace.NewTracerProvider(
sdktrace.WithSyncer(exp),
)
otel.SetTracerProvider(tp)
svc := NewOrderService(...)
err := svc.ProcessOrder(context.Background(), testOrder)
assert.NoError(t, err)
spans := exp.GetSpans()
assert.Len(t, spans, 1)
assert.Equal(t, "ProcessOrder", spans[0].Name)
}
Troubleshooting
| Problema | Causa provável | Solução |
|---|---|---|
| Traces não aparecem no Tempo | Endpoint incorreto ou sem conectividade | Verifique OTEL_EXPORTER_OTLP_TRACES_ENDPOINT e teste com curl |
| Métricas não aparecem no Mimir | Autenticação incorreta | Verifique OTEL_EXPORTER_OTLP_HEADERS com token URL-encoded |
Erro context deadline exceeded |
Collector inacessível | Verifique firewall e rede entre a aplicação e o collector |
| Métricas de banco em milissegundos | OTEL_SEMCONV_STABILITY_OPT_IN não definido |
Adicione OTEL_SEMCONV_STABILITY_OPT_IN=* no .env |
Spans sem nome de rota (/) |
otelhttp sem WithRouteTag |
Use otelhttp.WithRouteTag(route, w, r) nos handlers |
Checklist de deploy
- [ ]
OTEL_SERVICE_NAMEdefinido com o nome correto do serviço - [ ]
OTEL_EXPORTER_OTLP_TRACES_ENDPOINTapontando para o collector - [ ]
OTEL_EXPORTER_OTLP_METRICS_ENDPOINTapontando para o collector - [ ]
OTEL_EXPORTER_OTLP_HEADERScom token URL-encoded (se necessário) - [ ]
OTEL_SEMCONV_STABILITY_OPT_IN=*definido - [ ]
otelhttp.NewHandleraplicado no HTTP server - [ ] HTTP clients usando
otelhttp.NewTransport - [ ] Banco instrumentado com
otelsql - [ ] Redis instrumentado com
redisotel - [ ] Traces visíveis no Grafana Tempo
- [ ] Métricas visíveis no Grafana Mimir