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/traces e POST /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:

  1. O endpoint de traces está correto
  2. O endpoint de métricas está correto
  3. O header de autenticação está definido (se necessário)
  4. O collector aceita http/protobuf

Atenção: se o collector exigir token, o valor em OTEL_EXPORTER_OTLP_HEADERS deve 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 como database (ou *) ativa a semântica de banco de dados mais recente, incluindo a métrica db.client.operation.duration em 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.duration e http.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 queries
  • db.client.connection.count — pool de conexões
  • db.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:

  1. Acesse o Grafana Explore e selecione o datasource Tempo
  2. Busque por service.name = "go-otel-app"
  3. Verifique que os traces aparecem com spans de HTTP, banco e Redis
  4. Troque para o datasource Mimir e busque a métrica http_server_request_duration_seconds_count

Como adaptar para a sua aplicação

  1. Substitua go-otel-app pelo nome do seu serviço em OTEL_SERVICE_NAME e nos atributos do resource
  2. Remova os handlers e services de exemplo, mantenha apenas o bootstrap OTel em internal/otel/
  3. Adicione otelhttp.NewHandler no seu HTTP server
  4. Adicione otelhttp.NewTransport nos seus HTTP clients
  5. Instrumente o banco e Redis conforme as seções acima
  6. 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_NAME definido com o nome correto do serviço
  • [ ] OTEL_EXPORTER_OTLP_TRACES_ENDPOINT apontando para o collector
  • [ ] OTEL_EXPORTER_OTLP_METRICS_ENDPOINT apontando para o collector
  • [ ] OTEL_EXPORTER_OTLP_HEADERS com token URL-encoded (se necessário)
  • [ ] OTEL_SEMCONV_STABILITY_OPT_IN=* definido
  • [ ] otelhttp.NewHandler aplicado 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