Strukturerad loggning i Go med slog för observabilitet och larmhantering
Frågbars JSON-loggar som kopplas till spår.
Loggar är ett felsökningsgränssnitt som du fortfarande kan använda när systemet är i brand. Problemet är att rena textloggar åldras dåligt: så fort du behöver filtrering, aggregering och alarmering börjar du parsar meningar.

Strukturerad loggning är motmedlet. Det gör varje loggrad till en liten händelse med stabila fält, så att verktyg kan söka och aggregera på ett tillförlitligt sätt. För hur loggar kopplas till metrik, instrumentpaneler och alarmering i den vidare stacken, se Observability: Monitoring, Metrics, Prometheus & Grafana Guide.
Vad strukturerad loggning är och varför det skalar
Strukturerad loggning är loggning där en post inte bara är en sträng, utan ett meddelande plus typade nyckel-värde-attribut. Idén är tråkig på bästa sätt: när loggar blir maskinläsbara slutar en incident vara en grep-tävling.
En snabb jämförelse:
Ren text (människförst, fientlig mot verktyg)
failed to charge card user=42 amount=19.99 ms=842 err=timeout
Strukturerad (verktygsförst, men fortfarande läsbar)
{"msg":"failed to charge card","user_id":42,"amount":19.99,"duration_ms":842,"error":"timeout"}
I produktion hjälper det att tänka på loggar som en händelseström som emitteras av processen, medan routning och lagring finns utanför applikationen. Den mentala modellen driver dig mot att skriva en händelse per rad och hålla händelser enkla att skicka och bearbeta om.
Slog i Go som en delad loggningsgränssnitt
Go har haft den klassiska log-paketet sedan evigheter, men moderna tjänster behöver nivåer och fält. Paketet log/slog (Go 1.21 och senare) tar in strukturerad loggning i standardbiblioteket och formaliserar en gemensam form för loggpåst: tid, nivå, meddelande och attribut. För en kompakt språk- och kommandöversikt tillsammans med denna guide, se Go Cheatsheet.
Nyckeldelarna i modellen är:
Record (Post)
En post är vad som hände. I slog-term innehåller den tid, nivå, meddelande och ett sett av attribut. Du skapar poster via metoder som Info och Error, eller via Log när du vill specificera nivån explicit.
Attribut
Attribut är nyckel-värde-par som gör loggar sökbara. Om du loggar samma koncept under tre olika nycklar (user, userId, uid) får du tre olika datamängder. Konsistenta nycklar är där det verkliga värdet gömmer sig.
Handler (Hanterare)
En hanterare är hur poster blir till byte. Den inbyggda TextHandler skriver key=value-utdata, medan JSONHandler skriver radavgränsad JSON. Hanterare är också där radering, omnamnning av nycklar och utdata-routning ofta sker.
Ett underbetalat funktion är att slog kan sitta framför befintlig kod. När du sätter en standard slog-loggare används den av topp-nivå slog-funktioner, och den klassiska log-paketet kan också omdirigeras till den. Det gör inkrementell migration möjlig.
Groups (Grupper)
Grupper löser problemet “varje under-system använder id”. Du kan gruppera ett sett av attribut för en begäran (request.method, request.path) eller namnrymna ett helt under-system med WithGroup så att nycklar inte krockar.
En produktionsinriktad slog-uppsättning
Följande uppsättning träffar vanliga mål.
Exemplen använder ett litet logx-paket; för var paket som det oftast bor i en riktig modul, se Go Project Structure: Practices & Patterns.
- en JSON-händelse per rad
- loggar skrivna till stdout för insamling
- stabil tjänst-metadata bifogad en gång
- kontextmedveten loggning för begäran och spår-ID:n
- central radering för känsliga nycklar
package logx
import (
"log/slog"
"os"
)
var level slog.LevelVar // standard är INFO
func New() *slog.Logger {
opts := &slog.HandlerOptions{
Level: &level, // kan ändras vid körning
AddSource: true, // inkludera fil och rad när tillgängligt
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Centraliserad radering: konsekvent och svårt att kringgå av misstag.
switch a.Key {
case "password", "token", "authorization", "api_key":
return slog.String(a.Key, "[raderad]")
}
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) }
En liten detalj med stora konsekvenser: den inbyggda JSON-hanteraren använder standard nycklar (time, level, msg, source). När din logg-backend förväntar sig ett annat schema är ReplaceAttr tryckventilen som låter dig normalisera nycklar utan att om skriva anropssidor.
Schema är viktigare än loggaren
De flesta “strukturerad loggning”-misslyckanden är schema-misslyckanden.
Essentiella fält som fortsätter betala hyran
Varje logg-backend kommer att lagra ett tidsstämpel, nivå och meddelande. I praktiken lägger en användbar applikationsschema ofta till ett litet sett av stabila fält:
- service, env, version
- component (eller under-system)
- event (ett stabilt namn för det som hände)
- request_id (när en begäran finns)
- trace_id och span_id (när spårning finns)
- error (sträng) och error_kind (stabilt fack)
Notera mönstret: dessa fält svarar på operativa frågor, inte utvecklar nyfikenhet.
Semantiska konventioner är ett billigt konsistenshacks
Om du redan använder OpenTelemetry, ger dess semantiska konventioner en standard vokabular för attribut över telemetrisignaler. Även om du inte exporterar loggar via OpenTelemetry, minskar lånet av attributnamnen skatten “vad kallade vi detta fält i tjänst B”.
Hög kardinalitet och varför loggar blir dyra
Hög kardinalitet betyder “för många unika värden”. Det är okej inuti en JSON payload, men det blir smärtsamt när en backend behandlar vissa fält som indexerade etiketter eller strömningsnycklar. Användar-ID:n, IP-adresser, slumpmässiga begäranstoken och fulla URL:er tenderar att explodera kombinationer.
Det praktiska resultatet är enkelt: håll etiketter och indexnycklar tråkiga (service, miljö, region), och håll fält med hög kardinalitet inuti den strukturerade payloaden för filtrering vid frågetid.
Korrelation med begärans-ID:n och spår
Korrelation är punkten där loggar slutar vara bara text och börjar bete sig som telemetri.
Begärans-ID som den mest friktionslösa korrelationsnyckeln
Ett begärans-ID är den enklaste bro mellan en inkommande begäran och allt som händer på grund av den. Det fungerar ofta även utan distribuerad spårning, och det är fortfarande användbart när spår är utvalda.
Ett vanligt mönster är att bifoga en per-begäran loggare till kontexten:
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()
}
Spårskorrelation med W3C Trace Context och OpenTelemetry
W3C Trace Context definierar ett standardiserat sätt att propagera spåridentitet (för HTTP, via traceparent och tracestate). OpenTelemetry bygger på detta så att spår-ID:n och span-ID:n kan extraheras från kontexten.
Detta mellangrensexempel loggar både request_id och spår-identifikatorer när tillgängliga:
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))
})
}
}
När korrelationsfält finns, blir logglinjen en index till andra data. Skillnaden i ett levande incident är inte subtil.
Att omvandla strukturerade loggar till övervakning och alarmeringssignaler
Loggar är bra på att svara på “vad hände”. Alarmering handlar oftast om “hur ofta och hur illa”.
Ett praktiskt tillvägagångssätt är att behandla vissa logghändelser som räknare:
- event=payment_failed
- event=db_timeout
- event=cache_miss
Många plattformar kan härleda loggbaserade metrik genom att räkna matchande poster över en tidsfönster. Strukturerade loggar gör det räknandet robust, eftersom det baseras på en fältvärde snarare än ett skört textmatchning. När du är redo att visualisera och utforska dessa signaler, Install and Use Grafana on Ubuntu: Complete Guide går igenom en fullständig Grafana-uppsättning du kan peka mot vanliga logg- och metrikbackends.
Detta är också där loggningsnivåer börjar göra skillnad. Debug-loggar är ofta värdefulla, men de är också där kostnad och brus gömmer sig. Att använda en dynamisk nivå (LevelVar) låter systemet vara tyst som standard, samtidigt som det tillåter målinriktad detalj när det behövs.
Avslutande tankar
Strukturerad loggning i Go är inte längre en biblioteksdebatt. Det intressanta är om dina loggpåst är konsekventa, korrelerbara och ekonomiska att lagra.
När dina loggar bär stabila fält som event, request_id och trace_id, slutar de vara “strängar någon skrev” och börjar vara en datamängd du kan operera på.
Noteringar
Go-teamet introducerade log/slog i Go 1.21 och betonade att strukturerade loggar använder nyckel-värde-par så att de kan parsas, filtreras, sökas och analyseras pålitligt, och noterade också motiveringen att tillhandahålla en gemensam ram som delas över ekosystemet.
Dokumentationen för log/slog-paketet definierar postmodellen (tid, nivå, meddelande, nyckel-värde-par) och de inbyggda hanterarna (TextHandler för key=value och JSONHandler för radavgränsad JSON), och dokumenterar SetDefault-integration med den klassiska log-paketet.
För distribuerad korrelation standardiserar W3C Trace Context-specifikationen traceparent och tracestate-propagering, och OpenTelemetry specificerar att dess SpanContext följer W3C Trace Context och exponerar TraceId och SpanId, vilket gör logg-spårskorrelation enkel när en span finns.
För logglagringskostnad och prestanda rekommenderar Grafana Loki-dokumentationen starkt avgränsade, statiska etiketter och varnar om etiketter med hög kardinalitet som skapar för många strömmar och en enorm index, vilket är direkt relevant när du bestämmer vad som blir en etikett kontra vad som stannar som ett oindexerat JSON-fält.