Logi — okno na działającą aplikację

Kiedy aplikacja działa poprawnie, nikt nie zagląda do logów. Ale gdy coś się psuje — o drugiej w nocy, w piątek — logi stają się jedynym źródłem prawdy. Jakość logowania bezpośrednio wpływa na to, jak szybko zdiagnozujesz problem. Dobrze zaprojektowane logi potrafią skrócić czas naprawy z godzin do minut.

W naszych serwisach Go stosujemy spójne podejście do logowania, wypracowane na podstawie lat doświadczeń z aplikacjami produkcyjnymi. Dzielimy się tymi praktykami, bo wiemy, jak często logowanie jest traktowane po macoszemu.

Structured logging — koniec z parsowaniem tekstu

Tradycyjne logi tekstowe wyglądają tak:

2025-11-04 10:15:23 ERROR: Failed to send email to jan@example.com: connection timeout

Czytelne dla człowieka, ale koszmarne do automatycznego przetwarzania. Wyszukanie wszystkich błędów wysyłki maili wymaga wyrażeń regularnych i modlitwy.

Structured logging (logowanie strukturalne) zapisuje zdarzenia jako obiekty z polami:

{
    "time": "2025-11-04T10:15:23Z",
    "level": "ERROR",
    "msg": "Failed to send email",
    "recipient": "jan@example.com",
    "error": "connection timeout",
    "duration_ms": 5000,
    "attempt": 3
}

Każde pole jest osobnym kluczem, co umożliwia filtrowanie, agregację i alerting. Wyszukanie wszystkich błędów maili to proste zapytanie po polu msg, a nie regex.

Od Go 1.21 standardowa biblioteka oferuje pakiet log/slog z natywnym wsparciem dla structured loggingu:

slog.Error("Failed to send email",
    "recipient", email,
    "error", err,
    "duration_ms", elapsed.Milliseconds(),
    "attempt", attempt,
)

slog obsługuje format JSON i tekstowy, pozwala dodawać kontekst (logger z predefiniowanymi polami) i jest wydajny — alokuje minimalnie pamięci.

Poziomy logów

Poprawne użycie poziomów logów to fundament użytecznego systemu logowania:

DEBUG — szczegółowe informacje diagnostyczne. Wartości zmiennych, przebiegi algorytmów, zapytania SQL. Włączane tylko na potrzeby debugowania konkretnego problemu. Na produkcji domyślnie wyłączone — generują ogromne ilości danych.

INFO — normalne zdarzenia operacyjne. Start serwera, obsłużone żądanie, wysłany email, zalogowanie użytkownika. Te logi potwierdzają, że aplikacja działa prawidłowo.

WARN — sytuacje nietypowe, ale nie będące błędami. Zbliżanie się do limitu pamięci, nieudana próba z automatycznym ponowieniem, deprecated API użyte przez klienta. Ostrzeżenie oznacza: "teraz działa, ale coś może pójść źle".

ERROR — operacja nie powiodła się. Nieudana wysyłka maila, błąd połączenia z bazą, nieprawidłowe dane wejściowe od użytkownika. Błąd wymaga uwagi, ale aplikacja kontynuuje działanie.

Najczęstsze błędy w stosowaniu poziomów:

  • Logowanie wszystkiego jako ERROR — gdy wszystko jest błędem, nic nie jest błędem
  • Logowanie danych osobowych — adresy email, numery telefonów, hasła w logach to naruszenie RODO
  • Zbyt szczegółowe logi na produkcji — miliony wpisów DEBUG spowalniają aplikację i wypełniają dysk

Co logować, a czego nie

Zawsze loguj:

  • Start i zatrzymanie aplikacji (z wersją i konfiguracją)
  • Żądania HTTP (metoda, ścieżka, status, czas odpowiedzi, IP klienta)
  • Błędy i wyjątki (z pełnym kontekstem)
  • Operacje na danych (tworzenie, modyfikacja, usunięcie)
  • Zdarzenia bezpieczeństwa (logowanie, nieudane próby, zmiany uprawnień)
  • Połączenia z zewnętrznymi serwisami (baza danych, API, SMTP)

Nigdy nie loguj:

  • Haseł (nawet hashowanych — sam fakt logowania może być problemem)
  • Tokenów sesji i kluczy API
  • Numerów kart kredytowych
  • Pełnych danych osobowych (zamiast imienia i nazwiska loguj ID użytkownika)
  • Zawartości żądań z danymi wrażliwymi

Request ID — łączenie logów

W systemie obsługującym setki żądań na sekundę, powiązanie logów z jednego żądania HTTP jest kluczowe. Rozwiązaniem jest Request ID — unikalny identyfikator przypisywany każdemu żądaniu:

func requestIDMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        id := uuid.New().String()
        ctx := context.WithValue(r.Context(), "request_id", id)
        w.Header().Set("X-Request-ID", id)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Każdy log wygenerowany w ramach obsługi żądania zawiera ten sam Request ID. Gdy użytkownik zgłasza problem, wystarczy poprosić o Request ID z nagłówka odpowiedzi, aby odfiltrować wszystkie powiązane logi.

Rotacja logów

Logi rosną. Bez rotacji, plik logu prędzej czy później wypełni dysk i zatrzyma aplikację. W środowiskach kontenerowych (Docker) problem jest inny — domyślnie Docker przechowuje logi w plikach JSON, które też mogą urosnąć.

Dla Dockera konfigurujemy limit w docker-compose.yml:

services:
  app:
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

Trzy pliki po 10 MB — łącznie 30 MB na serwis. To wystarczy do bieżącej diagnostyki, a starsze logi powinny trafiać do systemu agregacji.

Dla aplikacji logujących bezpośrednio do pliku, logrotate na Linuksie jest standardowym rozwiązaniem — rotuje pliki dziennie lub po osiągnięciu rozmiaru, kompresuje stare i usuwa najstarsze.

Monitorowanie i alerting

Logi są wartościowe tylko wtedy, gdy ktoś je czyta — albo gdy system robi to automatycznie. Podstawowe podejście:

  • Agregacja — zbieraj logi ze wszystkich serwisów w jedno miejsce (Loki, Elasticsearch, Fluentd)
  • Wyszukiwanie — interfejs do przeszukiwania logów (Grafana, Kibana)
  • Alerty — automatyczne powiadomienia o krytycznych zdarzeniach (np. wzrost error rate powyżej progu)

W prostszych scenariuszach wystarczy grep po plikach logów i prosty skrypt monitorujący. Nie każda aplikacja potrzebuje ELK stacka — ważne, żeby logi były przeszukiwalne i żeby ktoś reagował na błędy.

Podsumowanie

Dobre logowanie to inwestycja, która zwraca się przy pierwszej awarii produkcyjnej. Używaj structured loggingu (pakiet slog w Go), stosuj poprawne poziomy logów, dodawaj Request ID do każdego żądania i nie loguj danych wrażliwych. Pamiętaj o rotacji i monitorowaniu — logi, których nikt nie czyta, nie mają wartości.

Potrzebujesz pomocy z wdrożeniem systemu logowania lub monitoringu? Napisz do nas — chętnie doradzimy.