---
title: "OpenTelemetry em Go: Traces e Métricas"
url: "https://golang.com.br/blog/go-opentelemetry-observabilidade-tracing-metricas/"
markdown_url: "https://golang.com.br/blog/go-opentelemetry-observabilidade-tracing-metricas.MD"
description: "Aprenda OpenTelemetry em Go para instrumentar APIs com traces, métricas, logs correlacionados, OTLP, collector e boas práticas de produção."
date: "2026-05-27"
author: "Golang Brasil"
---

# OpenTelemetry em Go: Traces e Métricas

Aprenda OpenTelemetry em Go para instrumentar APIs com traces, métricas, logs correlacionados, OTLP, collector e boas práticas de produção.


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 <a href="https://python.dev.br/blog/opentelemetry-python-observabilidade/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">OpenTelemetry com Python</a> 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](/aprenda/api-rest-go/), [slog em Go](/blog/slog-go-logging-estruturado/), [context.Context em Go](/blog/context-timeout-cancelamento-go/) e [rate limiting em Go](/blog/rate-limiting-go-api-producao/).

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

```bash
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:

```bash
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.

```go
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:

```go
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.

```go
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.version` e `deployment.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.

```go
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:

```bash
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 <a href="https://python.dev.br/blog/opentelemetry-python-observabilidade/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'python.dev.br' })">OpenTelemetry com Python</a> 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.
