feat(query-optimization): implement GraphQL query optimization and enhance regression suite
- **Optimization**: Implemented automatic Zabbix parameter optimization by analyzing GraphQL selection sets. - **ZabbixRequest**: Added optimizeZabbixParams with support for skippable parameters and implied field dependencies (e.g., state -> items). - **Resolvers**: Updated allHosts, allDevices, allHostGroups, and templates to pass requested fields to data sources. - **Data Sources**: Optimized ZabbixQueryHostsGenericRequest and ZabbixQueryTemplatesRequest to skip unnecessary Zabbix API calls. - **Regression Tests**: Enhanced RegressionTestExecutor with new tests for optimization (REG-OPT, REG-OPT-NEG), state retrieval (REG-STATE), dependent items (REG-DEP), and empty results (REG-EMPTY). - **Documentation**: Created query_optimization.md How-To guide and updated roadmap.md, README.md, and tests.md. - **Bug Fixes**: Fixed deviceType tag assignment during host import and corrected ZabbixCreateHostRequest to support tags.
This commit is contained in:
parent
ad104acde2
commit
97a0f70fd6
16 changed files with 835 additions and 69 deletions
|
|
@ -7,6 +7,9 @@ This directory contains detailed guides on how to use and extend the Zabbix Grap
|
|||
### 🍳 [Cookbook](./cookbook.md)
|
||||
Practical, step-by-step recipes for common tasks, designed for both humans and AI-based test generation.
|
||||
|
||||
### ⚡ [Query Optimization](./query_optimization.md)
|
||||
Learn how the API optimizes Zabbix requests by reducing output fields and skipping unnecessary parameters based on the GraphQL query.
|
||||
|
||||
### 📊 [Schema and Schema Extension](./schema.md)
|
||||
Learn about the GraphQL schema structure, how Zabbix entities map to GraphQL types, and how to use the dynamic schema extension system.
|
||||
|
||||
|
|
|
|||
51
docs/howtos/query_optimization.md
Normal file
51
docs/howtos/query_optimization.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# ⚡ Query Optimization
|
||||
|
||||
This document describes how the Zabbix GraphQL API optimizes queries to reduce data fetching from Zabbix, improving performance and reducing network load.
|
||||
|
||||
## 🚀 Overview
|
||||
|
||||
The optimization works by analyzing the requested GraphQL fields and only fetching the necessary data from the Zabbix API. This is achieved through:
|
||||
- **Output Reduction**: Dynamically setting the `output` parameter in Zabbix requests.
|
||||
- **Parameter Skipping**: Automatically removing optional Zabbix parameters (like `selectTags` or `selectItems`) if the corresponding GraphQL fields are not requested.
|
||||
|
||||
## 🏗️ Implementation Details
|
||||
|
||||
### 1. `ZabbixRequest` Enhancement
|
||||
The base `ZabbixRequest` class handles the core optimization logic:
|
||||
- `optimizeZabbixParams(params, output)`: This method modifies the Zabbix parameters. It filters the `output` array to match the requested fields and removes "skippable" parameters based on rules.
|
||||
- `skippableZabbixParams`: A map that defines dependencies between Zabbix parameters and GraphQL fields.
|
||||
- *Example*: `this.skippableZabbixParams.set("selectTags", "tags")` means `selectTags` will be removed if `tags` is not in the requested output.
|
||||
|
||||
### 2. Parameter Mapping
|
||||
The `GraphqlParamsToNeededZabbixOutput` class provides static methods to map GraphQL query arguments and the selection set (`GraphQLResolveInfo`) to a list of needed Zabbix fields.
|
||||
|
||||
### 3. Resolver Integration
|
||||
Resolvers use the mapper to determine the required output and pass it to the datasource:
|
||||
```typescript
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHosts(args, info);
|
||||
return await new ZabbixQueryHostsRequestWithItemsAndInventory(...)
|
||||
.executeRequestThrowError(dataSources.zabbixAPI, new ParsedArgs(args), output);
|
||||
```
|
||||
|
||||
### 4. Indirect Dependencies
|
||||
Some GraphQL fields are not directly returned by Zabbix but are computed from other data. The optimization logic ensures these dependencies are handled:
|
||||
- **`state`**: Requesting the `state` field on a `Device` requires Zabbix `items`. The mapper automatically adds `items` to the requested output if `state` is present.
|
||||
|
||||
## 🛠️ Configuration
|
||||
Optimization rules are defined in the constructor of specialized `ZabbixRequest` classes.
|
||||
|
||||
### 📋 Supported Optimizations
|
||||
- **Hosts & Devices**:
|
||||
- `selectParentTemplates` skipped if `parentTemplates` not requested.
|
||||
- `selectTags` and `selectInheritedTags` skipped if `tags` not requested.
|
||||
- `selectHostGroups` skipped if `hostgroups` not requested.
|
||||
- `selectItems` skipped if `items` (or `state`) not requested.
|
||||
- `selectInventory` skipped if `inventory` not requested.
|
||||
- **Templates**:
|
||||
- `selectItems` skipped if `items` not requested.
|
||||
|
||||
## ✅ Verification
|
||||
You can verify that optimization is working by:
|
||||
1. Enabling `debug` log level (`LOG_LEVEL=debug`).
|
||||
2. Observing the Zabbix request bodies in the logs.
|
||||
3. Checking that the `output` array is minimized and `select*` parameters are omitted when not needed.
|
||||
|
|
@ -53,6 +53,11 @@ This document outlines the test cases and coverage for the Zabbix GraphQL API.
|
|||
- **TC-AUTH-04**: Import user rights.
|
||||
- **TC-AUTH-05**: Import user rights using sample mutation.
|
||||
|
||||
### Query Optimization
|
||||
- **TC-OPT-01**: Verify that GraphQL queries only fetch requested fields from Zabbix (reduced output).
|
||||
- **TC-OPT-02**: Verify that skippable Zabbix parameters (like selectItems) are omitted if not requested in GraphQL.
|
||||
- **TC-OPT-03**: Verify that indirect dependencies (e.g., `state` requiring `items`) are correctly handled by the optimization logic.
|
||||
|
||||
### System and Configuration
|
||||
- **TC-CONF-01**: Schema loader uses Config variables.
|
||||
- **TC-CONF-02**: Zabbix API constants derived from Config.
|
||||
|
|
@ -77,6 +82,11 @@ The `runAllRegressionTests` mutation (TC-E2E-02) executes the following checks:
|
|||
- **Template technical name lookup**: Verifies that templates can be correctly identified by their technical name (`host` field) when linking them to hosts during import.
|
||||
- **HTTP Agent URL support**: Verifies that templates containing HTTP Agent items with a configured URL can be imported successfully (verifying the addition of the `url` field to `CreateTemplateItem`).
|
||||
- **Host retrieval and visibility**: Verifies that newly imported hosts are immediately visible and retrievable via the `allHosts` query, including correctly delivered assigned templates and assigned host groups (verifying the fix for `output` fields in the host query data source).
|
||||
- **Query Optimization**: Verifies that GraphQL requests correctly translate into optimized Zabbix parameters, reducing the amount of data fetched (verifying the query optimization feature).
|
||||
- **Empty result handling**: Verifies that queries return an empty array instead of an error when no entities match the provided filters.
|
||||
- **Dependent Items**: Verifies that templates with master and dependent items can be imported successfully, correctly resolving the dependency within the same import operation.
|
||||
- **State sub-properties**: Verifies that requesting device state sub-properties correctly triggers the retrieval of required Zabbix items, even if `items` is not explicitly requested (verifying the indirect dependency logic).
|
||||
- **Negative Optimization (allDevices)**: Verifies that items are NOT requested from Zabbix if neither `items` nor `state` (or state sub-properties) are requested within the `allDevices` query.
|
||||
|
||||
## ✅ Test Coverage Checklist
|
||||
|
||||
|
|
@ -116,6 +126,9 @@ The `runAllRegressionTests` mutation (TC-E2E-02) executes the following checks:
|
|||
| TC-AUTH-03 | hasPermissions query | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) |
|
||||
| TC-AUTH-04 | importUserRights mutation | Unit | Jest | [src/test/user_rights.test.ts](../src/test/user_rights.test.ts) |
|
||||
| TC-AUTH-05 | Import user rights using sample | Integration | Jest | [src/test/user_rights_integration.test.ts](../src/test/user_rights_integration.test.ts) |
|
||||
| TC-OPT-01 | Verify Query Optimization (reduced output) | Unit/E2E | Jest/Regression | [src/test/query_optimization.test.ts](../src/test/query_optimization.test.ts) |
|
||||
| TC-OPT-02 | Verify skippable parameters | Unit/E2E | Jest/Regression | [src/test/query_optimization.test.ts](../src/test/query_optimization.test.ts) |
|
||||
| TC-OPT-03 | Verify indirect dependencies | Unit | Jest | [src/test/indirect_dependencies.test.ts](../src/test/indirect_dependencies.test.ts) |
|
||||
| TC-CONF-01 | schema_loader uses Config variables | Unit | Jest | [src/test/schema_config.test.ts](../src/test/schema_config.test.ts) |
|
||||
| TC-CONF-02 | constants are derived from Config | Unit | Jest | [src/test/zabbix_api_config.test.ts](../src/test/zabbix_api_config.test.ts) |
|
||||
| TC-CONF-03 | logger levels initialized from Config | Unit | Jest | [src/test/logger_config.test.ts](../src/test/logger_config.test.ts) |
|
||||
|
|
|
|||
69
docs/use-cases/trade-fair-logistics-requirements.md
Normal file
69
docs/use-cases/trade-fair-logistics-requirements.md
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
# 🏗️ Trade Fair Logistics Requirements
|
||||
|
||||
This document outlines the requirements for extending the Zabbix GraphQL API to support trade fair logistics, derived from the analysis of the "KI-gestützte Orchestrierung in der Messelogistik" (AI-supported orchestration in trade fair logistics) pilot at Koelnmesse.
|
||||
|
||||
## 📋 Project Context
|
||||
The goal is to use the **Virtual Control Room (VCR)** as an orchestration platform to improve punctuality, throughput, and exception handling in trade fair logistics.
|
||||
|
||||
## 🛠️ Key Use Cases
|
||||
|
||||
- **Slot Risk Scoring & Proactive Rescheduling**:
|
||||
- *Description*: Proactive detection of missed delivery slots using ETAs and historical data.
|
||||
- *AI Function*: Calculates slot-miss risk and suggests next best actions (e.g. shift slot, alternative gate).
|
||||
- *Zabbix Role*: Monitoring ETA vs. Slot time, triggering alerts on high risk.
|
||||
|
||||
- **Exception Copilot for Dispatch & Gate**:
|
||||
- *Description*: Standardized workflows (Playbooks) for managing arrival deviations (e.g. no slot, wrong gate, missing documents).
|
||||
- *AI Function*: Classifies exceptions and provides communication templates.
|
||||
- *Zabbix Role*: Capturing exception events as triggers and managing the resolution state.
|
||||
|
||||
- **Multilingual Driver Assistant**:
|
||||
- *Description*: Step-by-step instructions for drivers on-site to reduce misunderstandings and wrong turns.
|
||||
- *Zabbix Role*: Providing real-time status updates (e.g. "Gate 4 is ready for you") to external communication interfaces.
|
||||
|
||||
- **Handling Readiness (Stapler/Personal/Rampe)**:
|
||||
- *Description*: Coordinating truck arrivals with the availability of handling resources like forklifts and ramps.
|
||||
- *Zabbix Role*: Monitoring the status and capacity of logistics assets and personnel.
|
||||
|
||||
- **VCR Setup Copilot**:
|
||||
- *Description*: Template-based configuration to scale the VCR for different venues (e.g. Koelnmesse, Düsseldorf) and varying event rules.
|
||||
- *Zabbix Role*: Management of venue-specific and event-specific host groups and templates.
|
||||
|
||||
## ⚙️ Technical Requirements for the API
|
||||
|
||||
- **Dynamic Device Modeling**:
|
||||
- Support for complex **Delivery** entities as Zabbix hosts.
|
||||
- Inclusion of dynamic attributes such as Slot-ID, ETA, and Gate assignments.
|
||||
|
||||
- **Hierarchical Data Mapping**:
|
||||
- Mapping nested logistics data (e.g. cargo details, handling status) to hierarchical Zabbix item structures.
|
||||
- Use of tags for classification and filtering of logistics tasks.
|
||||
|
||||
- **Real-time Telemetry Integration**:
|
||||
- High-frequency ingestion of GPS and sensor data (e.g. temperature, shock) from mobile tracking devices.
|
||||
- Support for Zabbix trapper items to receive external push updates.
|
||||
|
||||
- **AI-Integration Hooks**:
|
||||
- Enable external AI systems to push "Risk Scores" and "Next Best Actions" into Zabbix.
|
||||
- Use of Zabbix triggers to orchestrate AI-driven suggestions.
|
||||
|
||||
- **Workflow Orchestration**:
|
||||
- Ability to trigger external actions (e.g. sending notifications to drivers, creating tickets) based on Zabbix triggers.
|
||||
- Integration with the Model Context Protocol (MCP) to allow AI agents to manage logistics exceptions.
|
||||
|
||||
- **Multi-Venue Templates**:
|
||||
- Provision of reusable template structures for different exhibition centers and recurring events.
|
||||
- Support for bulk import/export of venue-specific configurations.
|
||||
|
||||
## ✅ KPIs for Success Measurement
|
||||
|
||||
- **Slot Hit Rate**: Percentage of vehicles arriving within their booked time window.
|
||||
- **P22-Quote**: Frequency of vehicles needing to be redirected to waiting areas (P22).
|
||||
- **Gate Waiting Time**: Average time from arrival at the venue to successful check-in.
|
||||
- **Throughput**: Number of vehicles processed per gate/hour.
|
||||
- **Average Handle Time (AHT)**: Mean time to resolve a logistics exception/ticket.
|
||||
- **First Contact Resolution**: Rate of exceptions resolved without further escalation.
|
||||
|
||||
## 🔗 References
|
||||
- **Analysis Document**: [docs/KI für Event- und Messelogistik.pdf](../KI%20f%C3%BCr%20Event-%20und%20Messelogistik.pdf)
|
||||
- **Roadmap**: [roadmap.md](../../roadmap.md)
|
||||
29
roadmap.md
29
roadmap.md
|
|
@ -3,21 +3,26 @@
|
|||
This document outlines the achieved milestones and planned enhancements for the Zabbix GraphQL API project.
|
||||
|
||||
## ✅ Achieved Milestones
|
||||
- **🎯 VCR Product Integration**:
|
||||
- Developed a specialized **GraphQL API** as part of the VCR Product. This enables the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
|
||||
- **🎯 VCR Product Integration**: Developed a specialized **GraphQL API** as part of the VCR Product to enable the use of **Zabbix** as a robust base for monitoring and controlling **IoT devices**.
|
||||
- *First use case*: Control of mobile traffic jam warning installations on **German Autobahns**.
|
||||
|
||||
- **🔓 Open Source Extraction & AI Integration**:
|
||||
- Extracted the core functionality of the API to publish it as an **Open Source** project.
|
||||
- Enhanced it with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases within the VCR or other applications.
|
||||
- **🔓 Open Source Extraction & AI Integration**: Extracted the core functionality of the API to publish it as an **Open Source** project.
|
||||
- *AI Integration*: Enhanced with **Model Context Protocol (MCP)** and **AI agent** integration to enable workflow and agent-supported use cases.
|
||||
|
||||
## 📅 Planned Enhancements
|
||||
- **📦 CI/CD & Package Publishing**:
|
||||
- Build and publish the API as an **npm package** as part of the `.forgejo` workflow to simplify distribution and updates.
|
||||
- **⚡ Query Optimization**: Optimize GraphQL API queries to reduce the amount of data fetched from Zabbix depending on the fields really requested and improve performance.
|
||||
|
||||
- **🧱 Flexible Usage by publishing to [npmjs.com](https://www.npmjs.com/)**:
|
||||
- Transform the package into a versatile tool that can be used either **standalone** or as a **core library**, allowing other projects to include and extend it easily.
|
||||
- **🏗️ Trade Fair Logistics Use Case**: Extend the API to support trade fair logistics use cases by analyzing requirements from business stakeholders.
|
||||
- *Analysis*: Analysis of "Trade Fair Logistics" and derived [requirements document](docs/use-cases/trade-fair-logistics-requirements.md).
|
||||
- *Simulation*:
|
||||
- Create mocked "real world sensor devices" relevant for the use case.
|
||||
- Create a sample device collecting relevant information from public APIs, e.g. weather conditions or traffic conditions at a given location.
|
||||
- Simulate the traffic conditions on the route by using the simulated sensor devices.
|
||||
- *Configuration*: Analyze a real-world transport and configure Zabbix by placing sensor devices at the right places of the route.
|
||||
|
||||
- **🧩 Extension Project**: `zabbix-graphql-api-problems`
|
||||
- Create the first official extension, `zabbix-graphql-api-problems`
|
||||
- Add support for **problem and trigger-related** queries. This will leverage **MCP + AI agents** to automatically react to Zabbix problems within external workflows.
|
||||
- **📦 CI/CD & Package Publishing**: Build and publish the API as an **npm package** as part of the `.forgejo` workflow to simplify distribution and updates.
|
||||
|
||||
- **🧱 Flexible Usage**: Transform the package into a versatile tool that can be used either **standalone** or as a **core library** (published to [npmjs.com](https://www.npmjs.com/)).
|
||||
|
||||
- **🧩 Extension Project**: Add support for **problem and trigger-related** queries through the `zabbix-graphql-api-problems` extension.
|
||||
- *AI Integration*: Leverage **MCP + AI agents** to automatically react to Zabbix problems within external workflows.
|
||||
|
|
|
|||
31
src/api/graphql_utils.ts
Normal file
31
src/api/graphql_utils.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import {GraphQLResolveInfo, FieldNode, FragmentDefinitionNode, InlineFragmentNode} from "graphql";
|
||||
|
||||
export function getRequestedFields(info: GraphQLResolveInfo): string[] {
|
||||
if (!info || !info.fieldNodes) return [];
|
||||
const fields: string[] = [];
|
||||
const fieldNode = info.fieldNodes[0];
|
||||
|
||||
function extractFields(selectionSet: any, prefix: string = "") {
|
||||
if (!selectionSet) return;
|
||||
for (const selection of selectionSet.selections) {
|
||||
if (selection.kind === 'Field') {
|
||||
const fieldName = (selection as FieldNode).name.value;
|
||||
const fullPath = prefix ? `${prefix}.${fieldName}` : fieldName;
|
||||
fields.push(fullPath);
|
||||
if (selection.selectionSet) {
|
||||
extractFields(selection.selectionSet, fullPath);
|
||||
}
|
||||
} else if (selection.kind === 'InlineFragment') {
|
||||
extractFields((selection as InlineFragmentNode).selectionSet, prefix);
|
||||
} else if (selection.kind === 'FragmentSpread') {
|
||||
const fragment = info.fragments[selection.name.value];
|
||||
if (fragment) {
|
||||
extractFields(fragment.selectionSet, prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extractFields(fieldNode.selectionSet);
|
||||
return fields;
|
||||
}
|
||||
|
|
@ -62,6 +62,7 @@ import {GraphQLInterfaceType, GraphQLList} from "graphql/type/index.js";
|
|||
import {isDevice} from "./resolver_helpers.js";
|
||||
import {ZabbixPermissionsHelper} from "../datasources/zabbix-permissions.js";
|
||||
import {Config} from "../common_utils.js";
|
||||
import {GraphqlParamsToNeededZabbixOutput} from "../datasources/graphql-params-to-zabbix-output.js";
|
||||
|
||||
|
||||
export function createResolvers(): Resolvers {
|
||||
|
|
@ -102,36 +103,39 @@ export function createResolvers(): Resolvers {
|
|||
allHosts: async (_parent: any, args: QueryAllHostsArgs, {
|
||||
zabbixAuthToken,
|
||||
cookie, dataSources
|
||||
}: any) => {
|
||||
}: any, info: any) => {
|
||||
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
||||
args.tag_hostType ??= [Config.HOST_TYPE_FILTER_DEFAULT];
|
||||
}
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHosts(args, info);
|
||||
return await new ZabbixQueryHostsRequestWithItemsAndInventory(zabbixAuthToken, cookie)
|
||||
.executeRequestThrowError(
|
||||
dataSources.zabbixAPI, new ParsedArgs(args)
|
||||
dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(args), output
|
||||
)
|
||||
},
|
||||
allDevices: async (_parent: any, args: QueryAllDevicesArgs, {
|
||||
zabbixAuthToken,
|
||||
cookie, dataSources
|
||||
}: any) => {
|
||||
}: any, info: any) => {
|
||||
if (Config.HOST_TYPE_FILTER_DEFAULT) {
|
||||
args.tag_hostType ??= [Config.HOST_TYPE_FILTER_DEFAULT];
|
||||
}
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllDevices(args, info);
|
||||
return await new ZabbixQueryDevices(zabbixAuthToken, cookie)
|
||||
.executeRequestThrowError(
|
||||
dataSources.zabbixAPI, new ZabbixQueryDevicesArgs(args)
|
||||
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryDevicesArgs(args), output
|
||||
)
|
||||
},
|
||||
allHostGroups: async (_parent: any, args: QueryAllHostGroupsArgs, {
|
||||
zabbixAuthToken,
|
||||
cookie
|
||||
}: any) => {
|
||||
cookie, dataSources
|
||||
}: any, info: any) => {
|
||||
if (!args.search_name && Config.HOST_GROUP_FILTER_DEFAULT) {
|
||||
args.search_name = Config.HOST_GROUP_FILTER_DEFAULT
|
||||
}
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapAllHostGroups(args, info);
|
||||
return await new ZabbixQueryHostgroupsRequest(zabbixAuthToken, cookie).executeRequestThrowError(
|
||||
zabbixAPI, new ZabbixQueryHostgroupsParams(args)
|
||||
dataSources?.zabbixAPI || zabbixAPI, new ZabbixQueryHostgroupsParams(args), output
|
||||
)
|
||||
},
|
||||
|
||||
|
|
@ -158,8 +162,8 @@ export function createResolvers(): Resolvers {
|
|||
|
||||
templates: async (_parent: any, args: QueryTemplatesArgs, {
|
||||
zabbixAuthToken,
|
||||
cookie
|
||||
}: any) => {
|
||||
cookie, dataSources
|
||||
}: any, info: any) => {
|
||||
let params: any = {}
|
||||
if (args.hostids) {
|
||||
params.templateids = args.hostids
|
||||
|
|
@ -169,8 +173,9 @@ export function createResolvers(): Resolvers {
|
|||
name: args.name_pattern
|
||||
}
|
||||
}
|
||||
const output = GraphqlParamsToNeededZabbixOutput.mapTemplates(args, info);
|
||||
return await new ZabbixQueryTemplatesRequest(zabbixAuthToken, cookie)
|
||||
.executeRequestThrowError(zabbixAPI, new ParsedArgs(params));
|
||||
.executeRequestThrowError(dataSources?.zabbixAPI || zabbixAPI, new ParsedArgs(params), output);
|
||||
},
|
||||
|
||||
allTemplateGroups: async (_parent: any, args: any, {
|
||||
|
|
|
|||
26
src/datasources/graphql-params-to-zabbix-output.ts
Normal file
26
src/datasources/graphql-params-to-zabbix-output.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import {GraphQLResolveInfo} from "graphql";
|
||||
import {getRequestedFields} from "../api/graphql_utils.js";
|
||||
import {
|
||||
QueryAllDevicesArgs,
|
||||
QueryAllHostGroupsArgs,
|
||||
QueryAllHostsArgs,
|
||||
QueryTemplatesArgs
|
||||
} from "../schema/generated/graphql.js";
|
||||
|
||||
export class GraphqlParamsToNeededZabbixOutput {
|
||||
static mapAllHosts(args: QueryAllHostsArgs, info: GraphQLResolveInfo): string[] {
|
||||
return getRequestedFields(info);
|
||||
}
|
||||
|
||||
static mapAllDevices(args: QueryAllDevicesArgs, info: GraphQLResolveInfo): string[] {
|
||||
return getRequestedFields(info);
|
||||
}
|
||||
|
||||
static mapAllHostGroups(args: QueryAllHostGroupsArgs, info: GraphQLResolveInfo): string[] {
|
||||
return getRequestedFields(info);
|
||||
}
|
||||
|
||||
static mapTemplates(args: QueryTemplatesArgs, info: GraphQLResolveInfo): string[] {
|
||||
return getRequestedFields(info);
|
||||
}
|
||||
}
|
||||
|
|
@ -80,12 +80,12 @@ export class ZabbixAPI
|
|||
return super.post(path, request);
|
||||
}
|
||||
|
||||
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true): Promise<T | ZabbixErrorResult> {
|
||||
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args) : zabbixRequest.executeRequestReturnError(this, args);
|
||||
async executeRequest<T extends ZabbixResult, A extends ParsedArgs>(zabbixRequest: ZabbixRequest<T, A>, args?: A, throwApiError: boolean = true, output?: string[]): Promise<T | ZabbixErrorResult> {
|
||||
return throwApiError ? zabbixRequest.executeRequestThrowError(this, args, output) : zabbixRequest.executeRequestReturnError(this, args, output);
|
||||
}
|
||||
|
||||
async requestByPath<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs>(path: string, args?: A, authToken?: string | null, cookies?: string, throwApiError: boolean = true) {
|
||||
return this.executeRequest<T, A>(new ZabbixRequest<T>(path, authToken, cookies), args, throwApiError);
|
||||
async requestByPath<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs>(path: string, args?: A, authToken?: string | null, cookies?: string, throwApiError: boolean = true, output?: string[]) {
|
||||
return this.executeRequest<T, A>(new ZabbixRequest<T>(path, authToken, cookies), args, throwApiError, output);
|
||||
}
|
||||
|
||||
async getLocations(args?: ParsedArgs, authToken?: string, cookies?: string) {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,14 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
|||
|
||||
constructor(path: string, authToken?: string | null, cookie?: string | null) {
|
||||
super(path, authToken, cookie);
|
||||
this.skippableZabbixParams.set("selectParentTemplates", "parentTemplates");
|
||||
this.skippableZabbixParams.set("selectTags", "tags");
|
||||
this.skippableZabbixParams.set("selectInheritedTags", "tags");
|
||||
this.skippableZabbixParams.set("selectHostGroups", "hostgroups");
|
||||
}
|
||||
|
||||
createZabbixParams(args?: A): ZabbixParams {
|
||||
return {
|
||||
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||
return this.optimizeZabbixParams({
|
||||
...super.createZabbixParams(args),
|
||||
selectParentTemplates: [
|
||||
"templateid",
|
||||
|
|
@ -43,7 +47,7 @@ export class ZabbixQueryHostsGenericRequest<T extends ZabbixResult, A extends Pa
|
|||
"description",
|
||||
"parentTemplates"
|
||||
]
|
||||
};
|
||||
}, output);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -67,10 +71,12 @@ export class ZabbixQueryHostsMetaRequest extends ZabbixQueryHostsGenericRequest<
|
|||
export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequest<T, A> {
|
||||
constructor(path: string, authToken?: string | null, cookie?: string) {
|
||||
super(path, authToken, cookie);
|
||||
this.skippableZabbixParams.set("selectItems", "items");
|
||||
this.impliedFields.set("state", ["items"]);
|
||||
}
|
||||
|
||||
createZabbixParams(args?: A): ZabbixParams {
|
||||
return {
|
||||
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||
return this.optimizeZabbixParams({
|
||||
...super.createZabbixParams(args),
|
||||
selectItems: [
|
||||
"itemid",
|
||||
|
|
@ -99,13 +105,13 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
|
|||
"description",
|
||||
"parentTemplates"
|
||||
],
|
||||
};
|
||||
}, output);
|
||||
}
|
||||
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A): Promise<ZabbixErrorResult | T> {
|
||||
let result = await super.executeRequestReturnError(zabbixAPI, args);
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<ZabbixErrorResult | T> {
|
||||
let result = await super.executeRequestReturnError(zabbixAPI, args, output);
|
||||
|
||||
if (result && !isZabbixErrorResult(result)) {
|
||||
if (result && !isZabbixErrorResult(result) && (!output || output.includes("items.preprocessing"))) {
|
||||
const hosts = <ZabbixHost[]>result;
|
||||
const hostids = hosts.map(h => h.hostid);
|
||||
|
||||
|
|
@ -125,21 +131,29 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
|
|||
for (let device of hosts) {
|
||||
for (let item of device.items || []) {
|
||||
item.preprocessing = itemidToPreprocessing.get(item.itemid.toString());
|
||||
if (!item.lastclock ) {
|
||||
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError(
|
||||
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type))
|
||||
if (isZabbixErrorResult(values)) {
|
||||
return values;
|
||||
}
|
||||
if (values.length) {
|
||||
let latestValue = values[0];
|
||||
item.lastvalue = latestValue.value;
|
||||
item.lastclock = latestValue.clock;
|
||||
} else {
|
||||
item.lastvalue = null;
|
||||
item.lastclock = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (result && !isZabbixErrorResult(result) && (!output || output.includes("items.lastclock") || output.includes("items.lastvalue"))) {
|
||||
const hosts = <ZabbixHost[]>result;
|
||||
for (let device of hosts) {
|
||||
for (let item of device.items || []) {
|
||||
if (!item.lastclock) {
|
||||
let values = await new ZabbixQueryHistoryRequest(this.authToken, this.cookie).executeRequestReturnError(
|
||||
zabbixAPI, new ZabbixHistoryGetParams(item.itemid, ["clock", "value", "itemid"], 1, item.value_type))
|
||||
if (isZabbixErrorResult(values)) {
|
||||
return values;
|
||||
}
|
||||
if (values.length) {
|
||||
let latestValue = values[0];
|
||||
item.lastvalue = latestValue.value;
|
||||
item.lastclock = latestValue.clock;
|
||||
} else {
|
||||
item.lastvalue = null;
|
||||
item.lastclock = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -153,15 +167,16 @@ export class ZabbixQueryHostsGenericRequestWithItems<T extends ZabbixResult, A e
|
|||
export class ZabbixQueryHostsGenericRequestWithItemsAndInventory<T extends ZabbixResult, A extends ParsedArgs = ParsedArgs> extends ZabbixQueryHostsGenericRequestWithItems<T, A> {
|
||||
constructor(path: string, authToken?: string | null, cookie?: string) {
|
||||
super(path, authToken, cookie);
|
||||
this.skippableZabbixParams.set("selectInventory", "inventory");
|
||||
}
|
||||
|
||||
createZabbixParams(args?: A): ZabbixParams {
|
||||
return {
|
||||
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||
return this.optimizeZabbixParams({
|
||||
...super.createZabbixParams(args),
|
||||
selectInventory: [
|
||||
"location", "location_lat", "location_lon"
|
||||
]
|
||||
};
|
||||
}, output);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +216,7 @@ export interface ZabbixCreateHostInputParams extends ZabbixParams {
|
|||
templateids?: [number];
|
||||
hostgroupids?: [number];
|
||||
macros?: { macro: string, value: string }[];
|
||||
tags?: { tag: string, value: string }[];
|
||||
additionalParams?: any;
|
||||
}
|
||||
|
||||
|
|
@ -231,6 +247,9 @@ class ZabbixCreateHostParams implements ZabbixParams {
|
|||
if (inputParams.macros) {
|
||||
this.macros = inputParams.macros;
|
||||
}
|
||||
if (inputParams.tags) {
|
||||
this.tags = inputParams.tags;
|
||||
}
|
||||
}
|
||||
|
||||
host: string
|
||||
|
|
@ -246,6 +265,7 @@ class ZabbixCreateHostParams implements ZabbixParams {
|
|||
templates?: any
|
||||
groups?: any
|
||||
macros?: { macro: string, value: string }[]
|
||||
tags?: { tag: string, value: string }[]
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -148,17 +148,57 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
|||
protected requestBodyTemplate: ZabbixRequestBody;
|
||||
protected method: string
|
||||
protected prepResult: T | ZabbixErrorResult | undefined = undefined
|
||||
protected skippableZabbixParams: Map<string, string> = new Map();
|
||||
protected impliedFields: Map<string, string[]> = new Map();
|
||||
|
||||
constructor(public path: string, public authToken?: string | null, public cookie?: string | null) {
|
||||
this.method = path.split(".", 2).join(".");
|
||||
this.requestBodyTemplate = new ZabbixRequestBody(this.method);
|
||||
}
|
||||
|
||||
createZabbixParams(args?: A): ZabbixParams {
|
||||
return args?.zabbix_params || {}
|
||||
optimizeZabbixParams(params: ZabbixParams, output?: string[]): ZabbixParams {
|
||||
if (!output || output.length === 0) {
|
||||
return params;
|
||||
}
|
||||
|
||||
const requestedTopLevelFields = Array.from(new Set(output.map(field => field.split('.')[0])));
|
||||
|
||||
// Apply implied fields (e.g. "state" implies "items")
|
||||
let neededTopLevelFields = [...requestedTopLevelFields];
|
||||
this.impliedFields.forEach((implied, field) => {
|
||||
if (requestedTopLevelFields.includes(field)) {
|
||||
neededTopLevelFields.push(...implied.map(f => f.split('.')[0]));
|
||||
}
|
||||
});
|
||||
|
||||
const topLevelOutput = Array.from(new Set(neededTopLevelFields));
|
||||
|
||||
// Reduce output subfields
|
||||
if (params.output) {
|
||||
if (Array.isArray(params.output)) {
|
||||
params.output = params.output.filter(field => topLevelOutput.includes(field));
|
||||
} else if (params.output === "extend") {
|
||||
params.output = topLevelOutput;
|
||||
}
|
||||
} else {
|
||||
params.output = topLevelOutput;
|
||||
}
|
||||
|
||||
// Remove skippable parameters
|
||||
this.skippableZabbixParams.forEach((neededField, skippableParam) => {
|
||||
if (!topLevelOutput.includes(neededField) && params.hasOwnProperty(skippableParam)) {
|
||||
delete params[skippableParam];
|
||||
}
|
||||
});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
getRequestBody(args?: A, zabbixParams?: ZabbixParams): ZabbixRequestBody {
|
||||
createZabbixParams(args?: A, output?: string[]): ZabbixParams {
|
||||
return this.optimizeZabbixParams(args?.zabbix_params || {}, output)
|
||||
}
|
||||
|
||||
getRequestBody(args?: A, zabbixParams?: ZabbixParams, output?: string[]): ZabbixRequestBody {
|
||||
let params: ZabbixParams
|
||||
if (Array.isArray(args?.zabbix_params)) {
|
||||
params = args?.zabbix_params.map(paramsObj => {
|
||||
|
|
@ -168,7 +208,7 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
|||
return paramsObj;
|
||||
})
|
||||
} else {
|
||||
params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args)}
|
||||
params = {...this.requestBodyTemplate.params, ...zabbixParams ?? this.createZabbixParams(args, output)}
|
||||
}
|
||||
return params ? {
|
||||
...this.requestBodyTemplate,
|
||||
|
|
@ -204,12 +244,12 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
|||
return this.prepResult;
|
||||
}
|
||||
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A): Promise<T | ZabbixErrorResult> {
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: A, output?: string[]): Promise<T | ZabbixErrorResult> {
|
||||
let prepareResult = await this.prepare(zabbixAPI, args);
|
||||
if (prepareResult) {
|
||||
return prepareResult;
|
||||
}
|
||||
let requestBody = this.getRequestBody(args);
|
||||
let requestBody = this.getRequestBody(args, undefined, output);
|
||||
|
||||
try {
|
||||
|
||||
|
|
@ -236,8 +276,8 @@ export class ZabbixRequest<T extends ZabbixResult, A extends ParsedArgs = Parsed
|
|||
}
|
||||
}
|
||||
|
||||
async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A): Promise<T> {
|
||||
let response = await this.executeRequestReturnError(zabbixApi, args);
|
||||
async executeRequestThrowError(zabbixApi: ZabbixAPI, args?: A, output?: string[]): Promise<T> {
|
||||
let response = await this.executeRequestReturnError(zabbixApi, args, output);
|
||||
if (isZabbixErrorResult(response)) {
|
||||
throw new GraphQLError(`Called Zabbix path ${this.path} with error: ${response.error.message || "Zabbix error."} ${response.error.data}`, {
|
||||
extensions: {
|
||||
|
|
|
|||
|
|
@ -15,20 +15,21 @@ export interface ZabbixQueryTemplateResponse {
|
|||
export class ZabbixQueryTemplatesRequest extends ZabbixRequest<ZabbixQueryTemplateResponse[]> {
|
||||
constructor(authToken?: string | null, cookie?: string | null,) {
|
||||
super("template.get", authToken, cookie);
|
||||
this.skippableZabbixParams.set("selectItems", "items");
|
||||
}
|
||||
|
||||
createZabbixParams(args?: ParsedArgs): ZabbixParams {
|
||||
return {
|
||||
createZabbixParams(args?: ParsedArgs, output?: string[]): ZabbixParams {
|
||||
return this.optimizeZabbixParams({
|
||||
"selectItems": "extend",
|
||||
"output": "extend",
|
||||
...args?.zabbix_params
|
||||
};
|
||||
}, output);
|
||||
}
|
||||
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs): Promise<ZabbixErrorResult | ZabbixQueryTemplateResponse[]> {
|
||||
let result = await super.executeRequestReturnError(zabbixAPI, args);
|
||||
async executeRequestReturnError(zabbixAPI: ZabbixAPI, args?: ParsedArgs, output?: string[]): Promise<ZabbixErrorResult | ZabbixQueryTemplateResponse[]> {
|
||||
let result = await super.executeRequestReturnError(zabbixAPI, args, output);
|
||||
|
||||
if (result && !isZabbixErrorResult(result) && Array.isArray(result)) {
|
||||
if (result && !isZabbixErrorResult(result) && Array.isArray(result) && (!output || output.includes("items.preprocessing"))) {
|
||||
const templateids = result.map(t => t.templateid);
|
||||
if (templateids.length > 0) {
|
||||
// Batch fetch preprocessing for all items of these templates
|
||||
|
|
|
|||
|
|
@ -144,7 +144,8 @@ export class HostImporter {
|
|||
location: device.location,
|
||||
templateids: templateids,
|
||||
hostgroupids: groupids,
|
||||
macros: device.macros
|
||||
macros: device.macros,
|
||||
tags: [{ tag: "deviceType", value: device.deviceType }]
|
||||
}
|
||||
))
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,12 @@ import {TemplateImporter} from "./template_importer.js";
|
|||
import {TemplateDeleter} from "./template_deleter.js";
|
||||
import {logger} from "../logging/logger.js";
|
||||
import {zabbixAPI} from "../datasources/zabbix-api.js";
|
||||
import {ZabbixQueryHostsGenericRequest, ZabbixQueryHostsGenericRequestWithItems} from "../datasources/zabbix-hosts.js";
|
||||
import {
|
||||
ZabbixQueryDevices,
|
||||
ZabbixQueryDevicesArgs,
|
||||
ZabbixQueryHostsGenericRequest,
|
||||
ZabbixQueryHostsGenericRequestWithItems
|
||||
} from "../datasources/zabbix-hosts.js";
|
||||
import {ZabbixQueryTemplatesRequest} from "../datasources/zabbix-templates.js";
|
||||
import {ParsedArgs} from "../datasources/zabbix-request.js";
|
||||
|
||||
|
|
@ -39,6 +44,11 @@ export class RegressionTestExecutor {
|
|||
const regGroupName = "Templates/Roadwork/Devices";
|
||||
const hostGroupName = "Roadwork/Devices";
|
||||
|
||||
// Assure template group exists
|
||||
await TemplateImporter.importTemplateGroups([{
|
||||
groupName: regGroupName
|
||||
}], zabbixAuthToken, cookie);
|
||||
|
||||
const tempResult = await TemplateImporter.importTemplates([{
|
||||
host: regTemplateName,
|
||||
name: "Regression Test Template",
|
||||
|
|
@ -274,6 +284,190 @@ export class RegressionTestExecutor {
|
|||
: `Failed: TempImport=${metaTempSuccess}, HostImport=${metaHostSuccess}, Verify=${metaVerifySuccess}`
|
||||
});
|
||||
if (!metaOverallSuccess) success = false;
|
||||
|
||||
// Regression 7: Query Optimization and Skippable Parameters
|
||||
let optSuccess = false;
|
||||
try {
|
||||
const optRequest = new ZabbixQueryHostsGenericRequestWithItems("host.get", zabbixAuthToken, cookie);
|
||||
|
||||
// 1. Test optimization logic: items NOT requested
|
||||
const testParams1 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "name"]);
|
||||
const hasSelectItems1 = "selectItems" in testParams1;
|
||||
const hasOutput1 = Array.isArray(testParams1.output) && testParams1.output.includes("hostid") && testParams1.output.includes("name");
|
||||
|
||||
// 2. Test skippable params: items requested, tags NOT requested
|
||||
const testParams2 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "items"]);
|
||||
const hasSelectItems2 = "selectItems" in testParams2;
|
||||
const hasSelectTags2 = "selectTags" in testParams2;
|
||||
|
||||
optSuccess = !hasSelectItems1 && hasOutput1 && hasSelectItems2 && !hasSelectTags2;
|
||||
|
||||
// 3. Test indirect dependencies: state implies items
|
||||
const testParams3 = optRequest.createZabbixParams(new ParsedArgs({}), ["hostid", "state"]);
|
||||
const hasSelectItems3 = "selectItems" in testParams3;
|
||||
const hasOutput3 = Array.isArray(testParams3.output) && testParams3.output.includes("hostid") && testParams3.output.includes("items");
|
||||
|
||||
optSuccess = optSuccess && hasSelectItems3 && hasOutput3;
|
||||
|
||||
if (!optSuccess) {
|
||||
logger.error(`REG-OPT: Optimization verification failed. hasSelectItems1: ${hasSelectItems1}, hasOutput1: ${hasOutput1}, hasSelectItems2: ${hasSelectItems2}, hasSelectTags2: ${hasSelectTags2}, hasSelectItems3: ${hasSelectItems3}, hasOutput3: ${hasOutput3}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`REG-OPT: Error during optimization test: ${error}`);
|
||||
}
|
||||
|
||||
steps.push({
|
||||
name: "REG-OPT: Query Optimization and Skippable Parameters",
|
||||
success: optSuccess,
|
||||
message: optSuccess
|
||||
? "Optimization logic correctly filters output fields and skippable parameters"
|
||||
: "Optimization logic failed to correctly filter parameters"
|
||||
});
|
||||
if (!optSuccess) success = false;
|
||||
|
||||
// Regression 8: Empty result handling with filters
|
||||
let emptySuccess = false;
|
||||
try {
|
||||
const emptyResult = await new ZabbixQueryHostsGenericRequest("host.get", zabbixAuthToken, cookie)
|
||||
.executeRequestReturnError(zabbixAPI, new ParsedArgs({
|
||||
filter_host: "NonExistentHost_" + Math.random()
|
||||
}));
|
||||
|
||||
emptySuccess = Array.isArray(emptyResult) && emptyResult.length === 0;
|
||||
} catch (error: any) {
|
||||
logger.error(`REG-EMPTY: Error during empty result test: ${error}`);
|
||||
}
|
||||
|
||||
steps.push({
|
||||
name: "REG-EMPTY: Empty result handling",
|
||||
success: emptySuccess,
|
||||
message: emptySuccess ? "Correctly returned empty array for non-existent host" : "Failed to return empty array for non-existent host"
|
||||
});
|
||||
if (!emptySuccess) success = false;
|
||||
|
||||
// Regression 9: Dependent Items in Templates
|
||||
const depTempName = "REG_DEP_TEMP_" + Math.random().toString(36).substring(7);
|
||||
const depTempResult = await TemplateImporter.importTemplates([{
|
||||
host: depTempName,
|
||||
name: "Regression Dependent Template",
|
||||
groupNames: [regGroupName],
|
||||
items: [
|
||||
{
|
||||
name: "Master Item",
|
||||
type: 2, // Trapper
|
||||
key: "master.item",
|
||||
value_type: 4, // Text
|
||||
history: "1d"
|
||||
},
|
||||
{
|
||||
name: "Dependent Item",
|
||||
type: 18, // Dependent
|
||||
key: "dependent.item",
|
||||
value_type: 4,
|
||||
master_item: { key: "master.item" },
|
||||
history: "1d"
|
||||
}
|
||||
]
|
||||
}], zabbixAuthToken, cookie);
|
||||
|
||||
const depSuccess = !!depTempResult?.length && !depTempResult[0].error;
|
||||
steps.push({
|
||||
name: "REG-DEP: Dependent Items support",
|
||||
success: depSuccess,
|
||||
message: depSuccess ? "Template with master and dependent items imported successfully" : `Failed: ${depTempResult?.[0]?.message}`
|
||||
});
|
||||
if (!depSuccess) success = false;
|
||||
|
||||
// Regression 10: State sub-properties retrieval (Optimization indirect dependency)
|
||||
const stateTempName = "REG_STATE_TEMP_" + Math.random().toString(36).substring(7);
|
||||
const stateHostName = "REG_STATE_HOST_" + Math.random().toString(36).substring(7);
|
||||
|
||||
const stateTempResult = await TemplateImporter.importTemplates([{
|
||||
host: stateTempName,
|
||||
name: "Regression State Template",
|
||||
groupNames: [regGroupName],
|
||||
tags: [{ tag: "deviceType", value: "GenericDevice" }],
|
||||
items: [{
|
||||
name: "Temperature",
|
||||
type: 2, // Trapper
|
||||
key: "operational.temperature",
|
||||
value_type: 0, // Float
|
||||
history: "1d"
|
||||
}]
|
||||
}], zabbixAuthToken, cookie);
|
||||
|
||||
const stateTempSuccess = !!stateTempResult?.length && !stateTempResult[0].error;
|
||||
let stateHostSuccess = false;
|
||||
let stateVerifySuccess = false;
|
||||
|
||||
if (stateTempSuccess) {
|
||||
const stateHostResult = await HostImporter.importHosts([{
|
||||
deviceKey: stateHostName,
|
||||
deviceType: "GenericDevice",
|
||||
groupNames: [hostGroupName],
|
||||
templateNames: [stateTempName]
|
||||
}], zabbixAuthToken, cookie);
|
||||
stateHostSuccess = !!stateHostResult?.length && !!stateHostResult[0].hostid;
|
||||
|
||||
if (stateHostSuccess) {
|
||||
// Query using ZabbixQueryDevices which handles state -> items mapping
|
||||
const devicesResult = await new ZabbixQueryDevices(zabbixAuthToken, cookie)
|
||||
.executeRequestReturnError(zabbixAPI, new ZabbixQueryDevicesArgs({
|
||||
filter_host: stateHostName
|
||||
}), ["hostid", "state.operational.temperature"]);
|
||||
|
||||
if (Array.isArray(devicesResult) && devicesResult.length > 0) {
|
||||
const device = devicesResult[0] as any;
|
||||
// Check if items were fetched (indirect dependency)
|
||||
const hasItems = Array.isArray(device.items) && device.items.some((i: any) => i.key_ === "operational.temperature");
|
||||
stateVerifySuccess = hasItems;
|
||||
|
||||
if (!hasItems) {
|
||||
logger.error(`REG-STATE: Items missing in device result despite requesting state. Device: ${JSON.stringify(device)}`);
|
||||
}
|
||||
} else {
|
||||
logger.error(`REG-STATE: Device not found after import. Result: ${JSON.stringify(devicesResult)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stateOverallSuccess = stateTempSuccess && stateHostSuccess && stateVerifySuccess;
|
||||
steps.push({
|
||||
name: "REG-STATE: State sub-properties retrieval (indirect dependency)",
|
||||
success: stateOverallSuccess,
|
||||
message: stateOverallSuccess
|
||||
? "State sub-properties correctly trigger item fetching and are available"
|
||||
: `Failed: TempImport=${stateTempSuccess}, HostImport=${stateHostSuccess}, Verify=${stateVerifySuccess}`
|
||||
});
|
||||
if (!stateOverallSuccess) success = false;
|
||||
|
||||
// Regression 11: Negative Optimization - items not requested (allDevices)
|
||||
let optNegSuccess = false;
|
||||
try {
|
||||
const optRequest = new ZabbixQueryDevices(zabbixAuthToken, cookie);
|
||||
|
||||
// Test optimization logic: items/state NOT requested
|
||||
const testParams = optRequest.createZabbixParams(new ZabbixQueryDevicesArgs({}), ["hostid", "name"]);
|
||||
const hasSelectItems = "selectItems" in testParams;
|
||||
const hasOutputItems = Array.isArray(testParams.output) && testParams.output.includes("items");
|
||||
|
||||
optNegSuccess = !hasSelectItems && !hasOutputItems;
|
||||
|
||||
if (!optNegSuccess) {
|
||||
logger.error(`REG-OPT-NEG: Negative optimization verification failed. hasSelectItems: ${hasSelectItems}, hasOutputItems: ${hasOutputItems}`);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`REG-OPT-NEG: Error during negative optimization test: ${error}`);
|
||||
}
|
||||
|
||||
steps.push({
|
||||
name: "REG-OPT-NEG: Negative Optimization - items not requested (allDevices)",
|
||||
success: optNegSuccess,
|
||||
message: optNegSuccess
|
||||
? "Optimization correctly omits items when neither items nor state are requested"
|
||||
: "Optimization failed to omit items when not requested"
|
||||
});
|
||||
if (!optNegSuccess) success = false;
|
||||
|
||||
// Step 1: Create Host Group (Legacy test kept for compatibility)
|
||||
const groupResult = await HostImporter.importHostGroups([{
|
||||
|
|
@ -296,6 +490,9 @@ export class RegressionTestExecutor {
|
|||
await TemplateDeleter.deleteTemplates(null, httpTempName, zabbixAuthToken, cookie);
|
||||
await TemplateDeleter.deleteTemplates(null, macroTemplateName, zabbixAuthToken, cookie);
|
||||
await TemplateDeleter.deleteTemplates(null, metaTempName, zabbixAuthToken, cookie);
|
||||
await TemplateDeleter.deleteTemplates(null, depTempName, zabbixAuthToken, cookie);
|
||||
await TemplateDeleter.deleteTemplates(null, stateTempName, zabbixAuthToken, cookie);
|
||||
await HostDeleter.deleteHosts(null, stateHostName, zabbixAuthToken, cookie);
|
||||
// We don't delete the group here as it might be shared or used by other tests in this run
|
||||
|
||||
} catch (error: any) {
|
||||
|
|
|
|||
113
src/test/indirect_dependencies.test.ts
Normal file
113
src/test/indirect_dependencies.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import {createResolvers} from "../api/resolvers.js";
|
||||
import {zabbixAPI} from "../datasources/zabbix-api.js";
|
||||
import {QueryAllDevicesArgs} from "../schema/generated/graphql.js";
|
||||
|
||||
// Mocking ZabbixAPI
|
||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||
zabbixAPI: {
|
||||
executeRequest: jest.fn(),
|
||||
post: jest.fn(),
|
||||
baseURL: "http://mock-zabbix",
|
||||
}
|
||||
}));
|
||||
|
||||
// Mocking Config
|
||||
jest.mock("../common_utils.js", () => ({
|
||||
Config: {
|
||||
HOST_TYPE_FILTER_DEFAULT: null,
|
||||
HOST_GROUP_FILTER_DEFAULT: null
|
||||
}
|
||||
}));
|
||||
|
||||
describe("Indirect Dependencies Optimization", () => {
|
||||
let resolvers: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
resolvers = createResolvers();
|
||||
});
|
||||
|
||||
test("allDevices optimization - state implies items", async () => {
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
||||
|
||||
const args: QueryAllDevicesArgs = {};
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
const info = {
|
||||
fieldNodes: [{
|
||||
selectionSet: {
|
||||
selections: [
|
||||
{ kind: 'Field', name: { value: 'hostid' } },
|
||||
{ kind: 'Field', name: { value: 'state' } }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
await resolvers.Query.allDevices(null, args, context, info);
|
||||
|
||||
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
|
||||
expect(callParams.output).toContain("items");
|
||||
expect(callParams.selectItems).toBeDefined();
|
||||
});
|
||||
|
||||
test("allHosts optimization - inventory implies selectInventory", async () => {
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
||||
|
||||
const args = {};
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
const info = {
|
||||
fieldNodes: [{
|
||||
selectionSet: {
|
||||
selections: [
|
||||
{ kind: 'Field', name: { value: 'inventory' } }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
await resolvers.Query.allHosts(null, args, context, info);
|
||||
|
||||
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
|
||||
// Zabbix inventory data is requested via selectInventory, and it maps to GraphQL 'inventory' field
|
||||
expect(callParams.selectInventory).toBeDefined();
|
||||
});
|
||||
|
||||
test("allHosts optimization - state fragment implies items", async () => {
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
||||
|
||||
const args = {};
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
const info = {
|
||||
fieldNodes: [{
|
||||
selectionSet: {
|
||||
selections: [
|
||||
{
|
||||
kind: 'InlineFragment',
|
||||
typeCondition: { kind: 'NamedType', name: { value: 'Device' } },
|
||||
selectionSet: {
|
||||
selections: [
|
||||
{ kind: 'Field', name: { value: 'state' } }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
await resolvers.Query.allHosts(null, args, context, info);
|
||||
|
||||
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
|
||||
expect(callParams.output).toContain("items");
|
||||
expect(callParams.selectItems).toBeDefined();
|
||||
});
|
||||
});
|
||||
191
src/test/query_optimization.test.ts
Normal file
191
src/test/query_optimization.test.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import {createResolvers} from "../api/resolvers.js";
|
||||
import {zabbixAPI} from "../datasources/zabbix-api.js";
|
||||
import {QueryAllHostsArgs, QueryTemplatesArgs} from "../schema/generated/graphql.js";
|
||||
|
||||
// Mocking ZabbixAPI
|
||||
jest.mock("../datasources/zabbix-api.js", () => ({
|
||||
zabbixAPI: {
|
||||
executeRequest: jest.fn(),
|
||||
post: jest.fn(),
|
||||
baseURL: "http://mock-zabbix",
|
||||
}
|
||||
}));
|
||||
|
||||
// Mocking Config
|
||||
jest.mock("../common_utils.js", () => ({
|
||||
Config: {
|
||||
HOST_TYPE_FILTER_DEFAULT: null,
|
||||
HOST_GROUP_FILTER_DEFAULT: null
|
||||
}
|
||||
}));
|
||||
|
||||
describe("Query Optimization", () => {
|
||||
let resolvers: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
resolvers = createResolvers();
|
||||
});
|
||||
|
||||
test("allHosts optimization - reduce output and skip selectTags/selectItems", 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: 'name' } }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
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", "name"]
|
||||
})
|
||||
})
|
||||
}));
|
||||
|
||||
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
|
||||
expect(callParams.selectTags).toBeUndefined();
|
||||
expect(callParams.selectItems).toBeUndefined();
|
||||
expect(callParams.selectParentTemplates).toBeUndefined();
|
||||
|
||||
// Verify no follow-up item.get call
|
||||
const itemGetCall = (zabbixAPI.post as jest.Mock).mock.calls.find(call => call[1].body.method === "item.get");
|
||||
expect(itemGetCall).toBeUndefined();
|
||||
});
|
||||
|
||||
test("allHosts optimization - keep selectTags when 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: 'tags' } }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
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("templates optimization - reduce output and skip selectItems", async () => {
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
||||
|
||||
const args: QueryTemplatesArgs = {};
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
const info = {
|
||||
fieldNodes: [{
|
||||
selectionSet: {
|
||||
selections: [
|
||||
{ kind: 'Field', name: { value: 'name' } }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
await resolvers.Query.templates(null, args, context, info);
|
||||
|
||||
expect(zabbixAPI.post).toHaveBeenCalledWith(expect.any(String), expect.objectContaining({
|
||||
body: expect.objectContaining({
|
||||
params: expect.objectContaining({
|
||||
output: ["name"]
|
||||
})
|
||||
})
|
||||
}));
|
||||
|
||||
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
|
||||
expect(callParams.selectItems).toBeUndefined();
|
||||
});
|
||||
|
||||
test("allHosts optimization - indirect dependency via nested state field", 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: 'state' }, selectionSet: {
|
||||
selections: [
|
||||
{ kind: 'Field', name: { value: 'operational' }, selectionSet: {
|
||||
selections: [
|
||||
{ kind: 'Field', name: { value: 'temperature' } }
|
||||
]
|
||||
} }
|
||||
]
|
||||
} }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
await resolvers.Query.allHosts(null, args, context, info);
|
||||
|
||||
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
|
||||
expect(callParams.selectItems).toBeDefined();
|
||||
expect(Array.isArray(callParams.output)).toBe(true);
|
||||
expect(callParams.output).toContain("items");
|
||||
});
|
||||
|
||||
test("allDevices optimization - skip items when not requested", async () => {
|
||||
(zabbixAPI.post as jest.Mock).mockResolvedValueOnce([]);
|
||||
|
||||
const args: any = {};
|
||||
const context = {
|
||||
zabbixAuthToken: "test-token",
|
||||
dataSources: { zabbixAPI: zabbixAPI }
|
||||
};
|
||||
const info = {
|
||||
fieldNodes: [{
|
||||
selectionSet: {
|
||||
selections: [
|
||||
{ kind: 'Field', name: { value: 'hostid' } },
|
||||
{ kind: 'Field', name: { value: 'name' } }
|
||||
]
|
||||
}
|
||||
}]
|
||||
};
|
||||
|
||||
await resolvers.Query.allDevices(null, args, context, info);
|
||||
|
||||
const callParams = (zabbixAPI.post as jest.Mock).mock.calls[0][1].body.params;
|
||||
expect(callParams.selectItems).toBeUndefined();
|
||||
expect(callParams.output).not.toContain("items");
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue