Strukturalne logowanie w Go przy użyciu slog w celu zapewnienia obserwowalności i powiadomień.
Zapytane logi JSON powiązane ze śladami.
Dzienniki (logi) to interfejs debugowania, który możesz nadal używać, gdy system jest w ogniu. Problem polega na tym, że zwykłe dzienniki tekstowe szybko się starzeją: gdy tylko potrzebujesz filtrowania, agregacji i powiadamiania, zaczynasz parsować zdania.

Strukturalne logowanie to antidotum. Zmienia każdą linię logu w małe zdarzenie ze stabilnymi polami, dzięki czemu narzędzia mogą wyszukiwać i agregować dane w sposób niezawodny. Aby dowiedzieć się, jak logi łączą się z metrykami, dashboardami i powiadamianiami w szerszym stosie technologicznym, zobacz Przewodnik po obserwowalności: monitoring, metryki, Prometheus i Grafana.
Czym jest logowanie strukturalne i dlaczego się skaluje
Logowanie strukturalne to logowanie, w którym rekord to nie tylko ciąg znaków, ale wiadomość wraz z typowanymi atrybutami klucz-wartość. Pomysł jest nudny w najlepszym możliwym sensie: gdy logi są maszynowo czytelne, incydent przestaje być konkursem grepem.
Szybkie porównanie:
Zwykły tekst (priorytetem jest człowiek, nieprzyjazny narzędziom)
nie udało się naliczyć karty user=42 amount=19.99 ms=842 err=timeout
Strukturalne (priorytetem są narzędzia, nadal czytelne)
{"msg":"nie udało się naliczyć karty","user_id":42,"amount":19.99,"duration_ms":842,"error":"timeout"}
W środowisku produkcyjnym pomaga traktowanie logów jako strumienia zdarzeń emitowanych przez proces, podczas gdy routowanie i przechowywanie znajdują się poza aplikacją. Ten model mentalny popycha w stronę zapisywania jednego zdarzenia na linię i utrzymywania zdarzeń łatwych do przesyłania i ponownego przetwarzania.
Slog w Go jako wspólny front-end logowania
Go posiada klasyczną pakiet log od zawsze, ale nowoczesne usługi potrzebują poziomów i pól. Pakiet log/slog (Go 1.21 i nowsze) wprowadza logowanie strukturalne do biblioteki standardowej i formalizuje wspólny kształt rekordów logów: czas, poziom, wiadomość i atrybuty. Aby uzyskać zwięzły refresher języka i komend obok tego przewodnika, zobacz Ściągawkę z Go.
Kluczowe części 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 sprawiają, że logi są zapytane. Jeśli zapisujesz to samo pojęcie pod trzema różnymi kluczami (user, userId, uid), otrzymujesz trzy różne zbiory danych. Spójne klucze to miejsce, gdzie kryje się prawdziwa wartość.
Obsługa (Handler)
Obsługa (Handler) to sposób, w jaki rekordy stają się bajtami. Wbudowany TextHandler zapisuje wyjście w formacie klucz=wartość, podczas gdy JSONHandler zapisuje JSON rozdzielany liniami. Obsługi to również miejsce, gdzie zazwyczaj następuje cenzurowanie, zmienianie nazw kluczy i kierowanie wyjścia.
Jedną z niedocenianych funkcji jest możliwość umieszczenia slog przed istniejącym kodem. Kiedy ustawisz domyślny logger slog, funkcje slog na najwyższym poziomie używają go, a klasyczny pakiet log może być również przekierowany do niego. To umożliwia migrację inkrementalną.
Grupy
Grupy rozwiązują problem “każdy podsystem używa id”. Możesz zgrupować zestaw atrybutów dla żądania (request.method, request.path) lub nazwać cały podsystem za pomocą WithGroup, aby klucze nie kolidowały.
Konfiguracja slog kształtowana przez produkcję
Poniższa konfiguracja osiąga typowe cele.
Przykłady używają małego pakietu logx; aby dowiedzieć się, gdzie takie pakiety zazwyczaj znajdują się w prawdziwym module, zobacz Struktura projektu Go: praktyki i wzorce.
- jedno zdarzenie JSON na linię
- logi zapisywane do stdout do zbierania
- stabilne metadane usługi dołączane raz
- logowanie zależne od kontekstu dla identyfikatorów żądań i śledzenia
- centralne cenzurowanie 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, // uwzględnij plik i linię, gdy dostępne
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Centralne cenzurowanie: spójne i trudne do obejścia przez pomyłkę.
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 obsługa 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 normalizować klucze bez przepisywania miejsc wywołań.
Schemat jest ważniejszy niż logger
Większość niepowodzeń “logowania strukturalnego” to niepowodzenia schematu.
Niezbędne pola, które wciąż są płatne
Każdy backend logów będzie przechowywał znacznik czasu, poziom i wiadomość. W praktyce użyteczny schemat aplikacji często dodaje mały zestaw stabilnych pól:
- service, env, version
- component (lub subsystem)
- event (stabilna nazwa tego, co się stało)
- request_id (gdy istnieje żądanie)
- trace_id i span_id (gdy istnieje śledzenie)
- error (ciąg) i error_kind (stabilna kategoria)
Zauważ wzorzec: te pola odpowiadają na pytania operacyjne, nie ciekawość programisty.
Semantyczne konwencje to tanie hacki dla spójności
Jeśli już używasz OpenTelemetry, jego semantyczne konwencje zapewniają standardowy słownik atrybutów dla sygnałów telemetrii. Nawet jeśli nie eksportujesz logów przez OpenTelemetry, pożyczanie nazw atrybutów zmniejsza podatek “jak nazwaliśmy to pole w usłudze B”.
Wysoka kardynalność i dlaczego logi stają się drogie
Wysoka kardynalność oznacza “za dużo unikalnych wartości”. Jest to w porządku wewnątrz płata JSON, ale staje się bolesne, gdy backend traktuje niektóre pola jako indeksowane etykiety lub klucze strumienia. Identyfikatory użytkowników, adresy IP, losowe tokeny żądań i pełne URLs mają tendencję do wybuchania kombinacjami.
Praktyczny wynik jest prosty: utrzymuj etykiety i klucze indeksowania nudne (usługa, środowisko, region), a pola o wysokiej kardynalności zachowuj wewnątrz strukturowanego płata do filtrowania w czasie zapytania.
Korelacja z identyfikatorami żądań i śladami
Korelacja to punkt, w którym logi przestają być tylko tekstem i zaczynają zachowywać się jak telemetria.
Request ID jako klucz korelacji o najmniejszym tarcieniu
Request ID to najprostszy most między przychodzącym żądaniem a wszystkim, co się dzieje w jego wyniku. Zazwyczaj działa nawet bez rozproszonego śledzenia, i nadal jest użyteczny, gdy ślady są próbkowane.
Powszechnym wzorcem jest dołączanie loggera per-request 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 śladu z W3C Trace Context i OpenTelemetry
W3C Trace Context definiuje standardowy sposób propagowania tożsamości śladu (dla HTTP, poprzez traceparent i tracestate). OpenTelemetry buduje na tym, aby identyfikatory śladu i spany można było wyodrębnić z kontekstu.
Ten przykład middlewareu loguje zarówno request_id, jak i identyfikatory śladu, 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 pola korelacji istnieją, linia logu staje się indeksem do innych danych. Różnica w żywym incydencie nie jest subtelna.
Przekształcanie strukturalnych logów w sygnały monitoringu i powiadamiania
Logi świetnie odpowiadają na pytanie “co się stało”. Powiadamianie zazwyczaj dotyczy “jak często i jak źle”.
Praktyczne podejście to traktowanie pewnych zdarzeń logów jako liczników:
- event=payment_failed
- event=db_timeout
- event=cache_miss
Wiele platform może wyprowadzać metryki oparte na logach, licząc pasujące rekordy w oknie czasowym. Strukturalne logi sprawiają, że to liczenie jest odporne, ponieważ opiera się na wartości pola, a nie na kruchym dopasowaniu tekstu. Kiedy jesteś gotowy wizualizować i badać te sygnały, Instalacja i użycie Grafany na Ubuntu: Kompletny przewodnik prowadzi przez pełną konfigurację Grafany, którą możesz skierować do wspólnych backendów logów i metryk.
To też jest miejsce, gdzie poziomy logów zaczynają mieć znaczenie. Logi debugowania są często wartościowe, ale to tam ukrywa się koszt i szum. Używanie dynamicznego poziomu (LevelVar) pozwala systemowi pozostać cichym domyślnie, jednocześnie pozwalając na celowe szczegóły gdy są potrzebne.
Myśli końcowe
Strukturalne logowanie w Go to już nie debata bibliotek. Ciekawą częścią jest to, czy Twoje rekordy logów są spójne, korelowalne i tanie do przechowywania.
Gdy Twoje logi przenoszą stabilne pola takie jak event, request_id i trace_id, przestają być “ciągami napisanymi przez kogoś” i zaczynają być zbiorem danych, którym możesz zarządzać.
Uwagi
Zespół Go wprowadził log/slog w Go 1.21 i podkreślił, że strukturalne logi używają par klucz-wartość, więc mogą być parsowane, filtrowane, wyszukiwane i analizowane niezawodnie, a także zauważył motywację zapewnienia wspólnego frameworku udostępnionego w ekosystemie.
Dokumentacja pakietu log/slog definiuje model rekordu (czas, poziom, wiadomość, pary klucz-wartość) i wbudowane obsłużyciele (TextHandler dla klucz=wartość i JSONHandler dla JSON rozdzielonego 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 odsłania TraceId i SpanId, czyniąc korelację log-ślad proste, gdy span jest obecny.
Dla kosztu przechowywania logów i wydajności, dokumentacja Grafana Loki silnie zaleca ograniczone, statyczne etykiety i ostrzega przed etykietami o wysokiej kardynalności tworzącymi zbyt wiele strumieni i ogromny indeks, co jest bezpośrednio istotne przy decydowaniu, co staje się etykietą, a co pozostaje nieindeksowanym polem JSON.