Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
Konferencja Mega Sekurak Hacking Party w Krakowie – 26-27 października!
Adminie… Czy znamy Twoje grzechy? ;-) Sprawdź!
W poprzednim artykule pokazaliśmy parser UQL — narzędzie, które pozwala w miarę bezboleśnie uporządkować odpowiedzi z API Zabbix i doprowadzić je do postaci akceptowalnej przez Grafanę (i ludzi). Dla wielu przypadków to w zupełności wystarcza i często jest najlepszym możliwym wyborem.
Są jednak sytuacje, w których odpowiedź z API zaczyna żyć własnym życiem: struktury robią się głębokie, warunki bardziej złożone, a liczba kroków w UQL zaczyna wyglądać podejrzanie. To właśnie w takich momentach pojawia się pytanie: „czy da się to zrobić inaczej?”.
Odpowiedzią jest JSONata — parser, który pozwala bardzo precyzyjnie pracować na strukturach JSON i wyciągać z nich dokładnie to, czego potrzebujemy, nawet jeśli wymaga to bardziej złożonej składni.
Jeżeli nie masz jeszcze za sobą artykułów o konfiguracji pluginu Infinity oraz parserze UQL, warto najpierw sięgnąć do >pierwszego oraz >drugiego artykułu. W przeciwnym razie — przechodzimy dalej.
Zaczynamy!
Na początek przypomnijmy sobie testowe środowisko:

Dodatkowe założenia w tym przykładzie:
Dalej będziemy bazować na stworzonym dashboardzie Przykładowy dashboard 3, na którym aktualnie znajduje się pojedynczy panel z informacjami o niedostępności interfejsów na hostach:

JSONata to lekki i niezwykle elastyczny język do zapytań i transformacji danych JSON. W podstawowym użyciu może wydawać się prosty – pozwala w łatwy sposób wybrać pola z tablic (trochę jak JSONPath czy XPath dla XML-a.). Jednak pod tą pozorną prostotą kryje się ogromna moc – JSONata pozwala również na zaawansowane operacje: mapowanie, grupowanie, łączenie danych z różnych miejsc JSON-a i tworzenie kompletnie nowych struktur wyjściowych.
W przeciwieństwie do UQL, JSONata może działać zarówno w przeglądarce (frontend), jak i po stronie serwera Grafany (backend). To właśnie w wersji backendowej mamy dostęp do dodatkowych funkcji Grafany – takich jak alerting, cache’owanie wyników czy inne mechanizmy, które nie działają w trybie przeglądarki klienta.
W praktyce oznacza to, że idealnie nadaje się do bardziej złożonych przekształceń, takich jak np. pobranie statusów agentów aktywnych, których brakowało nam w poprzednim artykule.
Dlatego tym razem stworzymy listę z niedostępnymi interfejsami aktywnymi dla grupy Linux servers. W tym miejscu zastosujemy również uproszczenie z poprzedniego artykułu – znamy groupid naszej grupy, w naszym przykładzie to 2. By pokazać jak trudny jest to przykład, dodajmy testowo do naszego środowiska nowego hosta tylko_pasywne, który – jak sama nazwa wskazuje – posiada tylko interfejs Zabbix Agent w trybie pasywnym:

Wracamy więc do edycji dashboardu Przykładowy dashboard 3 i tworzymy nowy panel. Dla przypomnienia – konfiguracja zapytania wygląda podobnie jak w przypadku UQL:
URL – jeśli źródło danych jest skonfigurowane, pole można pozostawić puste

W tym przykładzie wykorzystujemy ponownie funkcję host.get, ale w odróżnieniu od zapytania dla UQL zmieniamy zakres zwracanych danych. Chcemy pobrać:
Całe zapytanie wygląda następująco:
{
"jsonrpc": "2.0",
"method": "host.get",
"params": {
"groupids": 2,
"output": ["host", "active_available"],
"filter": { "status": 0 }
},
"id": 1
}
Pozostałe pola, takie jak jsonrpc, groupids, filter czy id, działają dokładnie tak samo jak w poprzednim artykule, więc nie będziemy ich powtarzać.
Wystarczy skopiować powyższe zapytanie do sekcji Body panelu w Grafanie i pamiętać o ustawieniu Body Content Type na JSON – reszta konfiguracji pozostaje bez zmian.
Teraz pozostaje wyciągnąć z odpowiedzi tylko te pola, które nas interesują. W JSONacie można to zrobić naprawdę zwięźle — wystarczy jedno krótkie wyrażenie:
$.result.{ "host": host, "stan interfejsu aktywnego": active_available }
Co tu się dzieje?
$.result
Przechodzimy do tablicy result, którą Zabbix API zwraca przy każdym wywołaniu.
{ "host": host, "stan interfejsu aktywnego": active_available }
Dla każdego elementu w tej tablicy budujemy nowy obiekt, zawierający wyłącznie dwa pola:
W efekcie JSONata przepisuje odpowiedź API na czystą, minimalną strukturę, idealną do wizualizacji w Grafanie — bez nadmiarowych pól, ręcznego filtrowania czy kombinowania.
Otrzymana tabela powinna wyglądać następująco:

Ale moment… dlaczego na liście pojawia się host tylko_pasywne? Przecież nie ma on aktywnych interfejsów.
Odpowiedź brzmi: tak to zostało zaprojektowane.
Metoda host.get zawsze zwraca pole active_available, niezależnie od tego, czy host faktycznie używa aktywnego trybu agenta. Zabbix w ten sposób nie weryfikuje istnienia „interfejsu aktywnego”. Pole te pokazuje ostatni stan interfejsu aktywnego.
Jeżeli host nigdy nie posiadał sprawdzeń aktywnych, to wartość ta zawsze będzie ustawiona na 0. Natomiast posiadając hosta z działającymi sprawdzeniami aktywnym, jeżeli usuniemy wszystkie pozycje z typem Zabbix Agent (active) – z poziomu GUI Zabbix, host ten nie będzie posiadał już interfejsu aktywnego, jednak w zapytaniu API dla host.get pole active_available nadal będzie ustawione na 1 (ostatni znany stan).
No dobrze, w takim razie jak Zabbix rozpoznaje, czy host faktycznie posiada interfejs aktywny?
W dużym uproszczeniu wykonuje następującą logikę: przegląda wszystkie pozycje przypisane do hosta i szuka tych, które mają typ Zabbix agent (active) oraz są włączone. Jeśli znajdzie choć jedną taką pozycje — traktuje host jako posiadający aktywny interfejs i raportuje faktyczny stan w active_available.
Mając już świadomość, jak działa pole active_available, możemy zmodyfikować nasze zapytanie API, aby sprawdzić nie tylko sam status interfejsu aktywnego, ale też uzyskać szczegóły dotyczące przypisanych pozycji. Dzięki temu będziemy mogli dokładniej ocenić, które hosty faktycznie mają aktywne sprawdzenia. Dodatkowo dla każdej pozycji będziemy pobierać również czas ostatniego odczytu – w ten sposób sprawdzimy, kiedy nastąpiło najświeższe sprawdzenie (zgodnie z znaną nam już wcześniej zasadą “tylko głupi by nie skorzystał”). Pozwoli to też na weryfikację opóźnień w przesyłaniu danych (kto nigdy nie zapomniał ustawić alarmu na brak odebranych danych niech pierwszy rzuci kamieniem).
Uwaga od Autora – przy bardzo dużych odpowiedziach API Grafana może działać wolniej i zużywać sporo zasobów. Jeśli planujesz obsługiwać setki tysięcy hostów, warto ograniczyć wyniki po stronie API, np. poprzez filtry, grupy lub limity.
Nasze zmodyfikowane zapytanie wygląda następująco:
{
"jsonrpc": "2.0",
"method": "host.get",
"params": {
"groupids": 2,
"output": ["host", "active_available"],
"selectItems": ["type", "lastclock", "status"],
"filter": { "status": 0 }
},
"id": 1
}
Krótki opis poszczególnych elementów zapytania:
"groupids": 2,
Pobieramy hosty tylko z grupy Linux server (groupid = 2).
"output": ["host", "active_available"],
Pobieramy nazwę hosta oraz ostatni znany stan interfejsu aktywnego.
"selectItems": ["type", "lastclock", "status"],
Prosimy API o listę pozycji przypisanych do hosta wraz z typem, czasem ostatniego odczytu i statusem włączony/wyłączony. Dzięki temu łatwo sprawdzimy, czy host posiada włączone pozycje typu Zabbix Agent (active).
"filter": { "status": 0 }
Ograniczamy wynik do hostów włączonych (status=0).
Dzięki tej modyfikacji uzyskamy nie tylko listę hostów i stan ich aktywnych interfejsów, ale również szczegółowe informacje o przypisanych itemach. Pozwala to na dokładniejszą analizę, które hosty mają aktywne sprawdzenia, bez ryzyka błędnej interpretacji pola active_available.
Na tym etapie powinniśmy otrzymać następujący wynik:

Mając już pełną odpowiedź API wraz z listą pozycji dla każdego hosta, możemy przetworzyć te dane w Grafanie, aby uzyskać czytelną i minimalną tabelę z interesującymi nas informacjami. W naszym przykładzie chcemy:
Do tego celu użyjemy już tylko trochę bardziej skomplikowanego kodu JSONata — taki, który pozwoli nam wszystko to zrobić bez dodatkowych transformat:
$map(
$filter(result, function($h){
$count($filter($h.items ? $h.items : [], function($it){
$it.type = "7" and $it.status = "0"
})) > 0
}),
function($h){
{
"host": $h.host,
"stan interfejsu aktywnego": $h.active_available,
"ostatni odczyt": (
$vals := $filter($h.items ? $h.items : [], function($it){
$it.type = "7" and $it.status = "0"
}).lastclock;
$nums := $map($vals, function($v){ $number($v) });
$maxv := $max($nums);
$maxv > 0 ? $maxv * 1000 : null
)
}
}
)
No dobra, może nie jest to już taki „lekki” przykład, który da się ogarnąć jednym rzutem oka. Kod jest wyraźnie bardziej rozbudowany, dlatego rozbijmy go na mniejsze fragmenty i przejdźmy przez całość krok po kroku.
Zaczynamy od głównej konstrukcji:
$map(... ,function($h){ ... })
Funkcja $map przechodzi po każdym elemencie wejściowej tablicy (pierwszy argument funkcji) i dla każdego z nich tworzy nowy obiekt wynikowy (drugi argument funkcji). Zmienna $h reprezentuje pojedynczego hosta i pozwala nam w czytelny sposób odwoływać się do jego pól w dalszej części wyrażenia. W praktyce oznacza to, że tworzymy jedną tabelę wynikową, w której każdy wiersz odpowiada jednemu hostowi. Źródłem danych dla $map jest tutaj funkcja filtrująca:
$filter(result, function($h){...})
W pierwszym argumencie funkcji $map posiadamy funkcję $filter, , aby zawęzić listę hostów tylko do tych, które spełniają określony warunek. Pierwszy argument result to nic innego jak domyślny obiekt który zawsze jest zwracany przez Zabbix API (czyli obiekty hostów z odpowiedzi API).
No dobrze, ale według jakiego kryterium filtrujemy hosty? Odpowiada za to poniższa funkcja:
function($h){
$count($filter($h.items ? $h.items : [], function($it){
$it.type = "7" and $it.status = "0"
})) > 0
}
Rozłóżmy ją na prostsze elementy.
$h.items ? $h.items : []
To proste zabezpieczenie: jeśli host posiada przypisane pozycje (items), pracujemy na tej tablicy; jeśli nie — podstawiamy pustą tablicę. Dzięki temu unikamy błędów przy przetwarzaniu hostów, które nie mają żadnych pozycji. Zabezpieczenie to wykorzystujemy potem w funkcji filtrującej:
$filter(<powyższe zabezpieczenie>, function($it){
$it.type = "7" and $it.status = "0"
})
W tej funkcji filtrujemy pozycje hosta, zostawiając wyłącznie te, które:
W praktyce oznacza to, że interesują nas tylko włączone sprawdzenia typu Zabbix Agent (acive). Jeżeli interesuje Cię więcej informacji o obiekcie pozycj, to więcej szczegółów możesz przeczytać w >oficjalnej dokumentacji.
$count (<powyższy filtr>) > 0
Na końcu zliczamy, ile takich pozycji pozostało po filtracji. Jeżeli wynik jest większy od zera, host przechodzi dalej.
Efekt końcowy jest prosty: do głównej funkcji $map trafiają wyłącznie hosty, które mają co najmniej jedną włączoną pozycję typu Zabbix Agent (active). Dzięki temu dalsza analiza opiera się już tylko na hostach, które faktycznie korzystają z aktywnego trybu agenta, a nie na samym polu active_available.
Kiedy mamy już listę hostów ograniczoną wyłącznie do tych z aktywnymi sprawdzeniami, możemy przejść do budowania obiektów wynikowych:
function($h){
{
"host": $h.host,
"stan interfejsu aktywnego": $h.active_available,
"ostatni odczyt": (<na razie pomijamy>)
}
}
Dla każdego przefiltrowanego hosta tworzymy jeden obiekt zawierający trzy pola:
Logika wyznaczania pola ostatni odczyt jest nieco bardziej złożona, dlatego również warto ją rozłożyć na etapy.
$vals := $filter($h.items ? $h.items : [], function($it){
$it.type = "7" and $it.status = "0"
}).lastclock;
Na początku ponownie filtrujemy listę itemów hosta, zostawiając wyłącznie włączone pozycje typu Zabbix Agent (active). Z każdego z nich pobieramy pole lastclock.
W efekcie zmienna $vals zawiera tablicę wartości lastclock — na tym etapie są to jeszcze ciągi znaków. Jeżeli host ma kilka aktywnych pozycji, w tablicy znajdzie się kilka wartości.
$nums := $map($vals, function($v){ $number($v) });
Następnie konwertujemy wszystkie wartości na typ numeryczny za pomocą funkcji $number. Po tej operacji pracujemy już na tablicy liczb (a dokładniej – timestampów).
$maxv := $max($nums);
Z tak przygotowanej tablicy wybieramy największą wartość — odpowiada ona najświeższemu odczytowi spośród wszystkich aktywnych pozycji hosta.
$maxv > 0 ? $maxv * 1000 : null
Na końcu:
W ten sposób dla każdego hosta otrzymujemy pojedynczy, jednoznaczny timestamp reprezentujący moment ostatniego faktycznego odczytu danych z aktywnego agenta — dokładnie to, czego potrzebujemy do dalszej analizy i wizualizacji.
Aby jeszcze lepiej zrozumieć działanie tego wyrażenia JSONata, przeanalizujmy przykładowe dane wejściowe zwrócone przez Zabbix API:
{
"jsonrpc": "2.0",
"result": [
{
"host": "web-01",
"hostid": "10101",
"active_available": "1",
"items": [
{ "lastclock": "1765000000", "status": "0", "type": "7" },
{ "lastclock": "1764990000", "status": "0", "type": "7" },
{ "lastclock": "1764880000", "status": "1", "type": "7" },
{ "lastclock": "1764000000", "status": "0", "type": "0" }
]
},
{
"host": "db-01",
"hostid": "10102",
"active_available": "0",
"items": [
{ "lastclock": "1764800000", "status": "0", "type": "0" },
{ "lastclock": "1764700000", "status": "0", "type": "5" }
]
},
{
"host": "app-01",
"hostid": "10103",
"active_available": "0",
"items": [
{ "lastclock": "0", "status": "0", "type": "7" }
]
}
],
"id": 1
}
W dużym skrócie:
Wracając do naszego kodu JSONata – pierwszym krokiem jest filtracja hostów. Zostawiamy tylko te, które posiadają co najmniej jedną włączoną pozycję typu Zabbix Agent (active), czyli:
Na tym etapie:
Po tej filtracji otrzymujemy (wynik skrócono dla czytelności):
[
{
"host": "web-01",
"hostid": "10101",
"active_available": "1",
"items": [<4 pozycje>]
},
{
"host": "app-01",
"hostid": "10103",
"active_available": "0",
"items": [<1 pozycja>]
}
]
Następnie, zgodnie z główną funkcją $map, z każdego hosta tworzymy docelowy obiekt zawierający następujące pola:
Na tym etapie struktura wygląda następująco:
[
{
"host": "web-01",
"stan interfejsu aktywnego": "1",
"ostatni odczyt": <zostanie obliczone>
},
{
"host": "app-01",
"stan interfejsu aktywnego": "0",
"ostatni odczyt": <zostanie obliczone>
}
]
Ostatnim etapem jest obliczanie pola ostatni odczyt.
Host web-01:
[ { "lastclock": "1765000000", "status": "0", "type": "7" }, |
["1765000000", "1764990000"] |
[1765000000, 1764990000] |
1765000000 |
1765000000 * 1000 = 1765000000000 |
Host app-01:
[ { "lastclock": "0", "status": "0", "type": "7" } |
["0"] |
| [0] |
null |
Po pełnym przetworzeniu danych otrzymujemy:
[
{
"host": "web-01",
"stan interfejsu aktywnego": "1",
"ostatni odczyt": 1765000000000
},
{
"host": "app-01",
"stan interfejsu aktywnego": "0",
"ostatni odczyt": null
}
]
To dokładnie ta, minimalna i jednoznaczna struktura, którą możemy bezpośrednio wykorzystać w Grafanie do dalszej analizy i wizualizacji.
Skoro część teoretyczną mamy już za sobą, możemy przejść do praktyki. Wklejamy całe wyrażenie JSONata do pola Parsing option & Result fields.

Od razu widać, że wszystko działa zgodnie z założeniami:
Na tym etapie pozostaje jedynie poprawić czytelność prezentacji danych w Grafanie.W okienku transformat dodajmy jedną o nazwie Convert field type. W jej konfiguracji w polu Field wybierzmy kolumnę ostatni odczyt – dzięki temu Grafana zacznie traktować tę kolumnę jako znacznik czasu i zamiast surowego timestampu wyświetli czytelną datę.



Na koniec możemy dostosować wygląd całej tabeli do własnych preferencji. Dobrym pomysłem jest np. włączenie filtrowania kolumn poprzez opcję Table – Column filter. Pozwala to szybko zawęzić widok, np. tylko do hostów ze statusem nieznany lub niedostępny.
Koniec! Możemy wyjść z konfiguracji panelu (pamiętając o zapisaniu zmian), a na dashboardzie powinna pojawić się gotowa tabela z listą hostów i stanem ich aktywnych interfejsów. Jeśli tabela wydaje się zbyt wąska — warto po prostu poszerzyć kafelek panelu.

Całość dashboardu Przykładowy dashboard 3 przedstawia się następująco:

Kilka praktycznych uwag, które warto mieć z tyłu głowy podczas pracy z JSONatą:
W tym artykule pokazaliśmy, jak przy użyciu parsera JSONata przetwarzać odpowiedzi API Zabbixa w Grafanie w sytuacjach, w których UQL zaczyna być niewystarczający. JSONata dobrze sprawdza się tam, gdzie logika przetwarzania danych jest bardziej złożona, a odpowiedź API wymaga realnej analizy, a nie tylko prostego filtrowania. Nie jest to parser najprostszy w czytaniu ani debugowaniu, ale w zamian daje dużą elastyczność i możliwość zamknięcia całej logiki w jednym miejscu.
W następnym artykule skupimy się na ostatnim parserze – JQ, który cieszy się dużą popularnością i jest szeroko stosowany do przetwarzania danych JSON w różnych środowiskach. Tym razem skupimy się na zbieraniu wszystkich błędnych pozycji (wraz z opisem błędu), i pogrupujemy je według rodzaju problemu.
Jeżeli artykuł okazał się wartościowy i pozwolił Ci spojrzeć na przetwarzanie danych z API Zabbixa w nowy sposób, możesz pokazać swoje uznanie, zostawiając autorowi >dobrą kawkę!
Jednocześnie zapraszamy na nasze nowe duże szkolenie Zabbix Expert z ponad 70% rabatem! Zapisy tutaj: https://zabbix.sekurak.pl
~ Albert Przybylski, zawodowo: Architekt ds. Monitoringu w firmie Aplitt, prywatnie: pełnoprawny fanatyk Zabbixa zasilany kawą