From 045c708c69f1083e5b35861bad7a5e20cfdb2c69 Mon Sep 17 00:00:00 2001 From: Jocelyn Collado-Kuri Date: Tue, 28 Oct 2025 19:57:45 -0700 Subject: [PATCH] Fix: ensure that applicationids parameter only gets passed when the datasource supports it. (#2110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zabbix 5.0.x supported filtering `Problems` feature with `applications`. When this got removed, we removed the filter dropdown from the UI, but failed to check whether applications were supported before sending out the request with the parameters. This was causing dashboards that had been created with zabbix version `5.0.x` to fail when querying with newer versions of our plugin with error: `Invalid params. Invalid parameter "/": unexpected parameter "applicationids".` These changes now ensure that we also check whether applications filter should be supported before sending the backend request to fetch problems. How to test: - use the attached JSON file. This was created using zabbix50 and applying an `applicationids` filter for `Problems` query type OR - run the `zabbix50` test environment: ``` cd devenv/zabbix50 docker-compose up -d ``` - create a dashboard that queries for `Problems` and filters with applications then export the dashboard JSON - stop the `zabbix50` test environment and start the `zabbix74` test environment ``` docker-compose stop cd ../zabbix74 docker-compose up -d ``` - import the dashboard you created above, it should load and work as expected. Bottom panel was created using zabbix50 and it used the application filter. Both panels now load as expected: Screenshot 2025-10-21 at 2 28
25 PM Fixes https://github.com/grafana/grafana-zabbix/issues/1852 --- .changeset/cuddly-cloths-join.md | 5 ++ .../zabbix_api/zabbixAPIConnector.test.ts | 52 +++++++++++++++++++ .../zabbix_api/zabbixAPIConnector.ts | 7 ++- src/datasource/zabbix/zabbix.test.ts | 44 ++++++++++++++++ src/datasource/zabbix/zabbix.ts | 22 ++++++-- 5 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 .changeset/cuddly-cloths-join.md diff --git a/.changeset/cuddly-cloths-join.md b/.changeset/cuddly-cloths-join.md new file mode 100644 index 0000000..70ac665 --- /dev/null +++ b/.changeset/cuddly-cloths-join.md @@ -0,0 +1,5 @@ +--- +'grafana-zabbix': minor +--- + +Fix support of applicationids filters with Zabbix problems for versions older than 5.0.x diff --git a/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.test.ts b/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.test.ts index 1cd3bea..05612aa 100644 --- a/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.test.ts +++ b/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.test.ts @@ -102,6 +102,58 @@ describe('Zabbix API connector', () => { expect(result).toBe(0); }); }); + + describe('getProblems', () => { + it('sends full filter payload with application ids when supported', async () => { + const zabbixAPIConnector = new ZabbixAPIConnector(true, true, 123); + zabbixAPIConnector.version = '7.0.0'; + zabbixAPIConnector.request = jest.fn(() => Promise.resolve([{ eventid: '1' }])); + + await zabbixAPIConnector.getProblems(['21'], ['31'], ['41'], true, { + timeFrom: 100, + timeTo: 200, + recent: 'true', + severities: [3, 4], + limit: 50, + acknowledged: 0, + tags: [{ tag: 'service', value: 'foo' }], + evaltype: 1, + }); + + expect(zabbixAPIConnector.request).toHaveBeenCalledWith('problem.get', { + output: 'extend', + selectAcknowledges: 'extend', + selectSuppressionData: 'extend', + selectTags: 'extend', + source: '0', + object: '0', + sortfield: ['eventid'], + sortorder: 'DESC', + evaltype: 1, + groupids: ['21'], + hostids: ['31'], + applicationids: ['41'], + recent: 'true', + severities: [3, 4], + acknowledged: 0, + tags: [{ tag: 'service', value: 'foo' }], + limit: 50, + time_from: 100, + time_till: 200, + }); + }); + + it('omits applicationids when applications are unsupported', async () => { + const zabbixAPIConnector = new ZabbixAPIConnector(true, true, 123); + zabbixAPIConnector.version = '7.0.0'; + zabbixAPIConnector.request = jest.fn(() => Promise.resolve([{ eventid: '1' }])); + + await zabbixAPIConnector.getProblems(['21'], ['31'], ['41'], false, {}); + + const [, params] = (zabbixAPIConnector.request as jest.Mock).mock.calls.at(-1)!; + expect(params.applicationids).toBeUndefined(); + }); + }); }); const triggers = [ diff --git a/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts b/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts index 6428584..a0cb700 100644 --- a/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts +++ b/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts @@ -482,7 +482,7 @@ export class ZabbixAPIConnector { return sliResponse; } - getProblems(groupids, hostids, applicationids, options): Promise { + getProblems(groupids, hostids, applicationids, supportsApplications, options): Promise { const { timeFrom, timeTo, recent, severities, limit, acknowledged, tags, evaltype } = options; const params: any = { @@ -498,7 +498,6 @@ export class ZabbixAPIConnector { // preservekeys: '1', groupids, hostids, - applicationids, recent, }; @@ -527,6 +526,10 @@ export class ZabbixAPIConnector { params.time_till = timeTo; } + if (supportsApplications) { + params.applicationids = applicationids; + } + return this.request('problem.get', params).then(utils.mustArray); } diff --git a/src/datasource/zabbix/zabbix.test.ts b/src/datasource/zabbix/zabbix.test.ts index 3c35a91..7132369 100644 --- a/src/datasource/zabbix/zabbix.test.ts +++ b/src/datasource/zabbix/zabbix.test.ts @@ -1,4 +1,10 @@ import { Zabbix } from './zabbix'; +import { joinTriggersWithEvents } from '../problemsHandler'; + +jest.mock('../problemsHandler', () => ({ + joinTriggersWithEvents: jest.fn(), + joinTriggersWithProblems: jest.fn(), +})); jest.mock( '@grafana/runtime', @@ -110,4 +116,42 @@ describe('Zabbix', () => { }); }); }); + + describe('getProblemsHistory', () => { + const ctx = { url: 'http://localhost' }; + let zabbix: Zabbix; + + beforeEach(() => { + zabbix = new Zabbix(ctx); + zabbix.getGroups = jest.fn().mockResolvedValue([{ groupid: '21' }]); + zabbix.getHosts = jest.fn().mockResolvedValue([{ hostid: '31' }]); + zabbix.getApps = jest.fn().mockResolvedValue([{ applicationid: '41' }]); + zabbix.supportsApplications = jest.fn().mockReturnValue(true); + zabbix.zabbixAPI.getEventsHistory = jest.fn().mockResolvedValue([{ objectid: '501' }]); + zabbix.zabbixAPI.getTriggersByIds = jest.fn().mockResolvedValue([{ triggerid: '501' }]); + (joinTriggersWithEvents as jest.Mock).mockReturnValue([{ triggerid: '501' }]); + zabbix.filterTriggersByProxy = jest.fn().mockResolvedValue([{ triggerid: '501' }]); + }); + + it('builds the history query and returns filtered triggers', async () => { + const result = await zabbix.getProblemsHistory('group.*', 'host.*', 'app.*', 'proxy-foo', { + valueFromEvent: true, + }); + + expect(zabbix.zabbixAPI.getEventsHistory).toHaveBeenCalledWith(['21'], ['31'], ['41'], { valueFromEvent: true }); + expect(joinTriggersWithEvents).toHaveBeenCalledWith([{ objectid: '501' }], [{ triggerid: '501' }], { + valueFromEvent: true, + }); + expect(zabbix.filterTriggersByProxy).toHaveBeenCalledWith([{ triggerid: '501' }], 'proxy-foo'); + expect(result).toEqual([{ triggerid: '501' }]); + }); + + it('omits applicationids when applications are unsupported', async () => { + (zabbix.supportsApplications as jest.Mock).mockReturnValue(false); + + await zabbix.getProblemsHistory('group.*', 'host.*', 'app.*', undefined, {}); + + expect(zabbix.zabbixAPI.getEventsHistory).toHaveBeenCalledWith(['21'], ['31'], undefined, {}); + }); + }); }); diff --git a/src/datasource/zabbix/zabbix.ts b/src/datasource/zabbix/zabbix.ts index 0dea2c4..8ee899e 100644 --- a/src/datasource/zabbix/zabbix.ts +++ b/src/datasource/zabbix/zabbix.ts @@ -509,7 +509,15 @@ export class Zabbix implements ZabbixConnector { return query; }) - .then((query) => this.zabbixAPI.getProblems(query.groupids, query.hostids, query.applicationids, options)) + .then((query) => + this.zabbixAPI.getProblems( + query.groupids, + query.hostids, + query.applicationids, + this.supportsApplications(), + options + ) + ) .then((problems) => { const triggerids = problems?.map((problem) => problem.objectid); return Promise.all([Promise.resolve(problems), this.zabbixAPI.getTriggersByIds(triggerids)]); @@ -533,7 +541,7 @@ export class Zabbix implements ZabbixConnector { const [filteredGroups, filteredHosts, filteredApps] = results; const query: any = {}; - if (appFilter) { + if (appFilter && this.supportsApplications()) { query.applicationids = _.flatten(_.map(filteredApps, 'applicationid')); } if (hostFilter) { @@ -579,7 +587,15 @@ export class Zabbix implements ZabbixConnector { return query; }) - .then((query) => this.zabbixAPI.getProblems(query.groupids, query.hostids, query.applicationids, options)) + .then((query) => + this.zabbixAPI.getProblems( + query.groupids, + query.hostids, + query.applicationids, + this.supportsApplications(), + options + ) + ) .then((problems) => findByFilter(problems, triggerFilter)) .then((problems) => { const triggerids = problems?.map((problem) => problem.objectid);