feat: optimize Zabbix queries and enhance specialized device support
- Implement query optimization (reduced output, parameter skipping) to minimize Zabbix API traffic. - Add indirect dependency handling: deviceType implies tags and state implies items. - Move schema extensions to samples/extensions/ to clarify their role as samples. - Enhance DistanceTrackerDevice with String time fields to support optional date portions. - Ensure allDevices strictly filters by deviceType and populates the field in results. - Refactor runAllRegressionTests mutation to use internal unique names and improve stability. - Fix unnecessary Zabbix API calls for item preprocessing during template and host imports. - Update documentation including cookbook recipes, test specifications, and optimization guides. - Add extensive unit, integration, and regression tests covering all implemented changes. - Update docker-compose.yml to mount the samples/ directory as a volume. - Update IntelliJ .idea run configurations to reflect the new sample extension paths.
This commit is contained in:
parent
97a0f70fd6
commit
b646b8c606
28 changed files with 551 additions and 74 deletions
|
|
@ -57,8 +57,8 @@ describe("Host and HostGroup Resolvers", () => {
|
|||
}));
|
||||
});
|
||||
|
||||
test("allDevices query", async () => {
|
||||
const mockDevices = [{ hostid: "2", host: "Device 1" }];
|
||||
test("allDevices query - with hostid", async () => {
|
||||
const mockDevices = [{ hostid: "2", host: "Device 1", deviceType: "GenericDevice" }];
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockDevices);
|
||||
|
||||
const args: QueryAllDevicesArgs = { hostids: 2 };
|
||||
|
|
@ -74,7 +74,63 @@ describe("Host and HostGroup Resolvers", () => {
|
|||
body: expect.objectContaining({
|
||||
method: "host.get",
|
||||
params: expect.objectContaining({
|
||||
hostids: 2
|
||||
hostids: 2,
|
||||
tags: expect.arrayContaining([{
|
||||
tag: "deviceType",
|
||||
operator: 4
|
||||
}])
|
||||
})
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
test("allDevices query - with deviceType filter", async () => {
|
||||
const mockDevices = [{ hostid: "2", host: "Device 1", deviceType: "SomeType" }];
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockDevices);
|
||||
|
||||
const args: QueryAllDevicesArgs = { tag_deviceType: ["SomeType"] };
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
|
||||
const result = await resolvers.Query.allDevices(null, args, context);
|
||||
|
||||
expect(result).toEqual(mockDevices);
|
||||
expect(zabbixAPI.post).toHaveBeenCalledWith("host.get.with_items", expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
tags: expect.arrayContaining([{
|
||||
tag: "deviceType",
|
||||
operator: 1,
|
||||
value: "SomeType"
|
||||
}])
|
||||
})
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
test("allDevices query - ensures deviceType exists if no filter provided", async () => {
|
||||
const mockDevices = [{ hostid: "3", host: "Device with tag", deviceType: "SomeType" }];
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce(mockDevices);
|
||||
|
||||
const args: QueryAllDevicesArgs = {};
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
|
||||
const result = await resolvers.Query.allDevices(null, args, context);
|
||||
|
||||
expect(result).toEqual(mockDevices);
|
||||
expect(zabbixAPI.post).toHaveBeenCalledWith("host.get.with_items", expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
method: "host.get",
|
||||
params: expect.objectContaining({
|
||||
tags: expect.arrayContaining([{
|
||||
tag: "deviceType",
|
||||
operator: 4
|
||||
}])
|
||||
})
|
||||
})
|
||||
}));
|
||||
|
|
|
|||
|
|
@ -163,6 +163,37 @@ describe("Query Optimization", () => {
|
|||
expect(callParams.output).toContain("items");
|
||||
});
|
||||
|
||||
test("allHosts optimization - keep selectTags when deviceType requested", async () => {
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
||||
|
||||
const args: QueryAllHostsArgs = {};
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
const info = {
|
||||
fieldNodes: [{
|
||||
selectionSet: {
|
||||
selections: [
|
||||
{ kind: 'Field', name: { value: 'hostid' } },
|
||||
{ kind: 'Field', name: { value: 'deviceType' } }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
await resolvers.Query.allHosts(null, args, context, info);
|
||||
|
||||
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
output: ["hostid"],
|
||||
selectTags: expect.any(Array)
|
||||
})
|
||||
})
|
||||
}));
|
||||
});
|
||||
|
||||
test("allDevices optimization - skip items when not requested", async () => {
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
||||
|
||||
|
|
|
|||
173
src/test/schema_dependent_queries.test.ts
Normal file
173
src/test/schema_dependent_queries.test.ts
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
import {ApolloServer} from '@apollo/server';
|
||||
import {schema_loader} from '../api/schema.js';
|
||||
import {readFileSync} from 'fs';
|
||||
import {join} from 'path';
|
||||
import {zabbixAPI} from '../datasources/zabbix-api.js';
|
||||
import {Config} from "../common_utils.js";
|
||||
|
||||
describe("Schema-dependent Queries Integration Tests", () => {
|
||||
let server: ApolloServer;
|
||||
let postSpy: jest.SpyInstance;
|
||||
|
||||
let originalSchemas: any;
|
||||
let originalResolvers: any;
|
||||
let originalApiVersion: any;
|
||||
|
||||
beforeAll(async () => {
|
||||
originalSchemas = Config.ADDITIONAL_SCHEMAS;
|
||||
originalResolvers = Config.ADDITIONAL_RESOLVERS;
|
||||
originalApiVersion = Config.API_VERSION;
|
||||
|
||||
// We need to bypass the static readonly nature of Config for this test.
|
||||
Object.defineProperty(Config, 'ADDITIONAL_SCHEMAS', {
|
||||
value: "./samples/extensions/location_tracker_devices.graphql,./samples/extensions/location_tracker_commons.graphql,./samples/extensions/display_devices.graphql",
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(Config, 'ADDITIONAL_RESOLVERS', {
|
||||
value: "DistanceTrackerDevice,SinglePanelDevice",
|
||||
configurable: true
|
||||
});
|
||||
Object.defineProperty(Config, 'API_VERSION', {
|
||||
value: "1.2.3",
|
||||
configurable: true
|
||||
});
|
||||
|
||||
const schema = await schema_loader();
|
||||
server = new ApolloServer({
|
||||
schema,
|
||||
});
|
||||
|
||||
postSpy = jest.spyOn(zabbixAPI, 'post');
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
postSpy.mockRestore();
|
||||
Object.defineProperty(Config, 'ADDITIONAL_SCHEMAS', { value: originalSchemas });
|
||||
Object.defineProperty(Config, 'ADDITIONAL_RESOLVERS', { value: originalResolvers });
|
||||
Object.defineProperty(Config, 'API_VERSION', { value: originalApiVersion });
|
||||
});
|
||||
|
||||
test("TC-SCHEMA-01: DistanceTrackerDevice Comprehensive Query", async () => {
|
||||
const filePath = join(process.cwd(), 'docs', 'queries', 'sample_distance_tracker_test_query.graphql');
|
||||
const content = readFileSync(filePath, 'utf-8').replace(/\r\n/g, '\n');
|
||||
|
||||
const queryMatch = content.match(/```graphql\n([\s\S]*?)\n```/);
|
||||
if (!queryMatch) {
|
||||
throw new Error(`No graphql block found in sample query file`);
|
||||
}
|
||||
const query = queryMatch[1];
|
||||
|
||||
// Setup mock responses for Zabbix API
|
||||
postSpy.mockImplementation((method: string) => {
|
||||
if (method === 'apiinfo.version') return Promise.resolve("7.4.0");
|
||||
if (method.startsWith('hostgroup.get')) {
|
||||
return Promise.resolve([
|
||||
{ groupid: "1", name: "Roadwork/Devices/Tracker" }
|
||||
]);
|
||||
}
|
||||
if (method.startsWith('host.get')) {
|
||||
return Promise.resolve([
|
||||
{
|
||||
hostid: "10001",
|
||||
host: "TRACKER_01",
|
||||
name: "Distance Tracker 01",
|
||||
deviceType: "DistanceTrackerDevice", // Manually mapped because we mock post()
|
||||
tags: [
|
||||
{ tag: "deviceType", value: "DistanceTrackerDevice" }
|
||||
],
|
||||
items: [
|
||||
{ itemid: "1", name: "Count", key_: "state.current.count", lastvalue: "5", lastclock: 1704103200, value_type: "3" },
|
||||
{ itemid: "2", name: "Time From", key_: "state.current.timeFrom", lastvalue: "2024-01-01T10:00:00Z", lastclock: 1704103200, value_type: "4" },
|
||||
{ itemid: "3", name: "Time Until", key_: "state.current.timeUntil", lastvalue: "2024-01-01T11:00:00Z", lastclock: 1704103200, value_type: "4" }
|
||||
],
|
||||
inheritedTags: []
|
||||
},
|
||||
{
|
||||
hostid: "10003",
|
||||
host: "TRACKER_02",
|
||||
name: "Distance Tracker 02",
|
||||
deviceType: "DistanceTrackerDevice",
|
||||
tags: [
|
||||
{ tag: "deviceType", value: "DistanceTrackerDevice" }
|
||||
],
|
||||
items: [
|
||||
{ itemid: "10", name: "Count", key_: "state.current.count", lastvalue: "10", lastclock: 1704103200, value_type: "3" },
|
||||
{ itemid: "11", name: "Time From", key_: "state.current.timeFrom", lastvalue: "09:58:09", lastclock: 1704103200, value_type: "4" }
|
||||
],
|
||||
inheritedTags: []
|
||||
},
|
||||
{
|
||||
hostid: "10004",
|
||||
host: "TRACKER_03",
|
||||
name: "Distance Tracker 03",
|
||||
deviceType: "DistanceTrackerDevice",
|
||||
tags: [
|
||||
{ tag: "deviceType", value: "DistanceTrackerDevice" }
|
||||
],
|
||||
items: [
|
||||
{ itemid: "20", name: "Count", key_: "state.current.count", lastvalue: "0", lastclock: 1704103200, value_type: "3" },
|
||||
{ itemid: "21", name: "Time From", key_: "state.current.timeFrom", lastvalue: "", lastclock: 1704103200, value_type: "4" }
|
||||
],
|
||||
inheritedTags: []
|
||||
},
|
||||
{
|
||||
hostid: "10002",
|
||||
host: "DISPLAY_01",
|
||||
name: "LED Display 01",
|
||||
deviceType: "SinglePanelDevice", // Manually mapped because we mock post()
|
||||
tags: [
|
||||
{ tag: "deviceType", value: "SinglePanelDevice" }
|
||||
],
|
||||
items: [
|
||||
{ itemid: "4", name: "Content", key_: "state.current.values.1.contentText", lastvalue: "Roadwork Ahead", lastclock: 1704103200, value_type: "4" }
|
||||
],
|
||||
inheritedTags: []
|
||||
}
|
||||
]);
|
||||
}
|
||||
return Promise.resolve([]);
|
||||
});
|
||||
|
||||
const response = await server.executeOperation({
|
||||
query: query,
|
||||
}, {
|
||||
contextValue: { zabbixAuthToken: 'test-token', dataSources: { zabbixAPI: zabbixAPI } }
|
||||
});
|
||||
|
||||
if (response.body.kind === 'single') {
|
||||
const result = response.body.singleResult;
|
||||
if (result.errors) {
|
||||
console.error(`Errors in query:`, JSON.stringify(result.errors, null, 2));
|
||||
}
|
||||
expect(result.errors).toBeUndefined();
|
||||
|
||||
const data = result.data as any;
|
||||
expect(data.apiVersion).toBe("1.2.3");
|
||||
expect(data.zabbixVersion).toBe("7.4.0");
|
||||
expect(data.allHostGroups).toHaveLength(1);
|
||||
expect(data.allDevices).toBeDefined();
|
||||
|
||||
// Verify DistanceTrackerDevice resolution
|
||||
const tracker = data.allDevices.find((d: any) => d.host === "TRACKER_01");
|
||||
expect(tracker.deviceType).toBe("DistanceTrackerDevice");
|
||||
expect(tracker.state.current.count).toBe(5);
|
||||
expect(tracker.state.current.timeFrom).toBe("2024-01-01T10:00:00Z");
|
||||
|
||||
const tracker02 = data.allDevices.find((d: any) => d.host === "TRACKER_02");
|
||||
expect(tracker02.state.current.count).toBe(10);
|
||||
expect(tracker02.state.current.timeFrom).toBe("09:58:09");
|
||||
|
||||
const tracker03 = data.allDevices.find((d: any) => d.host === "TRACKER_03");
|
||||
expect(tracker03.state.current.timeFrom).toBe("");
|
||||
|
||||
// Verify allHosts with fragments
|
||||
const trackerInHosts = data.allHosts.find((h: any) => h.host === "TRACKER_01");
|
||||
expect(trackerInHosts.state.current.count).toBe(5);
|
||||
|
||||
const displayInHosts = data.allHosts.find((h: any) => h.host === "DISPLAY_01");
|
||||
expect(displayInHosts.deviceType).toBe("SinglePanelDevice");
|
||||
} else {
|
||||
throw new Error(`Unexpected response kind: ${response.body.kind}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue