Od zbierania śmieci do mierzenia wydajności – zmiana podejścia do logów
Logi jako czarna skrzynka kontra logi jako termometr wydajności
Logi zwykle trafiają do projektu „z rozpędu”: biblioteka domyślnie coś wypisuje, framework dorzuca swoje, ktoś ręcznie wstawił kilka logger.info() w kodzie. Rezultat? Ogromny strumień tekstu, z którego rzadko da się szybko wyciągnąć odpowiedź na kluczowe pytanie: dlaczego system działa wolno albo niestabilnie.
Przy takim podejściu logi pełnią rolę „czarnej skrzynki po katastrofie”. Gdy usługa padnie, inżynier w panice zaczyna filtrować po „ERROR” i czytać linijki w poszukiwaniu wskazówek. Działa to od biedy przy jednorazowych awariach, ale zupełnie nie pomaga mierzyć i poprawiać wydajności na co dzień.
Zupełnie inne efekty daje traktowanie logów jak termometru wydajności. Zamiast chaotycznego „pisać wszystko”, projektuje się je tak, by modelowały przepływ żądań, czasy operacji, przeciążenia i degradacje usług. Logi z takiego systemu odpowiadają wprost na pytania: co spowalnia, kiedy spowalnia i kogo to dotyka.
Dlaczego tona logów nie poprawia czasu odpowiedzi
Więcej logów nie oznacza lepszej obserwowalności. Często jest odwrotnie: im więcej nieustrukturyzowanego tekstu, tym trudniej zbudować sensowny wykres czy raport. W kontekście wydajności szczególnie szkodliwe są:
- Logi bez czasu wykonania – wiadomo, że coś się wydarzyło, ale nie wiadomo, jak długo trwało.
- Logi bez kontekstu żądania – nie da się powiązać wpisu z konkretną operacją użytkownika.
- Logi w losowym formacie tekstowym – parsery się gubią, próby agregacji kończą się błędami.
- Logi o wszystkim i wszędzie – brak priorytetów, brak podziału na poziomy i typy zdarzeń.
Systemy logowania nastawione tylko na „posiadanie śladu” zużywają zasoby (dysk, sieć, CPU na serializację) i generują koszty, a praktycznie nie wspierają analiz wydajnościowych. Aby logi pomagały, muszą być celowo zaprojektowane wokół pytań wydajnościowych, a nie jedynie „odpalone domyślnie”.
Pytania, na które logi wydajnościowe mają dawać szybkie odpowiedzi
Dobrze skonfigurowany system logów pełni rolę szybkiego „radaru” zamiast powolnej „archeologii tekstu”. Powinien umożliwiać odpowiedź w minutach (nie godzinach) na takie pytania:
- Które endpointy są najwolniejsze i jak zmienia się ich czas odpowiedzi w ciągu dnia?
- Czy aktualne spowolnienia wynikają z bazy danych, zewnętrznego API, czy logiki aplikacji?
- Jak wygląda czas odpowiedzi dla konkretnych klientów / tenantów / krajów?
- Jaki odsetek żądań przekracza nasz cel, np. p95 < 300 ms?
- Czy po wdrożeniu nowej wersji zwiększyło się opóźnienie lub liczba błędów?
Jeśli obecny system logów nie jest w stanie udzielić takich odpowiedzi bez ręcznego przekopywania się przez setki wpisów, to znaczy, że logi nie są zaprojektowane pod wydajność, tylko pod „jakieś tam śledzenie zdarzeń”. To dobry moment, by przebudować je wokół konkretnych metryk i wskaźników.
Logowanie dla audytu a logowanie dla wydajności
W wielu firmach logi powstają pierwotnie z powodów audytowych: kto się zalogował, jakie dane zmienił, jakie dokumenty otworzył. Takie wpisy są ważne z perspektywy bezpieczeństwa i zgodności, ale z reguły niewiele mówią o wydajności.
Logowanie dla audytu skupia się na:
- Identyfikacji użytkownika (kto?).
- Rodzaju operacji (co zrobił?).
- Efekcie (powiodło się / nie powiodło, zmienione dane).
Logowanie dla wydajności musi dołożyć inne wymiary:
- Czas startu i zakończenia operacji.
- Miary obciążenia (liczba równoległych żądań, wielkość odpowiedzi).
- Źródło opóźnień (baza danych, sieć, zewnętrzny serwis).
- Identyfikatory korelacyjne (trace_id, span_id, correlation_id).
W praktyce oba światy można połączyć: logi audytowe wzbogaca się o minimalny zestaw pól wydajnościowych, a logi techniczne – o kluczowy kontekst biznesowy. System logów przestaje być wówczas zbiorem „przypadkowych notatek”, a staje się spójnym obrazem zarówno zachowań użytkowników, jak i kondycji systemu.
Co właściwie chcemy mierzyć? Kluczowe pojęcia wydajnościowe
Podstawowe wymiary: latency, throughput, error rate, resource usage
Zanim zaczną powstawać kolejne wpisy w logach, trzeba jasno określić, jakie wymiary wydajności są interesujące. Najczęściej są to:
- Latency (opóźnienie) – czas od otrzymania żądania do wysłania odpowiedzi. Kluczowy z punktu widzenia UX.
- Throughput (przepustowość) – liczba żądań / operacji obsługiwanych w jednostce czasu (np. na sekundę).
- Error rate – odsetek nieudanych żądań (kody HTTP 5xx, wyjątki, time-outy).
- Resource usage – zużycie CPU, pamięci, I/O, połączeń do bazy, wątków.
System logów nastawiony na mierzenie wydajności powinien wspierać analizę wszystkich tych wymiarów. Czasem wymaga to połączenia logów z metrykami systemowymi, ale klucz tkwi w tym, by logi odzwierciedlały przepływ żądań oraz krytyczne kroki w ich obsłudze.
SLI, SLO i SLA – jak przekuć je na wymagania wobec logów
Nowoczesne podejście do wydajności opiera się na trzech skrótach: SLI, SLO, SLA.
- SLI (Service Level Indicator) – konkretny wskaźnik, np. „p95 czasu odpowiedzi dla /api/orders”.
- SLO (Service Level Objective) – cel: „p95 < 300 ms przez 99% czasu w miesiącu”.
- SLA (Service Level Agreement) – obietnica względem klienta, często kontraktowa.
Każdy SLI, który ma być monitorowany, musi mieć odzwierciedlenie w logach lub metrykach. Jeśli SLO dotyczy czasu odpowiedzi endpointu, to w logach musi znaleźć się:
- identyfikator endpointu lub operacji,
- dokładny czas rozpoczęcia i zakończenia,
- status odpowiedzi (sukces/błąd),
- kontekst, po którym można filtrować (klient, region, środowisko).
To nie jest teoretyczne ćwiczenie. Gdy SLO są już ustalone, można dosłownie przełożyć je na wymagania dla logów: „aby móc mierzyć to SLO, każdy log request_completed musi zawierać pola X, Y, Z”. To prosty sposób na wymuszenie spójności i kompletności wpisów.
Poziomy: użytkownik, aplikacja, baza danych, infrastruktura
Opóźnienie odczuwane przez użytkownika to suma wielu mniejszych czasów na różnych poziomach:
- Poziom użytkownika (UX) – np. time-to-first-byte, first contentful paint, opóźnienia w SPA.
- Poziom aplikacji – czas przetwarzania żądania w serwerze HTTP / serwisie biznesowym.
- Poziom bazy danych – czasy wykonywania zapytań, blokady, kolejki.
- Poziom infrastruktury – load balancery, sieć, kontenery, maszyny wirtualne.
Logi zorientowane na wydajność muszą „przebijać się” przez te warstwy. Jeśli logowana jest tylko aplikacja, a baza danych i reverse proxy „milczą”, wiele problemów będzie wyglądało jak „magiczne spowolnienie”, bez jasnej przyczyny.
Przykład: „strona ładuje się zbyt wolno” rozpisany na dane w logach
Wyobraźmy sobie sygnał z biznesu: „strona zamówień ładuje się zbyt wolno”. Za tą prostą skargą kryje się cały łańcuch danych, które powinny znaleźć się w logach:
- Log „request_completed” z całkowitym czasem odpowiedzi dla /orders oraz identyfikatorem użytkownika / klienta.
- Logi z load balancera pokazujące time-to-first-byte i ewentualne time-outy.
- Logi zapytań do bazy z informacją o czasie wykonania i typie zapytania (SELECT/UPDATE, tabela).
- Logi ewentualnych wywołań zewnętrznych (np. system płatności) z czasem.
Dzięki temu można szybko zaobserwować, czy:
- większość czasu spędzana jest w aplikacji,
- wąskim gardłem jest baza danych (długie SELECT-y),
- problem występuje tylko dla części użytkowników (np. konkretnego regionu lub dużych koszyków),
- czy dotyczy wszystkich i pojawił się po konkretnym wdrożeniu.
Bez takiego rozpisania problemu na konkretne pola logów pozostaje zgadywanie lub ręczne profilowanie pod presją czasu. Dobrze zaprojektowane logi zamieniają te zgadywanki w zwykłą analizę danych.

Z czego zbudować system logów do mierzenia wydajności – narzędzia i koncepcje
Logi, metryki i trace’y – co czym jest i jak się uzupełniają
W dyskusjach o wydajności pojawiają się trzy słowa-klucze: logi, metryki, trace’y. Wszystkie są potrzebne, ale spełniają inne role.
- Logi – szczegółowe zapisy zdarzeń. Dobre do analizy „co dokładnie się stało” dla konkretnego żądania lub błędu. Mogą zawierać mnóstwo kontekstu.
- Metryki – zagregowane wartości liczbowe, np. liczba żądań na sekundę, p95 czasu odpowiedzi. Szybkie do analizy, świetne do dashboardów i alertów.
- Trace’y (tracing żądań) – śledzenie przepływu pojedynczego żądania przez wiele serwisów. Pokazują, ile czasu spędzono w każdym kroku i gdzie pojawiają się opóźnienia.
Logi są najbardziej elastyczne, ale też najcięższe w przetwarzaniu. Dlatego sensowna architektura wydajnościowa zakłada:
- w logach znajduje się surowy materiał (zdarzenia, pola),
- z logów lub bezpośrednio z aplikacji generowane są metryki,
- dla wybranych ścieżek włączony jest distributed tracing do diagnozowania trudnych przypadków.
Celem nie jest wybór jednego narzędzia, lecz spójne użycie trzech typów danych. W praktyce duża część metryk i trace’ów powstaje z tej samej instrumentacji, która generuje logi wydajnościowe.
Centralizacja logów – ELK, Loki, chmury i inne rozwiązania
Aby logi mogły służyć do realnej analizy wydajności, muszą być centralnie dostępne. Przeklikiwanie się przez pliki tekstowe na poszczególnych serwerach zwyczajnie nie skaluje się ani podczas incydentu, ani przy analizach historycznych.
Najczęściej stosowane podejścia to:
- ELK (Elasticsearch + Logstash + Kibana) – klasyk do wyszukiwania i wizualizacji logów. Mocny w elastycznych zapytaniach, nadaje się do obliczania percentyli, ale może być zasobożerny.
- Grafana Loki – zoptymalizowany pod „logi jak metryki”, tani w przechowywaniu, dobrze integruje się z Prometheusem i Grafaną.
- Rozwiązania chmurowe – AWS CloudWatch Logs, GCP Cloud Logging, Azure Monitor. Duży plus: łatwa integracja z innymi usługami, autoskalowanie.
| Rozwiązanie | Mocne strony pod kątem wydajności | Potencjalne ograniczenia |
|---|---|---|
| ELK | Elastyczne zapytania, potężne agregacje, wizualizacje w Kibanie | Wysokie koszty utrzymania klastra, wymaga tuningu pod duże wolumeny |
| Loki | Efektywne przechowywanie, łatwa integracja z Prometheusem, koncentracja na etykietach | Mniej zaawansowane agregacje niż pełny Elasticsearch |
| CloudWatch / Cloud Logging | Bezobsługowość, integracja z ekosystem |
Agent, sidecar czy biblioteka? Jak fizycznie dostarczyć logi
Zanim zaczną powstawać ładne dashboardy, trzeba odpowiedzieć na proste techniczne pytanie: jak logi trafią do systemu centralnego. Tu zwykle w grze są trzy podejścia: agent, sidecar albo biblioteka kliencka.
- Agent na hoście (np. Filebeat, Fluent Bit, Vector) – czyta pliki logów lub nasłuchuje na socketach i wysyła dane dalej. Dobrze sprawdza się przy usługach, które logują na stdout lub do pliku.
- Sidecar w Kubernetesie – kontener obok aplikacji, który zbiera logi tylko dla niej. Ułatwia separację konfiguracji logowania od kodu, ale wymaga ogarnięcia manifestów i limitów zasobów.
- Biblioteka kliencka (np. logback appender do Elasticsearch, Serilog sink, Log4j appender) – aplikacja wysyła logi bezpośrednio do brokera / systemu logów.
Pod kątem wydajności najbezpieczniej jest wysyłać logi lokalnie (na dysk albo na lokalny socket), a dalszy transport zlecić agentowi. Aplikacja powinna jak najmniej „myśleć” o tym, gdzie lecą logi – jej zadaniem jest generować sensowne wydarzenia z dodatkowymi polami, nie walczyć z retriami do klastra Elasticsearch.
Jeśli jednak tworzony jest system o bardzo wysokich wymaganiach czasowych (np. trading), czasami opłaca się pójść w dedykowane biblioteki z asynchronicznym kolejkowaniem logów w pamięci i bezpośrednim wysyłaniem do brokera (Kafka, NATS). Klucz wtedy w tym, by w razie awarii backendu logów system degradował się łagodnie, a nie zabijał samej aplikacji.
Integracja z brokerami – Kafka, RabbitMQ i spółka
Przy dużych wolumenach logów w pewnym momencie pojawia się pytanie: „czy nie wrzucić tego wszystkiego przez brokera wiadomości?”. W kontekście wydajności może to być naprawdę rozsądny krok.
- Kafka – świetnie radzi sobie z dużym throughputem, pozwala na wiele niezależnych konsumentów (np. system logów, system alertów, archiwizacja surowych danych).
- RabbitMQ – lepszy do scenariuszy, gdzie trzeba szybko przekazać logi do dalszego przetwarzania, a niekoniecznie trzymać je miesiącami.
Dlaczego to ma znaczenie dla mierzenia wydajności, a nie tylko dla skalowalności? Bo broker pozwala odseparować generowanie logów od ich analizy. Można spokojnie dorzucić nowego konsumenta, który liczy percentyle, bez ruszania klastra aplikacyjnego. Można też włączyć dodatkowe logowanie w godzinach szczytu na części instancji, nie martwiąc się, że ELK natychmiast padnie.
Instrumentacja – jak połączyć logi, metryki i trace’y jednym strzałem
Kolejny krok to instrumentacja – czyli miejsca w kodzie i infrastrukturze, które emitują dane. Dobrze zrobiona instrumentacja potrafi jednocześnie generować logi, metryki i trace’y, zamiast mieć dla każdego z tych światów osobny, przypadkowy zestaw hooków.
Przykładowo, pojedynczy middleware HTTP może:
- zmierzyć czas obsługi żądania (start/stop),
- zapisać log strukturalny
request_completed, - wystawić metrykę Prometheusa (
http_request_duration_secondsz etykietami), - otworzyć / zamknąć span w systemie trace’ów (np. OpenTelemetry).
Zespół później patrzy na te same czasy odpowiedzi z trzech stron: logów, dashboardu metryk i wykresu trace’ów. Dane są spójne, bo pochodzą z jednego miejsca w kodzie, a nie z pięciu różnych, które używają innych nazw i oznaczeń.
Projektowanie schematu logów nastawionego na wydajność
Dlaczego „ładne JSON-y” to za mało
Logi strukturalne (JSON) są dziś standardem, ale same w sobie nie rozwiązują żadnego problemu wydajności. Można mieć piękne JSON-y, z których wciąż nie da się policzyć p95 czasu odpowiedzi albo odfiltrować jednego klienta.
Sedno tkwi w spójnym schemacie – czyli zestawie pól, który:
- jest powtarzalny między serwisami,
- odzwierciedla realne przepływy (request → baza → zewnętrzne API),
- od razu nadaje się do agregacji i filtrów.
Można na to patrzeć jak na projektowanie API, tylko że dla danych obserwacyjnych. Chaos w nazewnictwie pól zemści się dokładnie tak samo, jak chaos w nazewnictwie endpointów.
Minimalny „kontrakt” logów wydajnościowych
Garść pól, które bardzo się przydają w prawie każdym systemie. Bez nich w praktyce trudno mierzyć wydajność, a nie tylko odtwarzać historie pojedynczych awarii.
- Identyfikator żądania –
trace_idlubrequest_id. Ten sam w całym łańcuchu, od gatewaya po bazę. Umożliwia złożenie historii jednego requestu z kilku systemów. - Znacznik czasu –
timestampz precyzją co najmniej milisekund. Ustalony format (np. ISO8601), bez lokalnych stref czasowych. - Czas trwania –
duration_mslub podobne pole, bez kombinacji z logowaniem tylko „start” i „stop” bez wyliczonej różnicy. - Kluczowy kontekst biznesowy – np.
user_id,tenant_id,order_id. Pozwala sprawdzić, czy problem nie dotyczy konkretnego klienta lub typu operacji. - Środowisko i region –
env(prod, staging),region(eu-central, us-east). Bez tego percentyle z kilku regionów mieszają się w jedną pulę. - Typ zdarzenia – np.
event=request_completed,event=db_query,event=external_call. Klucz do sensownych filtrów. - Status –
status=success/failure, ewentualniehttp_status. Pozwala budować SLI typu „odsetek udanych żądań”.
Standard pola a swoboda zespołów
Częsty konflikt: platforma chce „jednego, słusznego schematu”, a zespoły produktowe chcą swobody. Można to pogodzić, dzieląc logi na część wspólną i część lokalną.
- Część wspólna – wspólny zestaw pól jak wyżej: identyfikatory, środowisko, czas, status, typ zdarzenia. Ten „kontrakt” jest egzekwowany przez wspólną bibliotekę logowania lub middleware.
- Część lokalna – pola specyficzne dla domeny, np.
cart_size,payment_method,shipment_type. Tu zespoły mają sporą elastyczność.
Taki podział umożliwia budowę wspólnych dashboardów wydajności (bo wszędzie jest duration_ms, event, status), a jednocześnie nie zabija lokalnej innowacji. W razie potrzeby da się potem część lokalnych pól „awansować” do standardu, jeśli stają się użyteczne również gdzie indziej.
Idempotencja i logi – jak nie psuć analizy duplikatami
W systemach rozproszonych pojawiają się duplikaty logów: retrie, powtórzone żądania, odtwarzanie zdarzeń z kolejki. Z perspektywy wydajności potrafi to mocno zafałszować wyniki – nagle p95 czasu odpowiedzi wydaje się znacznie gorszy, bo liczy się pięć prób zamiast jednej.
Rozwiązaniem jest wprowadzenie idempotentnych identyfikatorów operacji, np. operation_id. Jeśli to możliwe, wszystkie próby wykonania tej samej biznesowej operacji (np. stworzenie zamówienia) dostają ten sam identyfikator. Wtedy:
- metryki SLI można liczyć na poziomie
operation_id, a nie pojedynczego requestu HTTP, - analiza „jak długo realnie trwała ta operacja” bierze pod uwagę pierwszą i ostatnią próbę.

Co logować, żeby dobrze widzieć wydajność – poziomy i zdarzenia
Wydzielanie „kluczowych zdarzeń wydajnościowych”
Nie ma sensu mierzyć wszystkiego. Znacznie rozsądniej jest wskazać kilka krytycznych chwil w życiu żądania i to je konsekwentnie logować. Najczęściej są to:
- wejście i wyjście z serwisu – np.
request_started,request_completed, - połączenia z bazą danych –
db_queryz czasem wykonania i typem zapytania, - wywołania zewnętrzne –
external_callz identyfikatorem systemu i czasem, - operacje w tle –
job_started,job_completeddla kolejek.
Jeśli te kilka zdarzeń jest dobrze zaprojektowanych, okazuje się, że debugowanie „magicznego spowolnienia” staje się zwykłym sprawdzeniem, na którym kroku robi się wąskie gardło.
Poziomy logów a wydajność – info, debug, trace
Klasyczne poziomy logów (INFO/DEBUG/ERROR/TRACE) też mają swój wymiar wydajnościowy. Przydaje się prosty podział:
- INFO – stabilne zdarzenia wydajnościowe:
request_completed,db_querypowyżej określonego progu, zakończone joby. To z tych logów budowane są dashboardy i SLI. - DEBUG – bardziej szczegółowy kontekst, np. parametry zapytań (bez danych wrażliwych), struktura koszyka. Włączany na konkretne komponenty / regiony, gdy trzeba przycisnąć analizę.
- TRACE – bardzo drobiazgowe logi krok po kroku, zwykle tylko lokalnie lub na krótką chwilę, bo generują ogromny wolumen i potrafią same zaszkodzić wydajności.
Ciekawą praktyką jest dynamiczne podnoszenie poziomu logowania dla konkretnej funkcji lub użytkownika: gdy zauważony jest problem z kontem klienta X, na 10 minut włączany jest DEBUG dla wszystkich requestów z jego tenant_id. Logi wydajnościowe pozostają w miarę lekkie, a przy trudnym case’ie nie trzeba robić pełnej re-konfiguracji.
Unikanie logowania „szumu”
To, że coś da się zalogować, nie znaczy, że trzeba. Każdy log to koszt: CPU, I/O, sieć, miejsce na dysku i czas przetwarzania w systemie analitycznym. Nadmiar szczegółowych logów potrafi skutecznie ukryć te naprawdę istotne.
Prosty filtr: jeśli dany wpis nie pomaga zrozumieć, gdzie zniknął czas albo co zawiodło, prawdopodobnie nie należy do głównej ścieżki logów wydajnościowych. Zamiast logować każdy krok pętli, lepiej zalogować jej podsumowanie z czasem wykonania i wielkością danych.
Logowanie błędów a metryka error rate
Error rate to nie to samo, co „liczba logów ERROR”. Część błędów jest obsłużona (np. retry, fallback) i nie powinna podnosić globalnego wskaźnika awaryjności usług. Dlatego przydaje się wyraźne rozróżnienie:
- „twarde” błędy – takie, które skutkują niepowodzeniem całej operacji (np. HTTP 5xx, brak możliwości zapisu danych),
- „miękkie” błędy – częściowe problemy, po których system stosuje degradację (np. brak rekomendacji, ale strona zamówień wciąż działa).
W logach może to wyglądać tak:
severity=ERROR,failure_type=hard– wpływa na SLI error rate,severity=WARNING,failure_type=soft– pomaga monitorować degradację, ale nie liczy się jako pełna awaria.
Jak mierzyć czasy i opóźnienia na podstawie logów
Start/stop czy bezpośredni duration?
Najczęstsza pułapka: osobne logi started i finished bez bezpośrednio zapisanego czasu trwania. Teoretycznie można później w analityce je dopasować i odjąć timestampy, ale w praktyce robi się z tego mało przyjemna zabawa, zwłaszcza przy duplikatach czy retriach.
Znacznie prościej i bezpieczniej jest liczyć czas trwania w aplikacji i zapisywać go w polu typu duration_ms. Log z zakończenia operacji ma wtedy w sobie wszystkie informacje potrzebne do analizy wydajności – można go agregować nawet prostymi narzędziami, bez skomplikowanych joinów.
Percentyle zamiast średnich
Średni czas odpowiedzi to bardzo zdradliwa metryka. Wystarczy kilku użytkowników z dramatycznie wolnymi odpowiedziami, żeby uśrednione wyniki wyglądały „w miarę okej”, mimo że realny UX jest fatalny. Dlatego przy analizie logów wydajnościowych liczy się głównie:
Jak liczyć percentyle z logów
Percentyle, szczególnie p50, p90, p95, p99, lepiej opisują doświadczenie użytkownika niż średnia. W logach mamy pojedyncze zdarzenia z polem duration_ms, więc dalsza praca to już kwestia agregacji. Można to robić na dwa sposoby: bezpośrednio w systemie logów albo pośrednio, po przekształceniu logów w metryki.
Jeśli silnik logów wspiera funkcje agregujące (np. percentile(duration_ms, 95)), wystarczy dobrać sensowne okno czasowe (np. 5 minut) i filtrować po event=request_completed, status=success, konkretnym service / endpoint. Przy krótkim historycznym oknie działa to sprawnie, ale przy analizie wielu dni może się to stać drogie obliczeniowo.
Druga droga to pipeline logi → metryki. Każdy log z kluczowym zdarzeniem przepływa przez procesor (np. w Logstash, Vector, Fluent Bit), który:
- parsuje JSON,
- wyciąga
duration_ms,service,endpoint,status, - tworzy z nich próbkę metryki typu „histogram” dla systemu monitoringu (Prometheus, Metrictank, itp.).
Percentyle obliczane są wtedy już po stronie silnika metryk, na bazie histogramów, a logi nadal służą do diagnosowania pojedynczych przypadków. Dwa światy korzystają z tego samego źródła prawdy, tylko na różnych poziomach szczegółowości.
Okna czasowe i granulacja
Percentyle liczone z logów mocno zależą od dobranego okna czasowego. Zbyt szerokie okno (np. doba) „wygładza” krótkie skoki opóźnień, zbyt wąskie (np. 1 minuta) potrafi szarpać wykres przy mniejszym ruchu. W praktyce przydaje się wielopoziomowe podejście:
- krótkie okna (1–5 minut) na dashboardach operacyjnych – do wychwytywania nagłych skoków,
- średnie okna (15–60 minut) – do porównywania zmian po deployu,
- dłuższe (dzień, tydzień) – do trendów i analiz produktu.
Dobrym nawykiem jest trzymanie w logach wyraźnego deployment_id lub version. Dzięki temu można łatwo nałożyć na wykres p95 pionową linię „tu była zmiana wersji” i od razu zobaczyć, czy percentyle „pojechały” w którąś stronę.
Łączenie logów z RUM i syntetykami
Logi z backendu mówią, jak szybko liczy serwer, ale użytkownik patrzy na to, co wyświetla mu przeglądarka czy aplikacja mobilna. Dobrze skonfigurowany system wydajnościowy składa się z trzech źródeł:
- logi serwerowe –
request_completed, czasy baz danych, zewnętrzne API, - RUM (Real User Monitoring) –
first_contentful_paint,time_to_interactive, błędy JS, - syntetyki – powtarzalne testy z robotów (np. co 5 minut), mierzące „czysty” czas w kontrolowanych warunkach.
Jeśli każde źródło ma w sobie trace_id lub choćby session_id, da się połączyć: „strona ładuje się wolno, bo backend p95 dla tego endpointu podskoczył o 200 ms od wczoraj”. Bez tego frontend i backend często przerzucają się odpowiedzialnością, a użytkownik dalej czeka.
Śledzenie opóźnień w całym łańcuchu
Gdy system składa się z wielu mikroserwisów, logi pokazują czasy dla pojedynczych kroków, ale potrzebny jest też widok całości: ile trwała cała podróż żądania przez system? Najprostsze podejście to propagacja jednego identyfikatora:
- generujesz
trace_idna brzegu (API Gateway, frontend), - doklejasz go do każdego loga w kolejnych serwisach,
- mierzysz czasy lokalnie (w każdym serwisie) i zagregowane (na wejściu i wyjściu całego requestu).
Przy analizie bierzesz logi o tym samym trace_id i układasz na osi czasu. Widać wtedy np. że 80% czasu znika na czekaniu na zewnętrzne płatności, a lokalna logika to tylko kilka milisekund. To dokładnie ten moment, kiedy decydujesz, czy cache, async albo zmiana integracji ma najlepszy ROI.
Praktyczna konfiguracja logowania w typowych stosach technologicznych
Ogólne zasady dla różnych języków
Niezależnie od tego, czy zespół używa Javy, .NET, Node.js, Pythona, Go czy jeszcze czegoś innego, kilka zasad jest wspólnych:
- JSON jako format wyjściowy – każdy log to jeden wiersz JSON, łatwy do parsowania i przenoszenia między systemami.
- wspólne pole na kontekst – np.
contextlublabels, gdzie trafiają identyfikatory:trace_id,span_id,user_id. - centralna konfiguracja formatowania – zamiast formatować logi własnoręcznie w każdym miejscu, ustawia się raz schemat w bibliotece logowania lub middleware.
- middleware/hook na wejściu i wyjściu – automatyczne logi
request_startedirequest_completedw każdym serwisie HTTP / gRPC / kolejce.
Ręcznie wypisywane logi zostają tylko tam, gdzie naprawdę potrzebny jest dodatkowy kontekst biznesowy lub diagnostyczny, a nie do powtarzania schematu, który można wygenerować mechanicznie.
Java / Spring Boot – MDC i logback/log4j2
W Spring Boot najwygodniej jest oprzeć się na MDC (Mapped Diagnostic Context). To taki „lokalny słownik” powiązany z wątkiem (lub żądaniem), z którego biblioteka logowania automatycznie pobiera pola i dokleja do każdego wpisu.
Typowy przepływ:
- Filtr HTTP na wejściu generuje
trace_id(albo bierze go z nagłówka) i zapisuje w MDC:MDC.put("trace_id", ...). - Ten sam filtr startuje licznik czasu, np.
long start = System.nanoTime(); po obsłudze żądania liczyduration_ms. - Przed zwróceniem odpowiedzi filtr loguje zdarzenie
request_completedzduration_ms,status,path,method,env, itp. - Wzorzec logbacka / log4j2 ma zdefiniowany encoder JSON, który czyta wszystkie pola z MDC i umieszcza je w logu.
Do tego dochodzą interceptory dla wywołań baz danych (np. Hibernate, JDBC) i klientów HTTP (RestTemplate, WebClient), które w ten sam sposób liczą duration_ms i logują db_query czy external_call. Programista nie musi pamiętać, żeby za każdym razem wyliczyć czas – robi to warstwa techniczna.
.NET / ASP.NET Core – middleware i ILogger
W ASP.NET Core analogiczną rolę pełni middleware oraz mechanizm scoped logging. W praktyce sprowadza się to do:
- middleware, który na początku requestu tworzy
Activity(System.Diagnostics) zTraceId, - dodania do
ILoggerstanu zawierającegotrace_id,user_iditd. (np. przezBeginScope), - ustawienia JSON console loggera lub integracji z serwerem logów (Serilog + sink do Elastic, Seq, itp.).
Middleware na wyjściu liczy czas trwania i wypisuje log request_completed. Dla baz danych przydaje się interceptory Entity Framework lub logowanie na poziomie ADO.NET – można tam wprowadzić próg, powyżej którego zapytanie jest logowane na INFO, a poniżej na DEBUG lub wcale. To prosty sposób na oddzielenie „zwykłych” zapytań od potencjalnych wąskich gardeł.
Node.js – Express / Fastify i logowanie asynchroniczne
W środowisku Node.js kluczowe jest, by logowanie nie blokowało pętli event loop. Dlatego przydają się biblioteki zoptymalizowane pod wydajność, jak Pino. Kilka elementów układanki:
- middleware Express/Fastify, który generuje
request_id/trace_idi trzyma go w kontekście (np.AsyncLocalStorage), - logger skonfigurowany do wypisywania JSON na
stdout, - osobny proces lub agent (np.
pino-pretty, Fluent Bit), który zbiera logi zstdouti wysyła do systemu centralnego.
Middleware startuje timer na wejściu, a na końcu żądania loguje request_completed z czasem, metodą, ścieżką i statusem. W Fastify spora część tego jest już załatwiona wbudowanym loggerem; trzeba głównie ujednolicić nazwy pól i dopisać własne pola kontekstowe (env, region, tenant_id).
Python – Django / Flask i strukturalne logowanie
W Pythonie logowanie bywa tradycyjnie tekstowe, ale nic nie stoi na przeszkodzie, by przejść na logi strukturalne. Popularne podejście to:
- użycie standardowego
loggingz JSONFormatter (np. z bibliotekipython-json-logger), - middleware Django / Flask, który:
- tworzy
request_id/trace_id, - ustawia je w
logging.LoggerAdapterlub kontekście (contextvars), - liczy czas obsługi żądania i loguje
request_completed.
- tworzy
Dla dłużej trwających zadań (Celery, RQ) opłaca się dodać wrapper na taski, który:
- loguje
job_startedz identyfikatorem zadania i kontekstem, - po wykonaniu –
job_completedzduration_msi statusem, - przy błędzie –
severity=ERROR,failure_type=hard, razem ze stosownym trace’em.
Dzięki temu w logach można łatwo odfiltrować „wolne joby” i zobaczyć np. że wieczorny batch generowania raportów dusi bazę i wpływa na p99 requestów API.
Go – lekkie logowanie i context.Context
W Go typowy wzorzec to przekazywanie context.Context przez całe wywołanie. Ten kontekst może zawierać trace_id, user_id i inne pola, które logger wyciąga automatycznie. Schemat wygląda tak:
- handler HTTP otrzymuje
ctxztrace_id(np. wygenerowany w middleware), - wszystkie wywołania niżej przyjmują ten sam
ctx, - logger (np. zap, zerolog, logrus) ma helpera
FromContext(ctx), który dokleja pola kontekstowe.
Funkcje odpowiedzialne za zewnętrzne wywołania (HTTP, gRPC, DB) mierzą czas lokalnie (np. time.Since(start)) i logują pojedyncze external_call lub db_query z duration_ms. Logi pozostają lekkie, a jednocześnie bardzo precyzyjne.
Front-end – rozsądne logi w przeglądarce
W aplikacjach SPA na froncie łatwo przesadzić z ilością logów wysyłanych do backendu. Zamiast logować każdy klik, lepiej podejść do tego podobnie jak na serwerze – zdefiniować kluczowe zdarzenia wydajnościowe:
page_viewz czasem renderu i kluczowymi metrykami (np.fcp_ms),api_call_completedzduration_ms,endpoint,status,asset_loadedtylko dla dużych/kluczowych zasobów (np. główny bundle JS, krytyczne CSS).
Do tego dochodzi odrobina sampling’u – np. pełny zestaw logów wydajnościowych z przeglądarki wysyła tylko część sesji (rzędu kilku procent), a reszta ogranicza się do bardziej zagregowanych metryk. W logach warto zachować ten sam trace_id, który wysyłany jest do backendu, aby dało się powiązać „użytkownik widzi spowolnienie” z konkretnymi logami serwerowymi.
Konfiguracja systemów zbierających – od stdout do centralnego magazynu
Niezależnie od języka i frameworka, logi muszą jakoś dotrzeć do systemu analitycznego. Najprostszy i najbardziej przenośny wzorzec to:
- Aplikacja wypisuje logi w formacie JSON na
stdout.
Najczęściej zadawane pytania (FAQ)
Jak zaprojektować logi, żeby mierzyć wydajność zamiast tylko zbierać zdarzenia?
Trzeba zacząć od pytań, na które chcesz odpowiadać: które endpointy są najwolniejsze, skąd biorą się opóźnienia, jaki odsetek żądań przekracza cel typu „p95 < 300 ms”. Dopiero pod te pytania projektujesz strukturę logów – jakie pola są obowiązkowe, jakie nazwy zdarzeń stosujesz, jak oznaczasz operacje.
Praktycznie oznacza to wprowadzenie kilku „kanonicznych” typów wpisów (np. request_started, request_completed, db_query, external_call) i pilnowanie, żeby każdy z nich miał spójny zestaw pól: czas startu i końca, identyfikator żądania, nazwę operacji, status, podstawowe informacje o kliencie/środowisku. Dzięki temu logi zaczynają przypominać uporządkowane dane pomiarowe, a nie losowe notatki programistów.
Jakie pola powinny się znaleźć w logach nastawionych na wydajność?
Minimum to wszystko, co pozwoli policzyć opóźnienia, przepustowość i błędy dla konkretnych operacji. Kluczowe pola to m.in.: znacznik czasu (czas startu i zakończenia), identyfikator żądania (request_id/trace_id), nazwa endpointu lub operacji, status (sukces/błąd, kod HTTP), czas trwania w milisekundach.
Do tego dochodzi kontekst, po którym będziesz później filtrować: klient/tenant, kraj/region, środowisko (prod/stage), czasem typ użytkownika (np. admin vs zwykły klient). Dobrym nawykiem jest też logowanie źródła potencjalnych opóźnień, np. pole source=database|external_api|application przy logach technicznych. Wtedy na wykresie od razu widać, że 70% czasu „ucieka” w zapytaniach do bazy.
Jaka jest różnica między logowaniem dla audytu a logowaniem pod wydajność?
Logi audytowe odpowiadają głównie na pytania „kto co zrobił i czy miał do tego prawo”. Skupiają się na identyfikacji użytkownika, rodzaju operacji (login, zmiana danych, pobranie dokumentu) i efekcie (powiodło się / odrzucono). Z punktu widzenia bezpieczeństwa są kluczowe, ale same w sobie niewiele mówią o tym, dlaczego system reaguje wolno.
Logi wydajnościowe patrzą na ten sam świat z innej strony: „jak długo to trwało, co było wąskim gardłem, ile takich operacji wykonujemy”. Dlatego dodają pola związane z czasem (start, koniec, czas trwania), obciążeniem (liczba równoległych żądań, rozmiar odpowiedzi) i korelacją (trace_id, span_id). W praktyce najlepiej połączyć oba podejścia: log audytowy „zmiana adresu klienta” od razu niesie informację, że operacja trwała np. 2,3 s i 80% czasu zajęła baza danych.
Jak powiązać SLI/SLO z tym, co zapisuję w logach?
Najpierw definiujesz SLI, czyli konkretne wskaźniki: np. „p95 czasu odpowiedzi dla /api/orders” albo „odsetek żądań /login zakończonych kodem 5xx”. Potem ustalasz SLO (jaki wynik cię satysfakcjonuje), np. „p95 < 300 ms przez 99% czasu w miesiącu”. Każdy taki wskaźnik możesz potraktować jak wymaganie do logów: jeśli chcesz to mierzyć, w logach muszą być odpowiednie pola.
Dla przykładu: aby policzyć p95 dla /api/orders per klient, każdy wpis typu request_completed musi zawierać nazwę endpointu, znacznik czasu, czas trwania, identyfikator klienta i status odpowiedzi. Jeśli któregoś z tych pól zabraknie, nie policzysz wskaźnika albo policzysz go „po łebkach”. To prosty filtr: nie ma pola → nie da się zmierzyć SLO → log jest niekompletny.
Jak ograniczyć „szum” w logach i jednocześnie nie stracić danych do analizy wydajności?
Najpierw rozdziel logi na kategorie: wydajnościowe, audytowe, debugowe. Logi debugowe (szczegółowe stack trace’y, wewnętrzne zmienne) trzymaj na niższych poziomach (DEBUG/TRACE) i włączaj selektywnie, np. tylko dla wybranego request_id albo w konkretnym mikroserwisie. Logi wydajnościowe traktuj jako „szkielet” – powinny być krótkie, ustrukturyzowane i zawsze włączone.
Dobrze działa także podejście event-based zamiast „loguj każdą linijkę kodu”. Loguj kluczowe zdarzenia w przepływie żądania: przyjęcie requestu, zakończenie obsługi, wykonanie zapytania do bazy, wywołanie zewnętrznego API. W tych miejscach mierzysz czas i dodajesz podstawowy kontekst. Resztę szczegółów trzymasz w logach technicznych, które można okresowo ścinać lub archiwizować.
Jak praktycznie zmierzyć, czy to baza danych, zewnętrzne API czy aplikacja spowalnia system?
Najprostsza strategia to osobne, ustrukturyzowane logi dla każdego „skoku” w łańcuchu: logi requestów HTTP, logi zapytań do bazy, logi wywołań zewnętrznych. Każdy taki wpis ma ten sam identyfikator korelacyjny (trace_id), dzięki czemu da się później odtworzyć cały przebieg jednego żądania użytkownika.
Przykładowo: przychodzi żądanie na /orders → logujesz request_started. Gdy wykonujesz zapytanie SQL, logujesz db_query z czasem trwania i typem zapytania. Gdy wołasz system płatności, pojawia się external_call z własnym czasem. Na końcu request_completed z całkowitym czasem odpowiedzi. W narzędziu do analizy możesz wtedy policzyć, ile procent czasu w typowym żądaniu zajmuje baza, ile zewnętrzne API, a ile sama logika aplikacji.
Czy same logi wystarczą do analizy wydajności, czy potrzebne są też metryki i trace’y?
Dobrze zaprojektowane logi potrafią zajść bardzo daleko – szczególnie gdy są ustrukturyzowane i da się je łatwo agregować. Pozwolą policzyć p95, throughput, error rate, a nawet pokazać rozkład czasów odpowiedzi dla konkretnych klientów czy krajów. W wielu mniejszych systemach to już robi sporą różnicę.
Przy większej skali logi często łączy się z metrykami (np. Prometheus) i tracingiem (OpenTelemetry, Jaeger, Zipkin). Metryki dają szybki podgląd „z lotu ptaka” w czasie rzeczywistym, a distributed tracing pozwala prześledzić pojedyncze żądanie przez wiele serwisów. Wspólnym klejem między tymi światami nadal jest jednak dobre logowanie: konsekwentne identyfikatory korelacyjne i spójne nazwy operacji.
Najważniejsze wnioski
- Logi „z rozpędu” tworzą hałas, a nie wiedzę: przypadkowe wpisy z domyślnych bibliotek zamieniają się w śmietnik tekstu, z którego trudno wyciągnąć przyczynę spowolnień czy niestabilności.
- Skuteczne logowanie wydajnościowe trzeba projektować z góry jak „termometr” systemu – wpisy mają odzwierciedlać przepływ żądań, czasy operacji, przeciążenia i degradacje, zamiast być zbiorem luźnych komunikatów.
- Ogromna ilość nieustrukturyzowanych logów bez czasu wykonania, kontekstu żądania i spójnego formatu utrudnia analizę i generuje koszty (dysk, sieć, CPU), zamiast realnie wspierać poprawę czasu odpowiedzi.
- Dobrze zaprojektowane logi powinny w kilka minut odpowiadać na konkretne pytania: które endpointy są najwolniejsze, gdzie powstają opóźnienia (baza, zewnętrzne API, logika), jak wygląda czas odpowiedzi dla różnych klientów czy regionów i jak zmienia się p95 po wdrożeniu nowej wersji.
- Logowanie audytowe (kto, co zrobił, z jakim efektem) nie wystarcza do pracy nad wydajnością – trzeba je uzupełnić o wymiary techniczne: czas startu i zakończenia operacji, obciążenie, źródła opóźnień oraz identyfikatory korelacyjne (trace_id, span_id, correlation_id).
- System logów wydajnościowych musi wspierać analizę kluczowych wymiarów: latency (opóźnień), throughput (przepustowości), error rate (odsetka błędów) oraz resource usage (zużycia zasobów), często w połączeniu z metrykami systemowymi.






