Go

Guia Completo de Observabilidade em Go: OpenTelemetry, Métricas e Tracing Distribuído

17 min de leitura

Guia Completo de Observabilidade em Go: OpenTelemetry, Métricas e Tracing Distribuído

O que é Observabilidade e por que você precisa dela Observabilidade é a capacidade de entender o estado interno de um sistema através de sinais que ele emite. Ao contrário de monitoramento tradicional, que responde perguntas pré-definidas, observabilidade permite explorar dados desconhecidos em tempo real. Um sistema é considerado observável quando você consegue deduzir o que está acontecendo internamente apenas olhando suas saídas externas: logs, métricas e traces. Em uma arquitetura de microsserviços em Go, você não consegue mais confiar em logs centralizados ou em um único servidor. Uma requisição HTTP passa por múltiplos serviços, bancos de dados, filas de mensagem. Se algo falha, você precisa rastreá-la através de toda a cadeia de chamadas. OpenTelemetry (OTel) é o padrão aberto que você usa para coletar e exportar esses sinais de forma consistente, sem depender de um fornecedor específico. OpenTelemetry: O Padrão Unificado O que é OpenTelemetry OpenTelemetry é um framework de instrumentação agnóstico que oferece SDKs, ferramentas e especificações para

<h2>O que é Observabilidade e por que você precisa dela</h2>

<p>Observabilidade é a capacidade de entender o estado interno de um sistema através de sinais que ele emite. Ao contrário de monitoramento tradicional, que responde perguntas pré-definidas, observabilidade permite explorar dados desconhecidos em tempo real. Um sistema é considerado observável quando você consegue deduzir o que está acontecendo internamente apenas olhando suas saídas externas: logs, métricas e traces.</p>

<p>Em uma arquitetura de microsserviços em Go, você não consegue mais confiar em logs centralizados ou em um único servidor. Uma requisição HTTP passa por múltiplos serviços, bancos de dados, filas de mensagem. Se algo falha, você precisa rastreá-la através de toda a cadeia de chamadas. OpenTelemetry (OTel) é o padrão aberto que você usa para coletar e exportar esses sinais de forma consistente, sem depender de um fornecedor específico.</p>

<h2>OpenTelemetry: O Padrão Unificado</h2>

<h3>O que é OpenTelemetry</h3>

<p>OpenTelemetry é um framework de instrumentação agnóstico que oferece SDKs, ferramentas e especificações para coletar sinais de observabilidade. Ele não é um backend (como Jaeger ou Datadog), mas sim uma camada de abstração que padroniza como você instrumenta sua aplicação. Você escreve o código uma única vez usando OpenTelemetry e pode enviar dados para qualquer backend compatível.</p>

<p>A beleza do OpenTelemetry está em sua arquitetura em camadas. Você tem APIs de alto nível que definem <strong>o que</strong> instrumentar, implementações do SDK que definem <strong>como</strong> coletar, e exportadores que definem <strong>para onde</strong> enviar os dados. Isso significa trocar de Jaeger para DataDog é uma mudança de configuração, não de código.</p>

<h3>Componentes principais do OpenTelemetry</h3>

<p>OpenTelemetry consiste em três pilares: <strong>Traces</strong> (rastreamento distribuído), <strong>Métricas</strong> (observação de valores numéricos ao longo do tempo) e <strong>Logs</strong> (eventos estruturados). Cada um responde uma pergunta diferente: traces respondem &quot;qual foi o caminho dessa requisição?&quot;, métricas respondem &quot;qual é a taxa de erro?&quot; e logs respondem &quot;qual mensagem específica foi registrada?&quot;.</p>

<p>Instalando o OpenTelemetry em Go:</p>

<pre><code class="language-bash">go get go.opentelemetry.io/otel

go get go.opentelemetry.io/otel/sdk

go get go.opentelemetry.io/otel/exporters/jaeger/otlp

go get go.opentelemetry.io/otel/trace</code></pre>

<p>A configuração mínima requer um <code>TracerProvider</code>, que é a fábrica responsável por criar instâncias de <code>Tracer</code>. Um <code>Tracer</code> é o instrumento real que você usa para criar spans (unidades de trabalho rastreáveis).</p>

<h2>Tracing Distribuído: Rastreando Requisições Através de Serviços</h2>

<h3>Conceito fundamental de spans</h3>

<p>Um span representa uma unidade de trabalho em uma operação. Imagine uma requisição HTTP que chega no serviço A, que faz uma chamada RPC para o serviço B, que consulta um banco de dados. Isso são três spans: um para a requisição HTTP, outro para a RPC e outro para a query do banco. O primeiro span é o &quot;span pai&quot; (root span), e os outros são &quot;spans filhos&quot;. Juntos, formam um trace.</p>

<p>Cada span armazena informações cruciais: quando começou, quanto tempo levou, se teve erro, qual foi o erro, quem foi seu pai, atributos customizados, e eventos. O tracer extrai o contexto (trace ID, span ID, flags) do cabeçalho da requisição recebida e injeta novamente antes de fazer requisições para outros serviços. Isso é como você &quot;pega o fio&quot; da requisição enquanto ela passa por múltiplos microsserviços.</p>

<h3>Exemplo prático: Instrumentando uma aplicação Go</h3>

<pre><code class="language-go">package main

import (

&quot;context&quot;

&quot;fmt&quot;

&quot;go.opentelemetry.io/otel&quot;

&quot;go.opentelemetry.io/otel/exporters/jaeger/otlp&quot;

&quot;go.opentelemetry.io/otel/sdk/resource&quot;

sdktrace &quot;go.opentelemetry.io/otel/sdk/trace&quot;

semconv &quot;go.opentelemetry.io/otel/semconv/v1.21.0&quot;

&quot;go.opentelemetry.io/otel/trace&quot;

&quot;log&quot;

)

func initTracer() (trace.TracerProvider, error) {

exporter, err := otlp.New(context.Background())

if err != nil {

return nil, err

}

tp := sdktrace.NewTracerProvider(

sdktrace.WithBatcher(exporter),

sdktrace.WithResource(resource.NewWithAttributes(

context.Background(),

semconv.ServiceNameKey.String(&quot;meu-servico&quot;),

semconv.ServiceVersionKey.String(&quot;1.0.0&quot;),

)),

)

otel.SetTracerProvider(tp)

return tp, nil

}

func processarPedido(ctx context.Context, tracer trace.Tracer, pedidoID string) error {

// Cria um span raiz para a operação completa

ctx, span := tracer.Start(ctx, &quot;processar-pedido&quot;)

defer span.End()

span.SetAttributes(semconv.DBOperationKey.String(&quot;SELECT&quot;))

// Simula uma operação de banco de dados

if err := buscarPedidoDoBanco(ctx, tracer, pedidoID); err != nil {

span.RecordError(err)

span.SetStatus(sdktrace.Status{

Code: sdktrace.StatusCodeError,

Description: err.Error(),

})

return err

}

// Simula uma chamada para outro serviço

if err := notificarServico(ctx, tracer, pedidoID); err != nil {

span.RecordError(err)

return err

}

return nil

}

func buscarPedidoDoBanco(ctx context.Context, tracer trace.Tracer, pedidoID string) error {

_, span := tracer.Start(ctx, &quot;buscar-pedido-banco&quot;)

defer span.End()

span.SetAttributes(

semconv.DBSystemKey.String(&quot;postgresql&quot;),

semconv.DBOperationKey.String(&quot;SELECT&quot;),

semconv.DBStatementKey.String(&quot;SELECT * FROM pedidos WHERE id = ?&quot;),

)

// Simula latência

fmt.Printf(&quot;Buscando pedido %s no banco...\n&quot;, pedidoID)

return nil

}

func notificarServico(ctx context.Context, tracer trace.Tracer, pedidoID string) error {

_, span := tracer.Start(ctx, &quot;notificar-servico-pagamento&quot;)

defer span.End()

span.SetAttributes(

semconv.RPCServiceKey.String(&quot;payment-service&quot;),

semconv.RPCMethodKey.String(&quot;ProcessPayment&quot;),

)

fmt.Printf(&quot;Notificando serviço de pagamento para pedido %s...\n&quot;, pedidoID)

return nil

}

func main() {

tp, err := initTracer()

if err != nil {

log.Fatal(err)

}

defer tp.Shutdown(context.Background())

tracer := otel.Tracer(&quot;meu-servico/operacoes&quot;)

ctx := context.Background()

if err := processarPedido(ctx, tracer, &quot;pedido-123&quot;); err != nil {

log.Fatal(err)

}

fmt.Println(&quot;Pedido processado com sucesso!&quot;)

}</code></pre>

<p>Neste exemplo, criamos uma hierarquia de spans. O span <code>processar-pedido</code> é o pai, e <code>buscar-pedido-banco</code> e <code>notificar-servico-pagamento</code> são filhos. O rastreamento distribuído funciona porque o contexto (<code>ctx</code>) carrega o trace ID e span ID através das chamadas de função. Quando você enviar a requisição para outro serviço, você injetaria esse contexto no cabeçalho HTTP.</p>

<h3>Propagação de contexto entre serviços</h3>

<p>A propagação de contexto é como o OpenTelemetry &quot;segue&quot; uma requisição através de múltiplos serviços. Você usa um <code>TextMapPropagator</code> para extrair o trace ID do cabeçalho recebido e injetar em novos cabeçalhos quando faz requisições. O padrão W3C Trace Context é o recomendado:</p>

<pre><code class="language-go">import (

&quot;go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp&quot;

&quot;go.opentelemetry.io/otel/propagation&quot;

&quot;net/http&quot;

)

// Configurar propagador global

otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(

propagation.TraceContext{},

propagation.Baggage{},

))

// Usar cliente HTTP com instrumentação automática

client := &amp;http.Client{

Transport: otelhttp.NewTransport(http.DefaultTransport),

}

// Fazer requisição - o contexto será automaticamente propagado

resp, err := client.Do(req.WithContext(ctx))</code></pre>

<p>Isso permite que quando serviço A chama serviço B, ambos apareçam no mesmo trace. O backend de visualização (Jaeger, DataDog, etc.) mostrará a cadeia completa de chamadas.</p>

<h2>Métricas: Monitorando o Comportamento Contínuo</h2>

<h3>Entendendo métricas vs traces</h3>

<p>Enquanto traces capturam eventos pontuais e sequências, métricas medem valores agregados ao longo do tempo. Uma métrica responde perguntas como &quot;qual é a latência p99 nos últimos 5 minutos?&quot; ou &quot;quantas requisições falharam por hora?&quot;. As métricas em OpenTelemetry são compostas por instrumentos: <strong>Counter</strong> (apenas aumenta), <strong>Histogram</strong> (distribui valores em buckets), <strong>Gauge</strong> (valor que pode subir ou descer) e <strong>UpDownCounter</strong> (contador que aumenta ou diminui).</p>

<p>Métricas são eficientes porque você não armazena cada evento individual. Em vez disso, você armazena resumos estatísticos. Um histogram com 1 milhão de observações ocupa espaço mínimo após agregação. Isso as torna ideais para alertas automáticos e dashboards em tempo real.</p>

<h3>Exemplo prático: Coletando métricas</h3>

<pre><code class="language-go">package main

import (

&quot;context&quot;

&quot;fmt&quot;

&quot;go.opentelemetry.io/otel&quot;

&quot;go.opentelemetry.io/otel/exporters/prometheus&quot;

&quot;go.opentelemetry.io/otel/metric&quot;

sdkmetric &quot;go.opentelemetry.io/otel/sdk/metric&quot;

&quot;go.opentelemetry.io/otel/sdk/resource&quot;

semconv &quot;go.opentelemetry.io/otel/semconv/v1.21.0&quot;

&quot;log&quot;

&quot;net/http&quot;

&quot;time&quot;

)

func initMetrics() (metric.MeterProvider, error) {

exporter, err := prometheus.New()

if err != nil {

return nil, err

}

mp := sdkmetric.NewMeterProvider(

sdkmetric.WithReader(exporter),

sdkmetric.WithResource(resource.NewWithAttributes(

context.Background(),

semconv.ServiceNameKey.String(&quot;meu-servico-metricas&quot;),

)),

)

otel.SetMeterProvider(mp)

return mp, nil

}

func processarRequisicoes(ctx context.Context, meter metric.Meter) {

// Criar instrumentos

counter, err := meter.Int64Counter(

&quot;requisicoes.total&quot;,

metric.WithDescription(&quot;Total de requisições processadas&quot;),

metric.WithUnit(&quot;{request}&quot;),

)

if err != nil {

log.Fatal(err)

}

histogram, err := meter.Float64Histogram(

&quot;requisicao.duracao&quot;,

metric.WithDescription(&quot;Duração das requisições&quot;),

metric.WithUnit(&quot;ms&quot;),

)

if err != nil {

log.Fatal(err)

}

gauge, err := meter.Int64Gauge(

&quot;conexoes.ativas&quot;,

metric.WithDescription(&quot;Número de conexões ativas&quot;),

metric.WithUnit(&quot;{connection}&quot;),

)

if err != nil {

log.Fatal(err)

}

// Simular requisições

for i := 0; i &lt; 10; i++ {

start := time.Now()

// Incrementa counter

counter.Add(ctx, 1, metric.WithAttributes(

semconv.HTTPStatusCodeKey.Int(200),

semconv.HTTPMethodKey.String(&quot;GET&quot;),

))

// Incrementa gauge de conexões

gauge.Record(ctx, 5)

// Simula latência

time.Sleep(100 * time.Millisecond)

// Registra duração no histogram

duration := time.Since(start).Milliseconds()

histogram.Record(ctx, float64(duration), metric.WithAttributes(

semconv.HTTPEndpointKey.String(&quot;/api/pedidos&quot;),

))

fmt.Printf(&quot;Requisição %d processada em %dms\n&quot;, i+1, duration)

}

}

func main() {

mp, err := initMetrics()

if err != nil {

log.Fatal(err)

}

meter := otel.Meter(&quot;meu-servico/metricas&quot;)

ctx := context.Background()

// Processar requisições em goroutine

go processarRequisicoes(ctx, meter)

// Expor métricas no endpoint Prometheus

http.Handle(&quot;/metrics&quot;, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

// O exporter de Prometheus já cuida disso automaticamente

}))

fmt.Println(&quot;Servidor de métricas rodando em http://localhost:9090/metrics&quot;)

http.ListenAndServe(&quot;:9090&quot;, nil)

}</code></pre>

<h3>Tipos de instrumentos e quando usá-los</h3>

<p>Um <strong>Counter</strong> é para coisas que apenas aumentam: requisições processadas, erros ocorridos, bytes transferidos. Um <strong>Histogram</strong> mede a distribuição de valores: latência, tamanho de payload, tempo de processamento. Um <strong>Gauge</strong> é útil para coisas que flutuam: temperatura, memória em uso, conexões abertas. Um <strong>UpDownCounter</strong> é raro, mas útil para filas: itens adicionados e removidos.</p>

<p>Na prática, para uma API REST, você usaria um Counter para total de requisições por método/endpoint/status, um Histogram para latência das requisições, e um Gauge para conexões ativas do banco de dados. Isso cria uma visão clara do comportamento do sistema.</p>

<h2>Integrando OpenTelemetry em uma Aplicação Real</h2>

<h3>Instrumentação automática vs manual</h3>

<p>Go tem bibliotecas instrumentadas prontas para OpenTelemetry. Para SQL, use <code>go.opentelemetry.io/contrib/instrumentation/database/sql/otelsql</code>. Para HTTP, use <code>go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp</code>. Isso significa você ganha observabilidade quase de graça, apenas envolvendo seus clientes/servidores existentes.</p>

<p>Instrumentação manual é para lógica específica do negócio. Você cria spans para operações que o framework não cobre automaticamente. A chave é não exagerar: mais spans não significa mais informação, significa mais ruído. Um bom rule of thumb é ter um span para cada &quot;decisão&quot; no código: &quot;devemos reprocessar essa requisição?&quot;, &quot;quais dados precisamos buscar?&quot;.</p>

<h3>Exemplo de aplicação completa com HTTP e Database</h3>

<pre><code class="language-go">package main

import (

&quot;context&quot;

&quot;database/sql&quot;

&quot;fmt&quot;

&quot;go.opentelemetry.io/contrib/instrumentation/database/sql/otelsql&quot;

&quot;go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp&quot;

&quot;go.opentelemetry.io/otel&quot;

&quot;go.opentelemetry.io/otel/codes&quot;

&quot;go.opentelemetry.io/otel/exporters/jaeger/otlp&quot;

&quot;go.opentelemetry.io/otel/exporters/prometheus&quot;

&quot;go.opentelemetry.io/otel/metric&quot;

sdkmetric &quot;go.opentelemetry.io/otel/sdk/metric&quot;

sdktrace &quot;go.opentelemetry.io/otel/sdk/trace&quot;

&quot;go.opentelemetry.io/otel/sdk/resource&quot;

semconv &quot;go.opentelemetry.io/otel/semconv/v1.21.0&quot;

&quot;go.opentelemetry.io/otel/trace&quot;

_ &quot;github.com/lib/pq&quot;

&quot;log&quot;

&quot;net/http&quot;

&quot;time&quot;

)

// Inicializar tracer e métricas

func initTelemetry() (trace.TracerProvider, metric.MeterProvider, error) {

res, err := resource.New(context.Background(),

resource.WithAttributes(

semconv.ServiceNameKey.String(&quot;pedidos-api&quot;),

semconv.ServiceVersionKey.String(&quot;1.0.0&quot;),

),

)

if err != nil {

return nil, nil, err

}

// Tracer

traceExporter, err := otlp.New(context.Background())

if err != nil {

return nil, nil, err

}

tp := sdktrace.NewTracerProvider(

sdktrace.WithBatcher(traceExporter),

sdktrace.WithResource(res),

)

// Métricas

metricExporter, err := prometheus.New()

if err != nil {

return nil, nil, err

}

mp := sdkmetric.NewMeterProvider(

sdkmetric.WithReader(metricExporter),

sdkmetric.WithResource(res),

)

otel.SetTracerProvider(tp)

otel.SetMeterProvider(mp)

return tp, mp, nil

}

type PedidosAPI struct {

db *sql.DB

tracer trace.Tracer

meter metric.Meter

}

func NewPedidosAPI(db sql.DB) PedidosAPI {

return &amp;PedidosAPI{

db: db,

tracer: otel.Tracer(&quot;pedidos-api&quot;),

meter: otel.Meter(&quot;pedidos-api&quot;),

}

}

func (api PedidosAPI) GetPedido(w http.ResponseWriter, r http.Request) {

ctx := r.Context()

pedidoID := r.URL.Query().Get(&quot;id&quot;)

ctx, span := api.tracer.Start(ctx, &quot;get-pedido&quot;)

defer span.End()

span.SetAttributes(

semconv.DBOperationKey.String(&quot;SELECT&quot;),

semconv.DBStatementKey.String(&quot;SELECT * FROM pedidos WHERE id = ?&quot;),

)

// Busca no banco

var pedido string

if err := api.db.QueryRowContext(ctx, &quot;SELECT dados FROM pedidos WHERE id = $1&quot;, pedidoID).Scan(&amp;pedido); err != nil {

span.RecordError(err)

span.SetStatus(codes.Error, err.Error())

http.Error(w, &quot;Pedido não encontrado&quot;, http.StatusNotFound)

return

}

w.Header().Set(&quot;Content-Type&quot;, &quot;application/json&quot;)

fmt.Fprintf(w, {&quot;id&quot;: &quot;%s&quot;, &quot;dados&quot;: &quot;%s&quot;}, pedidoID, pedido)

}

func main() {

tp, mp, err := initTelemetry()

if err != nil {

log.Fatal(err)

}

defer tp.Shutdown(context.Background())

defer mp.Shutdown(context.Background())

// Registrar driver de banco com instrumentação automática

driverName, err := otelsql.Register(&quot;postgres&quot;)

if err != nil {

log.Fatal(err)

}

// Conectar ao banco

db, err := sql.Open(driverName, &quot;postgres://user:password@localhost/pedidos&quot;)

if err != nil {

log.Fatal(err)

}

defer db.Close()

api := NewPedidosAPI(db)

// Usar cliente HTTP com instrumentação automática

mux := http.NewServeMux()

mux.HandleFunc(&quot;/pedido&quot;, api.GetPedido)

wrappedMux := otelhttp.NewHandler(mux, &quot;pedidos-api&quot;)

fmt.Println(&quot;API rodando em http://localhost:8080&quot;)

fmt.Println(&quot;Métricas disponíveis em http://localhost:9090/metrics&quot;)

// Metrics endpoint

go http.ListenAndServe(&quot;:9090&quot;, http.NewServeMux())

http.ListenAndServe(&quot;:8080&quot;, wrappedMux)

}</code></pre>

<p>Este exemplo mostra como tudo se integra: HTTP com rastreamento automático, banco de dados com spans automáticos, e métricas customizadas. Uma requisição chegando nessa API geraria um trace completo, desde o handler HTTP até a query do banco, com todas as latências registradas.</p>

<h2>Conclusão</h2>

<p>Os três pontos principais que você deve levar desta aula são: primeiro, <strong>OpenTelemetry é um padrão, não um produto</strong>. Você instrui uma vez e consegue trocar backends sem tocar no código. Segundo, <strong>traces distribuídos funcionam através de propagação de contexto</strong>: o trace ID viaja nos cabeçalhos HTTP, conectando requisições através de múltiplos serviços. Terceiro, <strong>métricas são mais eficientes que logs para monitoramento contínuo</strong>, mas traces são essenciais para entender o que exatamente deu errado em uma requisição específica.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://opentelemetry.io/docs/" target="_blank" rel="noopener noreferrer">OpenTelemetry Official Documentation</a></li>

<li><a href="https://github.com/open-telemetry/opentelemetry-go" target="_blank" rel="noopener noreferrer">OpenTelemetry Go SDK Repository</a></li>

<li><a href="https://www.jaegertracing.io/docs/" target="_blank" rel="noopener noreferrer">Jaeger Tracing Documentation</a></li>

<li><a href="https://www.oreilly.com/library/view/observability-engineering/9781492076438/" target="_blank" rel="noopener noreferrer">Observability Engineering by Charity Majors, Liz Fong-Jones, and George Miranda</a></li>

<li><a href="https://www.w3.org/TR/trace-context/" target="_blank" rel="noopener noreferrer">W3C Trace Context Specification</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em Go

Guia Completo de Gin em Go: Framework Web de Alta Performance na Prática
Guia Completo de Gin em Go: Framework Web de Alta Performance na Prática

O que é Gin e Por Que Escolher Este Framework Gin é um framework web escrito...

Guia Completo de Context em Go: Cancelamento, Timeout e Propagação de Valores
Guia Completo de Context em Go: Cancelamento, Timeout e Propagação de Valores

Context em Go: Cancelamento, Timeout e Propagação de Valores O é um dos pacot...

O que Todo Dev Deve Saber sobre PostgreSQL com Go: pgx Driver, Transactions e Connection Pool
O que Todo Dev Deve Saber sobre PostgreSQL com Go: pgx Driver, Transactions e Connection Pool

Introdução ao pgx: Por que ele é a melhor escolha para Go Quando você começar...