Zabbix 4.x で API を使って Active な Problem だけを引っ張ってくる方法
内容
色々あってZabbixを使っているわけですが,この手のものを使っているとAPIを介して Active なアラート (Zabbixでいうとproblem) を取りたくなります.
というわけで,Zabbix 4系でそれをやるにはどうしたら良いかということについて記すものです.
problem.get [Zabbix Documentation 4.4]
さてまずドキュメントを漁っていて使えそうに思えるのはこの辺でしょう.
This method is for retrieving unresolved problems.
などとそれっぽいことも書いています……がこのAPIだけ使っても駄目.なぜか Resolved な problem や,退役した host の problem も返脚されてしまいます *1.
実際に欲しいものは Zabbix UI で取得できる unresolved な problems (つまり active な problems) なので,それと等価なクエリを発行してやれば良い……ということで Zabbix Server で tcpdump して, RDBMS に対してどのような SELECT クエリを発行しているかを見ます.
RDBMS (MySQL) に対するトラフィックについて tcpdump する方法については以下のような記事が詳しいでしょう:
実際にやってみると,とてつもない量のクエリが発行されます *2 が,めげずに読み解いてみると
problem
table からフィルタ条件 (severityとか) で WHERE かけて引っ張ってくるtriggers
table から相関サブクエリを用いてtriggerid
,itemid
およびhostid
をどやこやして active な host, trigger, event を取ってくる
という感じで active な problems を抜き出してくる,という雰囲気になっています.tcpdump で実際に取れるクエリはこのような感じ:
SELECT p.eventid,p.objectid,p.clock,p.ns,p.name,p.severity FROM problem p WHERE p.source='0' AND p.object='0' AND p.severity IN ('4','5') AND NOT EXISTS ( SELECT NULL FROM event_suppress es WHERE es.eventid=p.eventid ) AND (p.r_eventid IS NULL OR p.r_clock>1579414413) ORDER BY p.eventid DESC LIMIT 1001 SELECT t.triggerid,t.priority FROM triggers t WHERE t.triggerid IN (/* IDs here */) AND NOT EXISTS ( SELECT NULL FROM functions f, items i, hosts h WHERE t.triggerid=f.triggerid AND f.itemid=i.itemid AND i.hostid=h.hostid AND (i.status<>0 OR h.status<>0) ) AND t.status=0 AND t.flags IN ('0','4')
これを API だけでやろうとすると
- problem.get API を呼んで event ID のリストを引っ張ってくる
- event ID のリストを使って event.get API を呼んで
- trigger ID のリストを引っ張ってくる
- trigger ID => event なマップを作る
- trigger ID のリストを使って trigger.get API を呼んで
- item ID のリストを引っ張ってくる
- trigger ID => trigger なマップを作る
- item ID のリストを使ってitem.get API を呼んで host ID のリストを引っ張ってくる
- この時 item の status が enabled かどうかによってフィルタする
- host ID のリストを使って host.get API を呼んで
- host の status が enable かどうかによってフィルタする
- host に紐付いている triggers を走査して
- trigger ID => trigger なマップから trigger を特定する
- trigger ID => event なマップから event を特定する
とやってやると Zabbix UI で取れる active な problems を 5発の API 呼び出しで抜き出すことが出来ます.めちゃめちゃ大変ですね!!!
おとなしくSQLを発行したほうが楽だったまであります.つらい……
おまけ
箇条書きで挙動を解説してもわけわからないと思うので,めっちゃ雑に TypeScript で書いたコードを適当に残しておきます.雰囲気を察してください:
fetchProblems(severity: ZabbixProblemSeverity[]): {}[] { return this.jsonRPCClient.doRequest("problem.get", { 'output': "extend", 'source': ZabbixProblemSource.createdByTrigger, 'object': ZabbixProblemObject.trigger, 'severities': severity, 'recent': false, 'sortfield': ["eventid"], 'sortorder': ["DESC"] })['result']; } fetchEvents(eventIds: string[]): {}[] { return this.jsonRPCClient.doRequest("event.get", { "output": "extend", "eventids": eventIds, "selectRelatedObject": "refer", })['result']; } fetchTriggers(triggerIds: string[]): {}[] { return this.jsonRPCClient.doRequest("trigger.get", { "output": "extend", "triggerids": triggerIds, "selectItems": "refer", })['result']; } fetchItems(itemIds: string[]): {}[] { return this.jsonRPCClient.doRequest("item.get", { "output": "extend", "itemids": itemIds, "selectHosts": "refer", })['result']; } fetchHosts(hostIds: string[]): {}[] { return this.jsonRPCClient.doRequest("host.get", { "output": "extend", "hostids": hostIds, "selectHosts": "refer", "selectTriggers": "refer", })['result']; } const problems = fetchProblems(this.severity); const eventIds = problems.map(p => p['eventid']); const events = fetchEvents(eventIds); const triggerIds: string[] = []; const triggerId2Event: { [triggerId: string]: {} } = {}; for (const event of events) { const triggerId = event['relatedObject']['triggerid']; triggerIds.push(triggerId); triggerId2Event[triggerId] = event; } const triggers = fetchTriggers(triggerIds); const itemIds: string[] = []; const triggerId2Trigger: { [triggerId: string]: {} } = {}; for (const trigger of triggers) { if (trigger['status'] != ZabbixTriggerStatus.enabled) { continue; } const itemId = trigger['items'][0]['itemid']; if (!itemId) { continue; } itemIds.push(itemId); triggerId2Trigger[trigger['triggerid']] = trigger; } const items = fetchItems(itemIds); const hostIds = items.filter(item => item['status'] == ZabbixItemStatus.enabled) .map(item => item['hosts'][0]['hostid']) .filter(item => item); const hosts = fetchHosts(hostIds); const activeProblems: ZabbixActiveProblem[] = []; hostLoop: for (const host of hosts) { if (host['status'] != ZabbixHostStatus.enabled) { continue; } for (const hostTrigger of host['triggers']) { const triggerId = hostTrigger['triggerid']; const trigger = triggerId2Trigger[triggerId]; if (!trigger) { continue; } const event = triggerId2Event[triggerId]; activeProblems.push(new ZabbixActiveProblem( host['host'], trigger['description'], triggerId, event['eventid'], parseInt(event['severity'], 10), )); if (activeProblems.length >= MAX_PROBLEMS_NUM) { break hostLoop; } } }
*1:この辺が関与してそうだけど深追いはやめた: "This method may return problems of a deleted entity if these problems have not been removed by the housekeeper yet."
*2: Zabbix,problemsの画面を表示するためだけに50件以上のSELECTクエリ発行しててこれマジですごいな