~/posts/aula-observability-go.md
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.