Aula

Aula: observability em Go do zero — traces, metrics e logs em 1 hora

OpenTelemetry, Prometheus, slog estruturado. Setup completo que você pode replicar no próximo serviço.

Esta aula é um passo-a-passo prático: pegamos uma API Go básica e instrumentamos os três pilares de observability (logs, metrics, traces) em 60 minutos. No final, você tem um serviço pronto para produção com visibilidade completa.

O serviço base (5 minutos)

package main

import (
    "net/http"
    "github.com/go-chi/chi/v5"
)

func main() {
    r := chi.NewRouter()
    r.Get("/users/{id}", getUser)
    http.ListenAndServe(":8080", r)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    // ... lógica de fetch user
    w.Write([]byte(`{"id":"` + id + `","name":"Maria"}`))
}

Funcional, mas cego. Vamos instrumentar.

Pilar 1: logs estruturados com slog (10 min)

A stdlib Go ganhou log/slog em 1.21 — não precisa mais de Zap, Logrus, etc.

import (
    "log/slog"
    "os"
)

func init() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
        Level: slog.LevelInfo,
    }))
    slog.SetDefault(logger)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    id := chi.URLParam(r, "id")
    slog.Info("user fetch",
        "user_id", id,
        "method", r.Method,
        "path", r.URL.Path,
    )
    // ...
}

Output:

{"time":"2026-05-02T10:00:00Z","level":"INFO","msg":"user fetch","user_id":"42","method":"GET","path":"/users/42"}

Já consumível por CloudWatch, Loki, Datadog sem parser custom.

Pilar 2: metrics com Prometheus (15 min)

import "github.com/prometheus/client_golang/prometheus/promhttp"
import "github.com/prometheus/client_golang/prometheus"

var (
    requestsTotal = prometheus.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total de requests HTTP",
        },
        []string{"method", "endpoint", "status"},
    )
    requestDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "http_request_duration_seconds",
            Buckets: []float64{0.01, 0.05, 0.1, 0.3, 1, 3, 10},
        },
        []string{"endpoint"},
    )
)

func init() {
    prometheus.MustRegister(requestsTotal, requestDuration)
}

// Middleware
func metricsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        ww := &statusWriter{ResponseWriter: w, status: 200}
        next.ServeHTTP(ww, r)
        requestsTotal.WithLabelValues(r.Method, r.URL.Path, fmt.Sprintf("%d", ww.status)).Inc()
        requestDuration.WithLabelValues(r.URL.Path).Observe(time.Since(start).Seconds())
    })
}

// main()
r.Use(metricsMiddleware)
r.Handle("/metrics", promhttp.Handler())

Agora curl :8080/metrics retorna formato Prometheus pronto pra scrape.

Pilar 3: traces com OpenTelemetry (25 min)

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    "go.opentelemetry.io/otel/sdk/resource"
    semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
    exporter, err := otlptracegrpc.New(ctx,
        otlptracegrpc.WithEndpoint("localhost:4317"),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil { return nil, err }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceName("users-api"),
        )),
    )
    otel.SetTracerProvider(tp)
    return tp, nil
}

func main() {
    ctx := context.Background()
    tp, _ := initTracer(ctx)
    defer tp.Shutdown(ctx)

    r := chi.NewRouter()
    r.Use(metricsMiddleware)
    r.Method("GET", "/users/{id}",
        otelhttp.NewHandler(http.HandlerFunc(getUser), "users.get"),
    )
    http.ListenAndServe(":8080", r)
}

func getUser(w http.ResponseWriter, r *http.Request) {
    ctx, span := otel.Tracer("users").Start(r.Context(), "fetch_from_db")
    defer span.End()
    // ... fetch user, e cada query passa ctx pra trazer no trace
}

Spans aparecem em qualquer backend OTel: Jaeger, Tempo, Datadog APM, Honeycomb.

Stack docker-compose pra testar localmente

services:
  prometheus:
    image: prom/prometheus
    ports: ["9090:9090"]
    volumes: ["./prometheus.yml:/etc/prometheus/prometheus.yml"]

  grafana:
    image: grafana/grafana
    ports: ["3000:3000"]

  tempo:
    image: grafana/tempo
    command: ["-config.file=/etc/tempo.yaml"]
    ports: ["4317:4317"]

Em 1 hora você tem:

  • Dashboard de RPS, latência, erro por endpoint
  • Distributed tracing entre serviços
  • Logs estruturados queryable

Custo operacional

  • Self-hosted (lab): 1 VM 4GB roda Prometheus + Grafana + Loki + Tempo sem suar.
  • Managed: Grafana Cloud free tier (10k metrics series, 50GB logs, 50GB traces) cobre serviços médios.
  • Datadog: caro mas turn-key. Considere para times sem SRE.

Conclusão

Observability não é luxo. É o que te avisa antes do usuário reclamar. Em Go, com a stack acima, o custo de instrumentar é ~200 linhas pra um serviço médio — e o ganho é noites dormindo durante incidentes.