Strukturalne logowanie w Go z wykorzystaniem slog w celu zwiększenia obserwowalności i alertów
Zapytania do dzienników JSON połączone ze śladami.
Dzienniki (logs) to interfejs debugowania, z którego możesz korzystać nawet wtedy, gdy system „pali się”. Problem polega na tym, że zwykłe dzienniki tekstowe szybko tracą przydatność: gdy tylko potrzebujesz filtrowania, agregacji i alerty, zaczynasz parsować zdania.

Logowanie strukturalne jest na to antidotum. Przekształca każdą linię logu w małe zdarzenie ze stabilnymi polami, dzięki czemu narzędzia mogą niezawodnie wyszukiwać i agregować dane. Aby dowiedzieć się, jak logi łączą się z metrykami, dashboardami i alertami w szerszym stosie technologicznym, zobacz Przewodnik: Obserwowalność: Monitorowanie, Metryki, Prometheus & Grafana.
Co to jest logowanie strukturalne i dlaczego jest skalowalne
Logowanie strukturalne to logowanie, w którym rekord to nie tylko łańcuch znaków, ale wiadomość oraz typowane atrybuty klucz-wartość. Pomysł jest nudny w najlepszym możliwym znaczeniu: gdy logi są czytelne maszynowo, incydent przestaje być konkursem w użyciu grep.
Krótka porównanie:
Tekst prosty (priorytet dla człowieka, nieprzyjazny dla narzędzi)
failed to charge card user=42 amount=19.99 ms=842 err=timeout
Strukturalny (priorytet dla narzędzi, nadal czytelny)
{"msg":"failed to charge card","user_id":42,"amount":19.99,"duration_ms":842,"error":"timeout"}
W środowisku produkcyjnym pomaga myśleć o logach jako o strumieniu zdarzeń emitowanym przez proces, podczas gdy routing i przechowywanie znajdują się poza aplikacją. Ten model mentalny skłania do zapisywania jednego zdarzenia w linii i utrzymywania zdarzeń w formie łatwej do przesyłania i ponownego przetwarzania.
Slog w Go jako wspólny interfejs logowania
Go ma klasyczny pakiet log od wieków, ale nowoczesne usługi potrzebują poziomów i pól. Pakiet log/slog (Go 1.21 i nowszy) wprowadza strukturalne logowanie do biblioteki standardowej i formalizuje wspólny kształt rekordów logów: czas, poziom, wiadomość i atrybuty. Aby odświeżyć wiedzę o języku i poleceniach obok tego przewodnika, zobacz Ściągawkę Go.
Kluczowe elementy modelu to:
Rekord
Rekord to to, co się stało. W terminologii slog zawiera czas, poziom, wiadomość i zestaw atrybutów. Tworzysz rekordy za pomocą metod takich jak Info i Error, lub za pomocą Log, gdy chcesz podać poziom jawnie.
Atrybuty
Atrybuty to pary klucz-wartość, które umożliwiają zapytania do logów. Jeśli będziesz logować ten sam koncept pod trzema różnymi kluczami (user, userId, uid), otrzymasz trzy różne zbiory danych. Warto ukryta w spójnych kluczach.
Handler
Handler to sposób, w jaki rekordy stają się bajtami. Wbudowany TextHandler zapisuje wyjście w formacie klucz=wartość, podczas gdy JSONHandler zapisuje JSON oddzielony liniami. Handlerzy to również miejsce, w którym zwykle zachodzi redagowanie, zmienianie nazw kluczy i routing wyjścia.
Jedną z niedocenianych funkcji jest możliwość umieszczenia slog przed istniejącym kodem. Gdy ustawisz domyślny logger slog, funkcje najwyższego poziomu slog go używają, a klasyczny pakiet log może być również do niego przekierowany. Umożliwia to migrację przyrostową.
Grupy
Grupy rozwiązują problem „każdy podsystem używa id”. Możesz zgrupować zestaw atrybutów dla żądania (request.method, request.path) lub nadać namespace całemu podsystemowi za pomocą WithGroup, aby klucze nie kolidowały.
Konfiguracja slog gotowa na produkcję
Poniższa konfiguracja osiąga typowe cele.
Przykłady używają małego pakietu logx; aby zobaczyć, gdzie takie pakiety zwykle mieszczą się w rzeczywistym module, zobacz Struktura projektu Go: Praktyki i wzorce.
- jeden zdarzenie JSON na linię
- logi zapisywane do stdout do zbierania
- stabilne metadane usługi dołączone raz
- logowanie z uwzględnieniem kontekstu dla identyfikatorów żądań i śladów
- centralne redagowanie dla wrażliwych kluczy
package logx
import (
"log/slog"
"os"
)
var level slog.LevelVar // domyślnie INFO
func New() *slog.Logger {
opts := &slog.HandlerOptions{
Level: &level, // można zmienić w czasie działania
AddSource: true, // dołączaj plik i linię, gdy są dostępne
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Centralizowane redagowanie: spójne i trudne do ominięcia przez przypadek.
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) }
Mały szczegół o dużych konsekwencjach: wbudowany handler JSON używa standardowych kluczy (time, level, msg, source). Gdy Twoje tło logów oczekuje innego schematu, ReplaceAttr jest zaworem bezpieczeństwa, który pozwala znormalizować klucze bez przepisywania miejsc wywołań.
Schemat ma większe znaczenie niż logger
Większość porażek „strukturalnego logowania” to porażki schematu.
Niezbędne pola, które ciągle się opłacają
Każde tło logów będzie przechowywać znacznik czasu, poziom i wiadomość. W praktyce przydatny schemat aplikacji często dodaje mały zestaw stabilnych pól:
- service, env, version
- component (lub subsystem)
- event (stabilna nazwa zdarzenia, które się wydarzyło)
- request_id (gdy istnieje żądanie)
- trace_id i span_id (gdy istnieje śledzenie)
- error (string) i error_kind (stabilny zasobnik)
Zauważ wzorzec: te pola odpowiadają na pytania operacyjne, a nie na ciekawość programistów.
Semantyczne konwencje to tani trik na konsystencję
Jeśli już używasz OpenTelemetry, jego konwencje semantyczne dostarczają standardowego słowniczka atrybutów dla sygnałów telemetrii. Nawet jeśli nie eksportujesz logów przez OpenTelemetry, pożyczanie nazw atrybutów redukuje podatek „jak nazwaliśmy to pole w usłudze B”.
Wysoka kardynalność i dlaczego logi drożeją
Wysoka kardynalność oznacza „za dużo unikalnych wartości”. Jest w porządku wewnątrz ładunku JSON, ale staje się bolesna, gdy tło traktuje niektóre pola jako indeksowane etykiety lub klucze strumieni. Identyfikatory użytkowników, adresy IP, losowe tokeny żądań i pełne adresy URL mają tendencję do eksplodowania kombinacji.
Praktyczny wynik jest prosty: utrzymuj etykiety i klucze indeksu nudnymi (service, environment, region), a utrzymuj pola o wysokiej kardynalności wewnątrz strukturalnego ładunku do filtrowania w czasie zapytania.
Korelacja z identyfikatorami żądań i śladami
Korelacja to moment, w którym logi przestają być tylko tekstem i zaczynają zachowywać się jak telemetria.
Request ID jako klucz korelacji o najniższym tarcie
Request ID to najprostszy most między przychodzącym żądaniem a wszystkim, co się dzieje w jego wyniku. Działa nawet bez rozproszonego śledzenia, i jest nadal przydatny, gdy ślady są próbkowane. Aby uzyskać pełny obraz tego, jak identyfikatory żądań i inne metadane powinny być przechowywane i pobierane z kontekstu — w tym wzorce typowanych kluczy i przykłady middleware — zobacz Go context.Context Done Right.
Częstym wzorcem jest dołączanie loggera per-żądanie do kontekstu:
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()
}
Korelacja śladów z W3C Trace Context i OpenTelemetry
W3C Trace Context definiuje standardowy sposób propagacji tożsamości śladu (dla HTTP, przez traceparent i tracestate). OpenTelemetry buduje na tym, aby identyfikatory trace ID i span ID można było wyodrębnić z kontekstu.
To przykładowe middleware loguje zarówno request_id, jak i identyfikatory śladów, gdy są dostępne:
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))
})
}
}
Gdy istnieją pola korelacyjne, linia logu staje się indeksem do innych danych. Różnica w żywym incydencie nie jest subtelna.
Przekształcanie strukturalnych logów w sygnały monitorowania i alertów
Logi świetnie odpowiadają na pytanie „co się stało”. Alerty zwykle dotyczą „jak często i jak bardzo”.
Praktyczne podejście polega na traktowaniu określonych zdarzeń logów jako liczników:
- event=payment_failed
- event=db_timeout
- event=cache_miss
Wiele platform może wyprowadzać metryki oparte na logach poprzez zliczanie pasujących rekordów w oknie czasowym. Strukturalne logy czynią to zliczanie odpornym, ponieważ opiera się ono na wartości pola, a nie na kruchej porównaniu tekstu. Gdy będziesz gotowy na wizualizację i eksplorację tych sygnałów, Instalacja i używanie Grafany na Ubuntu: Kompletny przewodnik przechodzi przez pełną konfigurację Grafany, którą możesz skierować na typowe tła logów i metryk.
To również miejsce, w którym poziomy logów zaczynają mieć znaczenie. Logi debug są często cenne, ale to również miejsce, gdzie kryje się koszt i szum. Używanie dynamicznego poziomu (LevelVar) pozwalia systemowi być cichym domyślnie, jednocześnie umożliwiając szczegółowe informacje, gdy są potrzebne.
Zamykające myśli
Strukturalne logowanie w Go to już nie debata o bibliotekach. Ciekawą częścią jest to, czy Twoje rekordy logów są spójne, korelowalne i kosztowne do przechowywania.
Gdy Twoje logi niosą stabilne pola takie jak event, request_id i trace_id, przestają być „łańcuchami napisanymi przez kogoś” i zaczynają być zbiorem danych, który możesz operować.
Notatki
Zespół Go wprowadził log/slog w Go 1.21 i podkreślił, że strukturalne logi używają par klucz-wartość, aby można je było parsować, filtrować, wyszukiwać i analizować niezawodnie, oraz zaznaczył motywację dostarczenia wspólnego frameworka wspieranego przez ekosystem.
Dokumentacja pakietu log/slog definiuje model rekordu (czas, poziom, wiadomość, pary klucz-wartość) i wbudowane handlery (TextHandler dla klucz=wartość i JSONHandler dla JSON oddzielonego liniami), oraz dokumentuje integrację SetDefault z klasycznym pakietem log.
Dla rozproszonej korelacji specyfikacja W3C Trace Context standaryzuje propagację traceparent i tracestate, a OpenTelemetry określa, że jego SpanContext jest zgodny z W3C Trace Context i wystawia TraceId i SpanId, co czyni korelację log-ślad prostej, gdy span jest obecny.
Dla kosztu i wydajności przechowywania logów dokumentacja Grafana Loki mocno zaleca ograniczone, statyczne etykiety i ostrzega przed etykietami o wysokiej kardynalności tworzącymi za dużo strumieni i ogromny indeks, co jest bezpośrednio istotne przy decyzji, co staje się etykietą, a co pozostaje jako nieindeksowane pole JSON.