Strukturalne logowanie w Go przy użyciu slog w celu zapewnienia obserwowalności i powiadomień.

Zapytane logi JSON powiązane ze śladami.

Page content

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.

miejsce pracy z laptopem i maskotkami Go dla lepszego logowania

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.