← Wszystkie artykuły
Observability

Centralne logi i tania baza zdarzeń w VictoriaLogs

Jak VictoriaLogs trzyma rok logów z ponad 40 źródeł i przechowuje zdarzenia z API dla dashboardów w Grafanie. Do tego pułapki LogsQL, na które straciłem trochę czasu.

Jak logi i zdarzenia trafiają do VictoriaLogs

Pięć ścieżek danych, czyli chmura prywatna, chmura publiczna, host stacku, firewalle i dane z API, prowadzi do VictoriaLogs. Grafana odpytuje VictoriaLogs.Chmura prywatnarsyslog → syslogChmura publicznaAlloy → HTTPSHost stackuAlloy → API LokiFirewallesyslog, filtr + IDSDane z APIWorkflowy → JSON linesVictoriaLogsJeden plik binarnyJedna VMRok historiiLogsQLGrafanaDashboardyExploreAlerty w LogsQLPięć ścieżek danych, czyli chmura prywatna, chmura publiczna, host stacku, firewalle i dane z API, prowadzi do VictoriaLogs. Grafana odpytuje VictoriaLogs.Chmura prywatnarsyslog → syslogChmura publicznaAlloy → HTTPSHost stackuAlloy → API LokiFirewallesyslog, filtr + IDSDane z APIWorkflowy → JSON linesVictoriaLogsJeden plik binarnyJedna VMRok historiiLogsQLGrafanaDashboardyExploreAlerty w LogsQL
Uproszczony obraz ścieżek danych. Sposób wysyłania zależy od tego, które adresy widzi dana maszyna.

VictoriaLogs trzyma przez rok logi z ponad 40 maszyn i z firewalli, wszystko na jednej VM. Przy okazji stała się bazą zdarzeń dla dashboardów, które z logami nie mają wiele wspólnego. Trafiają tam też incydenty z systemu on-call, uruchomienia workflowów, wygasające poświadczenia, deploymenty i zmiany DNS. Wystarczy wysłać JSON przez HTTP.

To moja ulubiona metoda na logi. Dzięki bardzo dobrej kompresji dane zajmują mało miejsca. Podgląd logów na żywo i wyszukiwanie działają sprawnie, a szybkość działania jest po prostu wzorowa. Z VictoriaLogs korzysta też mój HomeLab.

W pracy odpowiada za logi w stacku Grafana, który zbudowałem i utrzymuję. Poniżej są ścieżki danych, wzorzec dla zdarzeń z API i pułapki LogsQL zebrane przy budowie dashboardów.

Baza logów w jednym pliku binarnym

VictoriaLogs to baza logów open source od twórców VictoriaMetrics. Jeden plik binarny przyjmuje logi wieloma protokołami, między innymi syslog, Loki push, JSON lines, Elasticsearch bulk i OpenTelemetry. Zapytania pisze się w LogsQL.

Każdy wpis to zestaw pól, a trzy z nich są specjalne. _time to czas, _msg treść, a _stream pola identyfikujące strumień, na przykład host i aplikacja. Pozostałe pola są indeksowane automatycznie, bez schematu. Można wysłać dowolny JSON, a potem filtrować i agregować po każdym polu.

Jeden wpis i pola, które VictoriaLogs z niego czyta

POST /insert/jsonline
?_stream_fields=source,record{
  "_time": "2026-10-20T02:00:05Z",
  "_msg": "expires in 12 days",
  "source": "cert-check",
  "record": "item",
  "run_id": "r-1020",
  "name": "api.example.com",
  "days_left": 12
}
_time
Kiedy coś się stało. Bez tego pola liczy się czas przyjęcia wpisu.
_msg
Treść wpisu. Filtry po słowie domyślnie szukają właśnie tutaj.
_stream
Powstaje z pól strumienia podanych w żądaniu.
Cała reszta
Indeksowana automatycznie, bez schematu.
Fikcyjny wpis, sformatowany dla czytelności. W NDJSON każdy wpis zajmuje jedną linię.

Retencję ustawiają dwie flagi. -retentionPeriod określa, jak długo trzymać dane, a -retention.maxDiskSpaceUsageBytes limit dysku, po którego przekroczeniu znikają najstarsze dni. Tutaj to rok historii, a limit dysku działa jako zabezpieczenie.

Jak dane trafiają do środka

Nie ma jednej drogi, bo nie każda maszyna widzi te same adresy. Diagram na górze pokazuje pięć ścieżek.

  • Chmura prywatna. Hosty wysyłają logi przez rsyslog prosto do odbiornika syslog wbudowanego w VictoriaLogs. Nie ma tu żadnego pośrednika.
  • Chmura publiczna. Te hosty nie widzą prywatnego adresu. Ich journal wysyła więc Grafana Alloy przez HTTPS. To ten sam agent, który już wysyła ich metryki.
  • Host stacku. Alloy czyta pliki z /var/log, logi kontenerów Dockera i plik zdarzeń audytora DNS. VictoriaLogs udostępnia endpoint zgodny z Loki, więc standardowy loki.write w Alloy działa bez zmian.
  • Firewalle. OPNsense wysyła przez syslog logi filtra pakietów i alerty IDS. To zdecydowanie najcięższy strumień.
  • Dane z API. Workflowy normalizują dane do NDJSON i wysyłają je żądaniem POST na endpoint JSON lines. Ścieżka zapisu dla tych danych jest celowo wąska, a stały kontrakt strumienia ustawia serwer. Zaczęło się od danych o użyciu i kosztach narzędzi AI. Dziś ta ścieżka karmi wszystkie dashboardy z danymi z API.

Grafana czyta to wszystko przez źródło danych VictoriaLogs. Zapytania surowe zasilają panele z logami, a zapytania stats wykresy. Reguły alertów też korzystają z LogsQL, na przykład taka, która reaguje, gdy źródło przestaje wysyłać logi.

Zdarzenia, które nie są logami

Dashboardy z danymi z API, na przykład z systemu on-call, z platformy workflowów albo z Entra ID, potrzebują historii. Zwykle są dwa sposoby, żeby ją mieć. Osobna baza oznacza Postgresa, schemat i proces ETL do utrzymania. Zapytania do API na żywo oznaczają limity, wolne panele i brak historii.

Tutaj workflowy zbierają i normalizują dane, VictoriaLogs przechowuje je jako zdarzenia z polami, a Grafana liczy statystyki w LogsQL. Nic nie wymaga schematu ani migracji. Nowe pole to po prostu nowe pole w JSON.

Dziś w VictoriaLogs jest rekord startu i końca każdego uruchomienia pipeline’u, z commitem i liczbami changed, failed i unreachable. Jest każde wykonanie workflowu i historia incydentów z systemu on-call, uzupełniona wstecz z jego API razem z etykietami alertów. Codzienny snapshot sekretów i certyfikatów z datami wygaśnięcia leży obok wyników audytu DNS. Są w nim tylko nazwy i daty, nigdy wartości. Razem ze zwykłymi logami te dane zasilają kilkanaście dashboardów.

Snapshot zamiast sumy

Część danych opisuje stan, a nie zdarzenie. Dobry przykład to lista certyfikatów z datami wygaśnięcia. Jeśli workflow co noc zapisuje całą listę, zwykłe liczenie zsumuje wszystkie noce, w które działał.

Rozwiązanie to prosty wzorzec. Każde uruchomienie zapisuje najpierw jeden rekord run z run_id, a potem rekordy danych z tym samym run_id. Dashboard przez podzapytanie bierze tylko najnowsze uruchomienie.

Jak dashboard wybiera najnowszy snapshot

  1. run_id r-1018 · 18.10api.example.com · 14 dnivpn.example.com · 33 dnimail.example.com · 61 dnishop.example.com · 90 dni
  2. run_id r-1019 · 19.10api.example.com · 13 dnivpn.example.com · 32 dnimail.example.com · 60 dnishop.example.com · 89 dni
  3. run_id r-1020 · 20.10api.example.com · 12 dnivpn.example.com · 31 dnimail.example.com · 59 dnishop.example.com · 88 dniNajnowsze uruchomienie

Zwykły count() po wszystkich uruchomieniach12

Tylko najnowszy run_id4

Dane fikcyjne. Co noc workflow zapisuje te same cztery certyfikaty jeszcze raz. Aktualny stan opisuje tylko najnowsze uruchomienie.

W LogsQL wygląda to tak.

source:="cert-check" record:="item"
  run_id:in(
    source:="cert-check" record:="run"
    | sort by (_time desc) | limit 1 | fields run_id
  )
| stats count() as certificates

LogsQL w praktyce

Zapytanie w LogsQL czyta się jak potok. Najpierw są filtry, na przykład app_name:filterlog _time:24h dla jednego dnia logów filtra pakietów z firewalla. Resztę robią kolejne kroki potoku, takie jak | extract, | stats by (...) count(), | math, | sort albo | join. W Grafanie wynik stats staje się panelem.

Od zapytania LogsQL do panelu w Grafanie

source:="workflows"
| stats by (status) count() as runs

Wykonania workflowów dziennie

  • success
  • error
Fikcyjne dane z dwóch tygodni. Zapytanie liczy wykonania według statusu, a panel szeregu czasowego pokazuje jeden słupek na dzień.

Pułapki, na które straciłem czas

Wszystkie wyszły przy budowie dashboardów. Zachowanie może się zmieniać między wersjami, więc traktuj je jako notatki z mojej konfiguracji.

  • Liczenie unikalnych wartości. | last 1 by (_time) partition by (id) | stats count() zwracało 1. Goły count() nie czyta pól, więc partycja nie działała. count_uniq(id) odpowiada na to samo pytanie wprost.
  • Mediana, która wraca pusta. Brakujące pola liczą się jako puste wartości, więc median(x) nic nie zwraca, kiedy większość wierszy nie ma pola x. Zapis median(x) if (x:*) ogranicza liczenie do wierszy, które to pole mają.
  • Surowsze zapytania w panelach. Zapytanie stats w panelu musi kończyć się na stats albo math. Zapytania zakresowe do wykresów odrzucały last i join.
  • Lokalna data i czas letni. Stały offset, jak w stats by (_time:1d offset 2h), przez część roku daje złą datę. Lokalną datę liczę z _time przez math na nanosekundach, co uwzględnia też zmianę czasu.
  • 200 i nic nie zapisane. POST bez nagłówka Content-Type: application/stream+json dostawał odpowiedź 200 i zapisywał 0 wierszy. VictoriaLogs pomija też linie JSON, których nie umie odczytać, i przetwarza resztę. Sam kod odpowiedzi nie dowodzi, że coś się zapisało.
  • Testy backfillu na lokalnym pliku binarnym. Domyślna retencja to siedem dni, a starsze wpisy są odrzucane przy zapisie. Żądanie i tak kończy się sukcesem, a wspomina o tym tylko linia WARN w logu serwera. To potrafi zmylić.
  • Zdarzenia Windows bez _msg. Zdarzenia z Alloy trzymają tekst w polu message, więc zwykły filtr po słowie ich nie znajduje. Filtr na tym polu, na przykład message:timeout, już tak.
  • Długie zakresy na ciężkich strumieniach. Zapytania o siedem dni logów z firewalla lub więcej wymagają najpierw taniego filtra po słowie i dłuższego limitu czasu.

Co zrobiłbym tak samo

Baza logów bez schematu to wygodny magazyn dla każdego strumienia zdarzeń z polami. Warunek jest jeden, czyli stały kontrakt strumienia. Chodzi o te same pola _stream i to samo pole czasu za każdym razem.

Jeśli ścieżka zapisu musi być dostępna z internetu, niech będzie wąska. Powinna mieć jedno wejście, przyjmować tylko zapis, a kontrakt powinien ustawiać serwer, nie nadawca.

Dashboardy testuję najpierw lokalnie, na pliku binarnym VictoriaLogs i podglądzie Grafany. To oszczędza poprawek na produkcji.

Najbardziej podoba mi się to, jak mało potrzeba, żeby dodać nowe źródło. Workflow wysyła JSON przez HTTP i kolejny dashboard ma już swoje dane. Szerszy obraz stacku jest w artykule Grafana to więcej niż wykresy CPU.

Kontakt

Porozmawiajmy.

Jeśli masz projekt, problem z chmurą albo chcesz po prostu porozmawiać o technologii, tutaj mnie znajdziesz.