Menu

Logowanie w Golang: slog i log, logi strukturalne w Go

Jak logować w Go: klasyczny pakiet log z flagami i log.Fatal oraz log/slog (Go 1.21) do logów strukturalnych z poziomami, atrybutami klucz-wartość, handlerami tekstowymi i JSON oraz loggerami, które niosą kontekst dzięki With.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

slog w jednym przykładzie

log/slog (Go 1.21) zapisuje rekordy strukturalne: komunikat, poziom i atrybuty klucz-wartość.

Każda linia wychodzi w postaci time=... level=INFO msg="user logged in" user=ada attempts=1. Ponieważ każda wartość jest osobnym polem, system zbierania logów (Loki, Elasticsearch, CloudWatch, Datadog) może filtrować po user=ada albo level=ERROR bez wyrażeń regularnych na swobodnym tekście.

Przykłady na tej stronie piszą do os.Stdout, żeby wynik pojawiał się po kolei. W prawdziwym serwisie logi zwykle trafiają do os.Stderr, gdzie pisze też domyślny logger.

Poziomy

PoziomWartośćDo czego
slog.LevelDebug-4szczegóły dla programistów, wyłączone na produkcji
slog.LevelInfo0zwykłe zdarzenia: start, obsłużone żądanie, zakończone zadanie
slog.LevelWarn4coś nieoczekiwanego, co program obsłużył
slog.LevelError8operacja się nie powiodła

Handler odrzuca rekordy poniżej swojego minimalnego poziomu, a domyślne minimum to Info. Dlatego slog.Debug(...) nic nie wypisuje, dopóki nie skonfigurujesz handlera z Level: slog.LevelDebug. Odstępy między wartościami zostawiają miejsce na własne poziomy, np. slog.Level(2).

Aby zmieniać poziom w trakcie działania (z flagi, endpointu administracyjnego albo sygnału), umieść w opcjach slog.LevelVar i później wywołuj na nim Set:

var level slog.LevelVar // zero value: Info
logger := slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{Level: &level}))
level.Set(slog.LevelDebug) // from now on, debug records are written

Tekst czy JSON

slog.NewTextHandler zapisuje pary key=value, łatwe do czytania w terminalu. slog.NewJSONHandler zapisuje jeden obiekt JSON na linię, czyli format, którego oczekuje większość potoków logów. Wywołania logujące pozostają takie same; zmienia się tylko handler.

Wartości zachowują swoje typy: status w wyniku JSON jest liczbą, a retry wartością logiczną, time.Duration wypisuje się jako 42ms w tekście i jako nanosekundy w JSON, a error wypisuje swój komunikat. ReplaceAttr to punkt zaczepienia do przepisywania lub usuwania atrybutów; tutaj usuwa znacznik czasu, a w praktyce służy do zmiany nazw kluczy (msg na message) albo maskowania wartości.

Atrybuty

Luźno typowana forma przeplata klucze i wartości: "user", "ada", "attempts", 3. Jest krótka i ma jeden sposób na błąd: nieparzystą liczbę argumentów. Pozostała wartość zostaje zalogowana pod kluczem !BADKEY. go vet to wyłapuje:

./main.go:14:2: call to slog.Info missing a final value

Dla bezpieczeństwa typów i nieco mniejszej liczby alokacji używaj konstruktorów atrybutów, a przy logowaniu na gorącej ścieżce LogAttrs:

logger.Info("order placed",
	slog.Int("order_id", 1017),
	slog.String("currency", "EUR"),
	slog.Float64("total", 59.90),
	slog.Duration("took", elapsed),
)

logger.LogAttrs(ctx, slog.LevelInfo, "order placed", slog.Int("order_id", 1017))

Stosuj jedną konwencję nazewnictwa kluczy w całym kodzie (user_id wszędzie, a nie userID w jednym pakiecie i uid w innym). Zależą od tego zapytania w twoim systemie logów.

With: loggery, które niosą kontekst

logger.With(attrs...) zwraca nowy logger, który dodaje te atrybuty do każdego rekordu. Utwórz go dla każdego żądania albo zadania, a wszystkie zapisane przez niego linie da się ze sobą powiązać:

Każda linia niesie service, version, request_id i user bez powtarzania ich przy każdym wywołaniu. slog.Group zagnieżdża atrybuty, które handler JSON zapisuje jako zagnieżdżony obiekt ("payment":{"amount":25,"currency":"USD"}), a handler tekstowy jako klucze z kropkami (payment.amount=25). logger.WithGroup("db") umieszcza każdy późniejszy atrybut tego loggera w grupie.

Przekazuj logger danego żądania w dół jako parametr albo pole struktury. Przechowywanie go w context.Context jest możliwe, ale ukrywa zależność; metody slog InfoContext(ctx, ...) przekazują kontekst do handlera, a własny handler może z niego wyciągnąć identyfikatory śledzenia.

Ukrywanie sekretów przez LogValuer

Typ może kontrolować, jak jest logowany, implementując slog.LogValuer. Dzięki temu hasła i tokeny nie trafiają do logów, niezależnie od tego, kto loguje wartość:

User loguje tylko swoje ID i e-mail, a Token zalogowany samodzielnie wypisuje REDACTED. Handler wywołuje LogValue dopiero wtedy, gdy rekord faktycznie jest zapisywany, więc działa to także dla wartości kosztownych do obliczenia.

Klasyczny pakiet log

log jest starszy niż slog i nadal dobrze sprawdza się w małych programach i skryptach. Zapisuje linie na standardowe wyjście błędów z prefiksem z datą i godziną:

FlagaDodaje
log.LstdFlags (domyślna)datę i godzinę 2009/11/10 23:00:00
log.Lmicrosecondsmikrosekundy w czasie
log.LUTCczas w UTC
log.Lshortfile / log.Llongfilemain.go:14 / pełną ścieżkę
log.Lmsgprefixumieszcza prefiks przed komunikatem zamiast na początku linii

Trzy funkcje kończą program albo wywołują panikę, a różnica ma znaczenie:

  • log.Fatal, log.Fatalf, log.Fatalln wypisują komunikat, a potem wywołują os.Exit(1). Wywołania odroczone się nie wykonują. Używaj ich w main przy błędach startu, nigdy w kodzie bibliotek ani w handlerach żądań.
  • log.Panic i pokrewne wypisują komunikat, a potem wywołują panikę, więc wywołania odroczone się wykonują, a panikę można przechwycić.
  • Każda inna funkcja po prostu zapisuje linię.

Aby logować do pliku, otwórz go i przekaż do log.New albo log.SetOutput; io.MultiWriter(os.Stderr, f) zapisuje do obu miejsc.

log i slog razem

slog.SetDefault(logger) ustawia logger jako domyślny dla funkcji najwyższego poziomu, takich jak slog.Info, a także kieruje przez niego wyjście pakietu log. Istniejące wywołania log.Printf w twoim kodzie albo w zależnościach wychodzą wtedy jako rekordy strukturalne na poziomie Info:

slog.SetDefault(slog.New(slog.NewJSONHandler(os.Stderr, nil)))
log.Printf("legacy message") // {"time":"...","level":"INFO","msg":"legacy message"}

Przed SetDefault domyślny logger slog pisze przez pakiet log, dlatego samo slog.Info("hi") wypisuje 2026/09/23 14:30:00 INFO hi.

Praktyczne zasady

  • Loguj błąd albo go zwracaj, nie jedno i drugie. Funkcja, która loguje błąd i go zwraca, sprawia, że ta sama awaria jest logowana na każdym poziomie stosu wywołań. Zwracaj błędy w górę z kontekstem i loguj raz, tam gdzie są obsługiwane.
  • Zmienne dane umieszczaj w atrybutach, a nie w komunikacie. logger.Info("user created", "user_id", id) dobrze się grupuje w systemie logów; logger.Info(fmt.Sprintf("user %d created", id)) tworzy inny komunikat dla każdego użytkownika.
  • Nigdy nie loguj sekretów ani pełnych body żądań. Do maskowania używaj LogValuer albo ReplaceAttr.
  • JSON na produkcji, tekst w developmencie. Wybieraj handler przy starcie na podstawie flagi albo zmiennej środowiskowej.
  • Świadomie dobieraj poziomy. Jeśli wszystko jest logowane na poziomie Error, alerty o błędach stają się szumem.

Najczęściej zadawane pytania

Czym jest slog w Go?

log/slog to pakiet do logowania strukturalnego, dodany do biblioteki standardowej w Go 1.21. Zamiast sformatowanych stringów każdy rekord ma komunikat, poziom (Debug, Info, Warn, Error) i atrybuty klucz-wartość, a handler zapisuje go jako tekst key=value albo jako JSON: slog.Info("login", "user", "ada", "attempts", 3).

Jak włączyć logi debug w slog?

Domyślny minimalny poziom to Info, więc slog.Debug nic nie wypisuje. Utwórz handler z niższym poziomem i ustaw go jako domyślny: slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelDebug}))). Jeśli chcesz zmieniać poziom w trakcie działania programu, użyj slog.LevelVar zamiast stałej.

Czym różni się log od slog w Go?

log zapisuje linie w dowolnej formie z opcjonalnym prefiksem z datą i nie ma poziomów. slog zapisuje rekordy z poziomami i typowanymi atrybutami klucz-wartość, które systemy zbierania logów potrafią parsować i filtrować. Oba są w bibliotece standardowej; slog.SetDefault przekierowuje też wyjście pakietu log przez handler slog.

Czy log.Fatal uruchamia funkcje odroczone?

Nie. log.Fatal i log.Fatalf wypisują komunikat i wywołują os.Exit(1), co pomija wszystkie wywołania odroczone. Używaj ich tylko w main albo w kodzie startowym, gdzie nie ma nic do posprzątania. log.Panic zamiast tego wywołuje panikę, więc wywołania odroczone się wykonują.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ