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.