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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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:

1
{"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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.