Observability in Go services is geen luxe meer, maar een harde eis voor serieuze productie-applicaties. Zonder inzicht in wat je service doet, blijf je in het duister tasten zodra er iets misgaat. En geloof me: er gaat altijd iets mis.
In deze gids leer je hoe je observability praktisch opzet in Go. Je ontdekt de drie pijlers, logs, metrics en traces, en hoe je ze samenbrengt met OpenTelemetry, Prometheus en Grafana. Met concrete codevoorbeelden die je direct in je eigen services kunt toepassen.
Wat is observability eigenlijk?
Observability komt uit de controletheorie en betekent dat je het interne gedrag van een systeem kunt afleiden uit zijn externe signalen. In softwaretermen: je kunt begrijpen wat je service doet zonder een debugger aan te hoeven hangen.
Het verschil met klassieke monitoring is belangrijk. Monitoring vertelt je dat iets stuk is. Observability helpt je uitzoeken waarom. Bij een nieuwe, onverwachte bug is dat onderscheid cruciaal.
De drie pijlers die je kennen moet:
- Logs, tekstuele gebeurtenissen met context, bij voorkeur gestructureerd
- Metrics, numerieke tijdseries die trends en drempels meten
- Traces, de volledige weg van een request door je systeem
Deze drie vullen elkaar aan. Een metric toont een spike, een trace laat zien welke service traag is, en logs vertellen wat er precies misging.
De basis: structured logging met slog
Logs zijn het startpunt van elke observability-strategie. Sinds Go 1.21 heb je log/slog in de standaard library, en die moet je gebruiken. Geen externe dependency, uitstekende performance en JSON-output out of the box.
package main
import (
"log/slog"
"os"
)
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Info("service gestart",
"port", 8080,
"env", "production",
)
}
Het voordeel van JSON-logs is dat tools zoals Loki, Elasticsearch of Datadog velden direct kunnen indexeren. Zoeken op user_id=42 of filteren op level=error werkt dan out of the box.
Wil je dieper ingaan op de basis? Lees dan eerst de gids over logging en observability in Go. Deze post bouwt daar op voort met focus op metrics en distributed tracing.
Context toevoegen aan je logs
Logs zonder context zijn waardeloos. Voeg altijd een trace_id, request_id en relevante business-identifiers toe. Met slog.With() maak je een sublogger per request:
func handleRequest(w http.ResponseWriter, r *http.Request) {
logger := slog.With(
"request_id", r.Header.Get("X-Request-ID"),
"path", r.URL.Path,
"method", r.Method,
)
logger.Info("request ontvangen")
// verdere verwerking...
}
Combineer dit met het context package om de logger door je hele call-chain te dragen zonder elke functie-signature te vervuilen.
Metrics met Prometheus
Metrics zijn de ruggengraat van je monitoring. Ze zijn goedkoop te produceren, makkelijk te aggregeren en perfect voor dashboards en alerts. In de Go-wereld is Prometheus de de-facto standaard.
Installeer de client library:
go get github.com/prometheus/client_golang/prometheus
go get github.com/prometheus/client_golang/prometheus/promhttp
Een typische setup met een request counter en een latency histogram:
package main
import (
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
httpRequests = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "Totaal aantal HTTP requests",
},
[]string{"method", "path", "status"},
)
httpDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "Duur van HTTP requests",
Buckets: prometheus.DefBuckets,
},
[]string{"method", "path"},
)
)
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &statusRecorder{ResponseWriter: w, status: 200}
next.ServeHTTP(rw, r)
duration := time.Since(start).Seconds()
httpRequests.WithLabelValues(r.Method, r.URL.Path, http.StatusText(rw.status)).Inc()
httpDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
})
}
Expose vervolgens /metrics met promhttp.Handler() en Prometheus kan je service scrapen. Zie de officiële Prometheus Go-documentatie voor meer patronen.
De vier golden signals
Google's SRE-boek definieert vier metrics die je áltijd moet meten:
- Latency, hoe lang duren succesvolle en gefaalde requests?
- Traffic, hoeveel requests per seconde verwerk je?
- Errors, welk percentage van requests faalt?
- Saturation, hoe vol zit je systeem? CPU, memory, worker pools.
Voor Go voeg je daar runtime metrics aan toe: goroutine count, GC pauses en heap size. Het collectors-pakket van Prometheus doet dit automatisch:
prometheus.MustRegister(collectors.NewGoCollector())
prometheus.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}))
Distributed tracing met OpenTelemetry
Bij microservices is één request vaak verspreid over tien of meer services. Logs en metrics vertellen je dát iets traag is, maar niet waar. Daar komen distributed traces binnen.
OpenTelemetry is de vendor-neutrale standaard en heeft uitstekende Go-support. Je instrumenteert één keer en exporteert naar Jaeger, Tempo, Honeycomb of wat je maar wilt.
Setup:
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
exporter, err := otlptracegrpc.New(ctx)
if err != nil {
return nil, err
}
res, _ := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName("order-service"),
semconv.ServiceVersion("1.4.2"),
),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.TraceIDRatioBased(0.1)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
De sampler op 10 procent houdt overhead laag. Voor kritieke paden kun je tail-based sampling toepassen via de OpenTelemetry Collector.
Spans maken in je handlers
Een trace bestaat uit spans. Elke meaningful operatie krijgt een eigen span:
func (s *OrderService) CreateOrder(ctx context.Context, req OrderRequest) error {
ctx, span := otel.Tracer("order-service").Start(ctx, "CreateOrder")
defer span.End()
span.SetAttributes(
attribute.String("user.id", req.UserID),
attribute.Int("items.count", len(req.Items)),
)
if err := s.validate(ctx, req); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, "validatie gefaald")
return err
}
return s.persist(ctx, req)
}
Voor meer diepte in context-propagatie en microservices in Go vind je uitgebreide voorbeelden met gRPC en HTTP trace-headers.
Alles samenbrengen: correlation
Observability wordt pas krachtig als je tussen de pijlers kunt springen. Je ziet een latency-spike in Grafana, klikt door naar een voorbeeld-trace, en vandaar naar de logs van die exacte request.
De sleutel is een gedeelde trace_id. Trek hem uit de span-context en voeg hem toe aan elke logregel:
func loggerFromContext(ctx context.Context) *slog.Logger {
span := trace.SpanFromContext(ctx)
if !span.SpanContext().IsValid() {
return slog.Default()
}
return slog.With(
"trace_id", span.SpanContext().TraceID().String(),
"span_id", span.SpanContext().SpanID().String(),
)
}
In Grafana met Loki en Tempo werkt dit als magie: je klikt op een trace_id in je logs en krijgt de bijbehorende trace te zien. Dat is observability in actie.
Runtime health checks
Naast telemetrie wil je dat Kubernetes of je load balancer weet of je service gezond is. Twee endpoints zijn standaard:
/healthz, liveness: leeft het proces nog?/readyz, readiness: kan ik verkeer aan?
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
if err := db.PingContext(r.Context()); err != nil {
http.Error(w, "database niet bereikbaar", http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusOK)
})
Meer diepgang over deze patronen vind je in Go in Kubernetes: patterns en valkuilen.
Profiling integreren
Voor diepe performance-analyse voeg je pprof toe. Het is een paar regels code en geeft je CPU-, memory- en goroutine-profiles op aanvraag:
import _ "net/http/pprof"
go func() {
slog.Info("pprof luistert", "addr", ":6060")
http.ListenAndServe(":6060", nil)
}()
Zorg dat deze poort niet publiek bereikbaar is, dit is een intern debugging-endpoint. Lees onze diepgaande gids over profiling met pprof voor concrete analyse-technieken.
Best practices in productie
Een paar lessen uit de praktijk die je vooraf kunt meenemen:
- Gebruik labels spaarzaam, elke unieke label-combinatie in Prometheus is een aparte tijdreeks. Gebruik geen user-IDs of request-IDs als labels, dat blaast je storage op.
- Sample traces verstandig, 100 procent sampling werkt niet op schaal. Start op 1 tot 10 procent en gebruik head-based of tail-based sampling afhankelijk van je use-case.
- Alert op symptomen, niet op oorzaken, alert op "checkout duurt langer dan 2 seconden", niet op "CPU boven 80 procent". Je gebruikers voelen symptomen.
- Houd cardinality in de gaten, hoge cardinality is de stille killer van elk observability-platform.
- Test je observability, doe chaos engineering en verifieer dat je dashboards en alerts daadwerkelijk triggeren.
Voor complete productie-setups zie production deployment van Go apps en de gids over performance tuning.
Veelgestelde vragen
Wat is observability in Go services?
Observability is het vermogen om het interne gedrag van je Go service af te leiden uit de externe signalen die de service produceert. De drie pijlers zijn logs, metrics en traces, die samen een compleet beeld geven van wat je applicatie doet in productie.
Wat is het verschil tussen monitoring en observability?
Monitoring vertelt je of iets kapot is op basis van vooraf gedefinieerde dashboards en alerts. Observability laat je onderzoeken waarom iets kapot is, ook voor problemen die je niet van tevoren had voorzien, door gedetailleerde telemetrie te combineren.
Waarom OpenTelemetry gebruiken in Go?
OpenTelemetry is de industriestandaard voor telemetrie en werkt vendor-neutraal. Je instrumenteert je Go code één keer en kunt daarna exporteren naar Prometheus, Jaeger, Tempo of commerciële backends zonder je code aan te passen.
Welke metrics moet ik minimaal meten in een Go service?
Focus op de vier golden signals: latency, traffic, errors en saturation. Voor Go services voeg je daar runtime metrics aan toe zoals goroutine count, memory allocations en GC pauses.
Hoeveel overhead voegt observability toe?
Met goede sampling en efficiënte exporters blijft de overhead meestal onder de 5 procent. Traces kun je samplen op 1 tot 10 procent, metrics zijn zeer goedkoop, en structured logging met slog kost weinig CPU.