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
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.
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 standardowyloki.writew 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
- run_id r-1018 · 18.10api.example.com · 14 dnivpn.example.com · 33 dnimail.example.com · 61 dnishop.example.com · 90 dni
- run_id r-1019 · 19.10api.example.com · 13 dnivpn.example.com · 32 dnimail.example.com · 60 dnishop.example.com · 89 dni
- 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
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
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łycount()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 polax. Zapismedian(x) if (x:*)ogranicza liczenie do wierszy, które to pole mają. - Surowsze zapytania w panelach. Zapytanie
statsw panelu musi kończyć się nastatsalbomath. Zapytania zakresowe do wykresów odrzucałylastijoin. - 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_timeprzezmathna nanosekundach, co uwzględnia też zmianę czasu. - 200 i nic nie zapisane. POST bez nagłówka
Content-Type: application/stream+jsondostawał 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 polumessage, więc zwykły filtr po słowie ich nie znajduje. Filtr na tym polu, na przykładmessage: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.