Strukturiertes Logging in Go mit slog für Observability und Alerting
Abfragbare JSON-Logs, die mit Spuren verknüpft sind.
Logs sind eine Debug-Schnittstelle, die Sie noch nutzen können, wenn das System brennt. Das Problem ist, dass reine Text-Logs schlecht altern: Sobald Sie Filterung, Aggregation und Alarme benötigen, beginnen Sie, Sätze zu parsen.

Strukturiertes Logging ist das Gegenmittel. Es verwandelt jede Logzeile in ein kleines Ereignis mit stabilen Feldern, sodass Tools zuverlässig suchen und aggregieren können. Wie Logs mit Metriken, Dashboards und Alarmen im weiteren Stack verbunden sind, erfahren Sie im Leitfaden zu Observability: Monitoring, Metriken, Prometheus & Grafana.
Was strukturiertes Logging ist und warum es skalierbar ist
Strukturiertes Logging bedeutet, dass ein Datensatz nicht nur eine Zeichenfolge ist, sondern eine Nachricht plus typisierte Schlüssel-Wert-Attribute. Die Idee ist auf die beste Art langweilig: Sobald Logs maschinenlesbar sind, ist ein Vorfall kein grep-Wettbewerb mehr.
Ein kurzer Vergleich:
Reiner Text (menschzentriert, tools-feindlich)
failed to charge card user=42 amount=19.99 ms=842 err=timeout
Strukturiert (tool-zentriert, aber lesbar)
{"msg":"failed to charge card","user_id":42,"amount":19.99,"duration_ms":842,"error":"timeout"}
In der Produktion hilft es, Logs als einen vom Prozess emittierten Ereignisstrom zu betrachten, während Routing und Speicherung außerhalb der Anwendung stattfinden. Dieses mentale Modell drängt Sie dazu, eine Zeile pro Ereignis zu schreiben und Ereignisse einfach zu versenden und neu zu verarbeiten.
Slog in Go als gemeinsame Logging-Frontend
Go hat seit jeher das klassische log-Paket, aber moderne Dienste benötigen Level und Felder. Das Paket log/slog (ab Go 1.21) bringt strukturiertes Logging in die Standardbibliothek und formalisiert eine gemeinsame Struktur für Log- Datensätze: Zeit, Level, Nachricht und Attribute. Für eine kompakte Sprache- und Befehlsübersicht im Zusammenhang mit diesem Leitfaden, siehe Go-Cheat-Sheet.
Die wichtigsten Teile des Modells sind:
Datensatz (Record)
Ein Datensatz ist das, was passiert ist. In Slog-Begriffen enthält er Zeit, Level, Nachricht und eine Menge von Attributen. Sie erstellen Datensätze über Methoden wie Info und Error oder über Log, wenn Sie das Level explizit angeben möchten.
Attribute
Attribute sind die Schlüssel-Wert-Paare, die Logs abfragbar machen. Wenn Sie dasselbe Konzept unter drei verschiedenen Schlüsseln (user, userId, uid) protokollieren, erhalten Sie drei verschiedene Datensätze. Konsistente Schlüssel sind, wo der eigentliche Wert liegt.
Handler
Ein Handler ist, wie Datensätze zu Bytes werden. Der integrierte TextHandler schreibt key=value-Ausgabe, während JSONHandler zeilengetrenntes JSON schreibt. Handler sind auch der Ort, an dem Redaktion, Umbenennung von Schlüsseln und Ausgaberouting stattfinden.
Eine unterschätzte Funktion ist, dass Slog vor bestehendem Code sitzen kann. Wenn Sie einen Standard-Slog-Logger festlegen, verwenden top-level Slog-Funktionen diesen, und das klassische log-Paket kann ebenfalls darauf umgeleitet werden. Das macht eine schrittweise Migration möglich.
Gruppen
Gruppen lösen das Problem, dass „jedes Subsystem id verwendet". Sie können eine Menge von Attributen für eine Anfrage gruppieren (request.method, request.path) oder ein ganzes Subsystem mit WithGroup namespace, damit Schlüssel nicht kollidieren.
Eine produktionsreife Slog-Einrichtung
Die folgende Einrichtung trifft die üblichen Ziele.
Die Beispiele verwenden ein kleines logx-Paket; wo solche Pakete in einem echten Modul normalerweise liegen, erfahren Sie unter Go-Projektstruktur: Praktiken & Muster.
- ein JSON-Ereignis pro Zeile
- Logs werden zur Sammlung in stdout geschrieben
- stabile Service-Metadaten werden einmal angehängt
- kontextbewusstes Logging für Anfrage- und Trace-IDs
- zentrale Redaktion für sensible Schlüssel
package logx
import (
"log/slog"
"os"
)
var level slog.LevelVar // Standard ist INFO
func New() *slog.Logger {
opts := &slog.HandlerOptions{
Level: &level, // kann zur Laufzeit geändert werden
AddSource: true, // Datei und Zeile einschließen, falls verfügbar
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Zentrale Redaktion: konsistent und schwer versehentlich zu umgehen.
switch a.Key {
case "password", "token", "authorization", "api_key":
return slog.String(a.Key, "[redacted]")
}
return a
},
}
h := slog.NewJSONHandler(os.Stdout, opts)
return slog.New(h).With(
"service", os.Getenv("SERVICE_NAME"),
"env", os.Getenv("ENV"),
"version", os.Getenv("VERSION"),
)
}
func SetLevel(l slog.Level) { level.Set(l) }
Ein winziges Detail mit großen Konsequenzen: Der integrierte JSON-Handler verwendet Standard- Schlüssel (time, level, msg, source). Wenn Ihr Log-Backend ein anderes Schema erwartet, ist ReplaceAttr das Druckventil, das es ermöglicht, Schlüssel zu normalisieren, ohne Aufrufstellen umzuschreiben.
Das Schema ist wichtiger als der Logger
Die meisten „Strukturiertes-Logging"-Fehler sind Schema-Fehler.
Wesentliche Felder, die ihre Miete zahlen
Jedes Log-Backend speichert einen Zeitstempel, ein Level und eine Nachricht. In der Praxis fügt ein nützliches Anwendungsschema oft eine kleine Menge stabiler Felder hinzu:
- service, env, version
- component (oder subsystem)
- event (ein stabiler Name für das, was passiert ist)
- request_id (wenn eine Anfrage existiert)
- trace_id und span_id (wenn Tracing existiert)
- error (string) und error_kind (stabiler Bucket)
Beachten Sie das Muster: Diese Felder beantworten operative Fragen, nicht die Neugier von Entwicklern.
Semantische Konventionen sind ein billiger Konsistenz-Trick
Wenn Sie bereits OpenTelemetry verwenden, bieten dessen semantische Konventionen einen Standard- Wortschatz für Attribute über Telemetriesignale hinweg. Selbst wenn Sie keine Logs über OpenTelemetry exportieren, reduziert das Leihen von Attributnamen die Steuer „wie haben wir dieses Feld in Service B genannt".
Hohe Kardinalität und warum Logs teuer werden
Hohe Kardinalität bedeutet „zu viele eindeutige Werte". Das ist innerhalb einer JSON- Last in Ordnung, wird aber schmerzhaft, wenn ein Backend einige Felder als indizierte Labels oder Stream-Schlüssel behandelt. Benutzer-IDs, IP-Adressen, zufällige Anfrage-Token und vollständige URLs neigen dazu, Kombinationen zu explodieren.
Das praktische Ergebnis ist einfach: Halten Sie Labels und Index-Schlüssel langweilig (service, environment, region) und halten Sie Felder mit hoher Kardinalität innerhalb der strukturierten Last für Filterung zur Abfragezeit.
Korrelation mit Request-IDs und Traces
Korrelation ist der Punkt, an dem Logs aufhören, nur Text zu sein, und beginnen, wie Telemetriedaten zu agieren.
Request-ID als Korrelationsschlüssel mit geringster Reibung
Eine Request-ID ist die einfachste Brücke zwischen einer eingehenden Anfrage und allem, was daraus resultiert. Sie funktioniert oft auch ohne verteiltes Tracing, und sie ist immer noch nützlich, wenn Traces gesampelt werden.
Ein häufiges Muster ist, einen pro-Anfrage-Logger an den Kontext anzuhängen:
package logx
import (
"context"
"log/slog"
)
type ctxKey struct{}
func WithLogger(ctx context.Context, l *slog.Logger) context.Context {
return context.WithValue(ctx, ctxKey{}, l)
}
func FromContext(ctx context.Context) *slog.Logger {
if l, ok := ctx.Value(ctxKey{}).(*slog.Logger); ok && l != nil {
return l
}
return slog.Default()
}
Trace-Korrelation mit W3C Trace Context und OpenTelemetry
W3C Trace Context definiert eine Standardmethode zur Weitergabe von Trace-Identität (für HTTP via traceparent und tracestate). OpenTelemetry baut darauf auf, sodass Trace-IDs und Span-IDs aus dem Kontext extrahiert werden können.
Dieses Middleware-Beispiel protokolliert sowohl request_id als auch Trace-Identifikatoren, wenn verfügbar:
package middleware
import (
"crypto/rand"
"encoding/hex"
"net/http"
"go.opentelemetry.io/otel/trace"
"log/slog"
"example.com/project/logx"
)
func requestID() string {
var b [16]byte
_, _ = rand.Read(b[:])
return hex.EncodeToString(b[:])
}
func WithRequestLogger(base *slog.Logger) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rid := r.Header.Get("X-Request-Id")
if rid == "" {
rid = requestID()
}
l := base.With(
"request_id", rid,
"method", r.Method,
"path", r.URL.Path,
)
if sc := trace.SpanContextFromContext(r.Context()); sc.IsValid() {
l = l.With(
"trace_id", sc.TraceID().String(),
"span_id", sc.SpanID().String(),
)
}
ctx := logx.WithLogger(r.Context(), l)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
Sobald Korrelationsfelder existieren, wird die Logzeile zu einem Index in andere Daten. Der Unterschied bei einem laufenden Vorfall ist nicht subtil.
Strukturierte Logs in Monitoring- und Alarmierungssignale umwandeln
Logs sind großartig darin, die Frage „was ist passiert?" zu beantworten. Alarmierung dreht sich meist um „wie oft und wie schlimm".
Ein praktischer Ansatz ist, bestimmte Log-Ereignisse als Zähler zu behandeln:
- event=payment_failed
- event=db_timeout
- event=cache_miss
Viele Plattformen können logbasierte Metriken ableiten, indem sie übereinstimmende Datensätze über ein Fenster zählen. Strukturierte Logs machen diese Zählung widerstandsfähig, da sie auf einem Feldwert und nicht auf einem brüchigen Textmatch basiert. Wenn Sie bereit sind, diese Signale zu visualisieren und zu erkunden, führt Installieren und Nutzen von Grafana auf Ubuntu: Kompletter Leitfaden Sie durch eine vollständige Grafana-Einrichtung, die Sie auf gängige Log- und Metrik-Backends ausrichten können.
Hier beginnen auch Log-Levels eine Rolle zu spielen. Debug-Logs sind oft wertvoll, aber auch dort verstecken sich Kosten und Rauschen. Die Verwendung eines dynamischen Levels (LevelVar) lässt das System standardmäßig ruhig bleiben, ermöglicht aber gezielte Details bei Bedarf.
Abschließende Gedanken
Strukturiertes Logging in Go ist keine Bibliotheksdebatte mehr. Der interessante Teil ist, ob Ihre Log-Datensätze konsistent, korrelierbar und speicherbar sind.
Wenn Ihre Logs stabile Felder wie event, request_id und trace_id tragen, hören sie auf, „Zeichenfolgen, die jemand geschrieben hat" zu sein, und werden zu einem Datensatz, mit dem Sie operieren können.
Hinweise
Das Go-Team hat log/slog in Go 1.21 eingeführt und betont, dass strukturierte Logs Schlüssel-Wert-Paare verwenden, damit sie zuverlässig geparst, gefiltert, durchsucht und analysiert werden können, und wies auch auf die Motivation hin, einen gemeinsamen Rahmen für das gesamte Ökosystem bereitzustellen.
Die Dokumentation des Pakets log/slog definiert das Record-Modell (Zeit, Level, Nachricht, Schlüssel-Wert-Paare) und die integrierten Handler (TextHandler für key=value und JSONHandler für zeilengetrenntes JSON) und dokumentiert die SetDefault-Integration mit dem klassischen log-Paket.
Für verteilte Korrelation standardisiert die W3C Trace Context-Spezifikation die Weitergabe von traceparent und tracestate, und OpenTelemetry spezifiziert, dass dessen SpanContext der W3C Trace Context-Spezifikation entspricht und TraceId und SpanId ausliefert, was die Log-Trace-Korrelation bei vorhandener Span einfach macht.
Für Log-Speicherkosten und -Leistung empfiehlt die Grafana Loki-Dokumentation dringend begrenzte, statische Labels und warnt vor Labels mit hoher Kardinalität, die zu viele Streams und einen riesigen Index erzeugen, was direkt relevant ist, wenn entschieden wird, was ein Label wird und was als nicht-indiziertes JSON-Feld bleibt.