diff --git a/.changeset/funny-fans-fry.md b/.changeset/funny-fans-fry.md new file mode 100644 index 0000000..2417fb3 --- /dev/null +++ b/.changeset/funny-fans-fry.md @@ -0,0 +1,5 @@ +--- +'grafana-zabbix': minor +--- + +Add support for host tags when querying metrics diff --git a/pkg/zabbix/methods.go b/pkg/zabbix/methods.go index fb53581..372471c 100644 --- a/pkg/zabbix/methods.go +++ b/pkg/zabbix/methods.go @@ -233,7 +233,7 @@ func filterAppsByQuery(items []Application, filter string) ([]Application, error return filteredItems, nil } -func (ds *Zabbix) GetItemTags(ctx context.Context, groupFilter string, hostFilter string, tagFilter string) ([]ItemTag, error) { +func (ds *Zabbix) GetItemTags(ctx context.Context, groupFilter string, hostFilter string, tagFilter string) ([]Tag, error) { hosts, err := ds.GetHosts(ctx, groupFilter, hostFilter) if err != nil { return nil, err @@ -252,8 +252,8 @@ func (ds *Zabbix) GetItemTags(ctx context.Context, groupFilter string, hostFilte return nil, err } - var allTags []ItemTag - tagsMap := make(map[string]ItemTag) + var allTags []Tag + tagsMap := make(map[string]Tag) for _, item := range allItems { for _, itemTag := range item.Tags { tagStr := itemTagToString(itemTag) @@ -267,13 +267,13 @@ func (ds *Zabbix) GetItemTags(ctx context.Context, groupFilter string, hostFilte return filterTags(allTags, tagFilter) } -func filterTags(items []ItemTag, filter string) ([]ItemTag, error) { +func filterTags(items []Tag, filter string) ([]Tag, error) { re, err := parseFilter(filter) if err != nil { return nil, err } - filteredItems := make([]ItemTag, 0) + filteredItems := make([]Tag, 0) for _, i := range items { tagStr := itemTagToString(i) if re != nil { diff --git a/pkg/zabbix/methods_test.go b/pkg/zabbix/methods_test.go index 139bc9a..3c1383b 100644 --- a/pkg/zabbix/methods_test.go +++ b/pkg/zabbix/methods_test.go @@ -217,14 +217,14 @@ func TestGetItemTags(t *testing.T) { tags, err := client.GetItemTags(context.Background(), "Servers", "web01", "/^Env/") require.NoError(t, err) - assert.ElementsMatch(t, []ItemTag{ + assert.ElementsMatch(t, []Tag{ {Tag: "Env", Value: "prod"}, {Tag: "Env", Value: "stage"}, }, tags) } func TestFilterTags(t *testing.T) { - tags := []ItemTag{ + tags := []Tag{ {Tag: "Env", Value: "prod"}, {Tag: "Application", Value: "api"}, } diff --git a/pkg/zabbix/models.go b/pkg/zabbix/models.go index ef31415..8f865d2 100644 --- a/pkg/zabbix/models.go +++ b/pkg/zabbix/models.go @@ -51,7 +51,7 @@ type Item struct { Delay string `json:"delay,omitempty"` Units string `json:"units,omitempty"` ValueMapID string `json:"valuemapid,omitempty"` - Tags []ItemTag `json:"tags,omitempty"` + Tags []Tag `json:"tags,omitempty"` } type ItemHost struct { @@ -59,7 +59,7 @@ type ItemHost struct { Name string `json:"name,omitempty"` } -type ItemTag struct { +type Tag struct { Tag string `json:"tag,omitempty"` Value string `json:"value,omitempty"` } @@ -90,9 +90,10 @@ type Group struct { } type Host struct { - Name string `json:"name"` - Host string `json:"host"` - ID string `json:"hostid"` + Name string `json:"name"` + Host string `json:"host"` + ID string `json:"hostid"` + Tags []Tag `json:"tags,omitempty"` } type Application struct { diff --git a/pkg/zabbix/utils.go b/pkg/zabbix/utils.go index 80c2767..7a0c227 100644 --- a/pkg/zabbix/utils.go +++ b/pkg/zabbix/utils.go @@ -132,7 +132,7 @@ func isRegex(filter string) bool { return regex.MatchString(filter) } -func itemTagToString(tag ItemTag) string { +func itemTagToString(tag Tag) string { if tag.Value != "" { return fmt.Sprintf("%s: %s", tag.Tag, tag.Value) } else { @@ -140,8 +140,8 @@ func itemTagToString(tag ItemTag) string { } } -func parseItemTag(tagStr string) ItemTag { - tag := ItemTag{} +func parseItemTag(tagStr string) Tag { + tag := Tag{} firstIdx := strings.Index(tagStr, ":") if firstIdx > 0 { tag.Tag = strings.TrimSpace(tagStr[:firstIdx]) diff --git a/src/datasource/components/QueryEditor/HostTagQueryEditor.tsx b/src/datasource/components/QueryEditor/HostTagQueryEditor.tsx new file mode 100644 index 0000000..a0afcf1 --- /dev/null +++ b/src/datasource/components/QueryEditor/HostTagQueryEditor.tsx @@ -0,0 +1,151 @@ +import { Tooltip, Button, Combobox, ComboboxOption, Stack, Input, RadioButtonGroup } from '@grafana/ui'; +import React, { FormEvent, useCallback, useEffect, useState } from 'react'; +import { HostTagOperatorLabel, HostTagOperatorValue } from './types'; +import { HostTagFilter, ZabbixTagEvalType } from 'datasource/types/query'; +import { getHostTagOptionLabel } from './utils'; + +interface Props { + hostTagOptions: ComboboxOption[]; + hostTagOptionsLoading: boolean; + version: string; + evalTypeValue?: ZabbixTagEvalType; + onHostTagFilterChange?: (hostTags: HostTagFilter[]) => void; + onHostTagEvalTypeChange?: (evalType: ZabbixTagEvalType) => void; +} + +export const HostTagQueryEditor = ({ + hostTagOptions, + hostTagOptionsLoading, + version, + evalTypeValue, + onHostTagFilterChange, + onHostTagEvalTypeChange, +}: Props) => { + const [hostTagFilters, setHostTagFilters] = useState([]); + const [hostTagValueDrafts, setHostTagValueDrafts] = useState([]); + const operatorOptions: ComboboxOption[] = [ + { value: HostTagOperatorValue.Exists, label: HostTagOperatorLabel.Exists }, + { value: HostTagOperatorValue.Equals, label: HostTagOperatorLabel.Equals }, + { value: HostTagOperatorValue.Contains, label: HostTagOperatorLabel.Contains }, + { + value: HostTagOperatorValue.DoesNotExist, + label: getHostTagOptionLabel(HostTagOperatorValue.DoesNotExist, version), + }, + { + value: HostTagOperatorValue.DoesNotEqual, + label: getHostTagOptionLabel(HostTagOperatorValue.DoesNotEqual, version), + }, + { + value: HostTagOperatorValue.DoesNotContain, + label: getHostTagOptionLabel(HostTagOperatorValue.DoesNotContain, version), + }, + ]; + + const onAddHostTagFilter = useCallback(() => { + setHostTagFilters((prevFilters) => [ + ...prevFilters, + { tag: '', value: '', operator: HostTagOperatorValue.Contains }, + ]); + setHostTagValueDrafts((prevDrafts) => [...prevDrafts, '']); + }, []); + + const onRemoveHostTagFilter = useCallback((index: number) => { + setHostTagFilters((prevFilters) => prevFilters.filter((_, i) => i !== index)); + setHostTagValueDrafts((prevDrafts) => prevDrafts.filter((_, i) => i !== index)); + }, []); + + const setHostTagFilterName = useCallback((index: number, name: string) => { + setHostTagFilters((prevFilters) => + prevFilters.map((filter, i) => (i === index ? { ...filter, tag: name } : filter)) + ); + }, []); + + const setHostTagFilterValue = useCallback((index: number, value: string) => { + if (value !== undefined) { + setHostTagFilters((prevFilters) => + prevFilters.map((filter, i) => (i === index ? { ...filter, value: value } : filter)) + ); + } + }, []); + + const setHostTagFilterOperator = useCallback((index: number, operator: HostTagOperatorValue) => { + setHostTagFilters((prevFilters) => + prevFilters.map((filter, i) => (i === index ? { ...filter, operator } : filter)) + ); + }, []); + + useEffect(() => { + onHostTagFilterChange(hostTagFilters); + }, [hostTagFilters]); + + return ( +
+ + +
+ ); +}; diff --git a/src/datasource/components/QueryEditor/MetricsQueryEditor.tsx b/src/datasource/components/QueryEditor/MetricsQueryEditor.tsx index 2907103..ece283d 100644 --- a/src/datasource/components/QueryEditor/MetricsQueryEditor.tsx +++ b/src/datasource/components/QueryEditor/MetricsQueryEditor.tsx @@ -1,15 +1,16 @@ -import _ from 'lodash'; -import React, { useEffect } from 'react'; +import { flatten, uniqBy } from 'lodash'; +import React, { useCallback, useEffect } from 'react'; import { useAsyncFn } from 'react-use'; import { InlineField, ComboboxOption } from '@grafana/ui'; import { QueryEditorRow } from './QueryEditorRow'; import { MetricPicker } from '../../../components'; -import { getVariableOptions } from './utils'; +import { getVariableOptions, processHostTags } from './utils'; import { ZabbixDatasource } from '../../datasource'; -import { ZabbixMetricsQuery } from '../../types/query'; +import { HostTagFilter, ZabbixMetricsQuery, ZabbixTagEvalType } from '../../types/query'; import { ZBXItem, ZBXItemTag } from '../../types'; import { itemTagToString } from '../../utils'; +import { HostTagQueryEditor } from './HostTagQueryEditor'; import { useInterpolatedQuery } from '../../hooks/useInterpolatedQuery'; export interface Props { @@ -28,6 +29,9 @@ export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountCha value: group.name, label: group.name, })); + if (options.length > 0) { + options.unshift({ value: '/.*/' }); + } options.unshift(...getVariableOptions()); return options; }; @@ -37,22 +41,44 @@ export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountCha return options; }, []); - const loadHostOptions = async (group: string) => { - const hosts = await datasource.zabbix.getAllHosts(group); + const loadHostTagOptions = async (group: string) => { + const hostsWithTags = await datasource.zabbix.getAllHosts(group, true); + const hostTags = processHostTags(hostsWithTags ?? []); + let options: Array> = hostTags?.map((tag) => ({ + value: tag.tag, + label: tag.tag, + })); + return options; + }; + + const loadHostOptions = async (group: string, hostTags?: HostTagFilter[], evalType?: ZabbixTagEvalType) => { + const hosts = await datasource.zabbix.getAllHosts(group, false, hostTags, evalType); let options: Array> = hosts?.map((host) => ({ value: host.name, label: host.name, })); - options = _.uniqBy(options, (o) => o.value); - options.unshift({ value: '/.*/' }); + options = uniqBy(options, (o) => o.value); + if (options.length > 0) { + options.unshift({ value: '/.*/' }); + } options.unshift(...getVariableOptions()); return options; }; - const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => { - const options = await loadHostOptions(interpolatedQuery.group.filter); + const [{ loading: hostTagsLoading, value: hostTagsOptions }, fetchHostTags] = useAsyncFn(async () => { + const options = await loadHostTagOptions(query.group.filter); return options; - }, [interpolatedQuery.group.filter]); + }, [query.group.filter]); + + const [{ loading: hostsLoading, value: hostOptions }, fetchHosts] = useAsyncFn(async () => { + const options = await loadHostOptions( + interpolatedQuery.group.filter, + interpolatedQuery.hostTags, + interpolatedQuery.evaltype + ); + + return options; + }, [interpolatedQuery.group.filter, interpolatedQuery.hostTags, interpolatedQuery.evaltype]); const loadAppOptions = async (group: string, host: string) => { const apps = await datasource.zabbix.getAllApps(group, host); @@ -60,7 +86,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountCha value: app.name, label: app.name, })); - options = _.uniqBy(options, (o) => o.value); + options = uniqBy(options, (o) => o.value); options.unshift(...getVariableOptions()); return options; }; @@ -77,15 +103,15 @@ export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountCha } const items = await datasource.zabbix.getAllItems(group, host, null, null, {}); - const tags: ZBXItemTag[] = _.flatten(items.map((item: ZBXItem) => item.tags || [])); + const tags: ZBXItemTag[] = flatten(items.map((item: ZBXItem) => item.tags || [])); // const tags: ZBXItemTag[] = await datasource.zabbix.getItemTags(groupFilter, hostFilter, null); - const tagList = _.uniqBy(tags, (t) => t.tag + t.value || '').map((t) => itemTagToString(t)); + const tagList = uniqBy(tags, (t) => t.tag + t.value || '').map((t) => itemTagToString(t)); let options: Array> = tagList?.map((tag) => ({ value: tag, label: tag, })); - options = _.uniqBy(options, (o) => o.value); + options = uniqBy(options, (o) => o.value); options.unshift(...getVariableOptions()); return options; }; @@ -123,7 +149,7 @@ export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountCha value: item.name, label: item.name, })); - itemOptions = _.uniqBy(itemOptions, (o) => o.value); + itemOptions = uniqBy(itemOptions, (o) => o.value); itemOptions.unshift(...getVariableOptions()); return itemOptions; }; @@ -147,6 +173,8 @@ export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountCha // Update suggestions on every metric change const groupFilter = interpolatedQuery.group?.filter; + const hostTagFilters = interpolatedQuery.hostTags; + const evalType = interpolatedQuery.evaltype; const hostFilter = interpolatedQuery.host?.filter; const appFilter = interpolatedQuery.application?.filter; const tagFilter = interpolatedQuery.itemTag?.filter; @@ -157,9 +185,13 @@ export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountCha }, []); useEffect(() => { - fetchHosts(); + fetchHostTags(); }, [groupFilter]); + useEffect(() => { + fetchHosts(); + }, [groupFilter, hostTagFilters, evalType]); + useEffect(() => { fetchApps(); }, [groupFilter, hostFilter]); @@ -180,6 +212,20 @@ export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountCha }; }; + const onHostTagFilterChange = useCallback( + (hostTags: HostTagFilter[]) => { + onChange({ ...query, hostTags: hostTags }); + }, + [onChange, query] + ); + + const onHostTagEvalTypeChange = useCallback( + (evalType: ZabbixTagEvalType) => { + onChange({ ...query, evaltype: evalType }); + }, + [onChange, query] + ); + const supportsApplications = datasource.zabbix.supportsApplications(); return ( @@ -195,6 +241,16 @@ export const MetricsQueryEditor = ({ query, datasource, onChange, onItemCountCha placeholder="Group name" /> + + + ({ + getTemplateSrv: jest.fn(), + }), + { virtual: true } +); + +describe('QueryEditor utils', () => { + describe('getVariableOptions', () => { + it('returns template variables except datasource and interval types', () => { + (getTemplateSrv as jest.Mock).mockReturnValue({ + getVariables: jest.fn().mockReturnValue([ + { name: 'env', type: 'query' }, + { name: 'ds', type: 'datasource' }, + { name: 'step', type: 'interval' }, + { name: 'region', type: 'custom' }, + ]), + }); + + const options = getVariableOptions(); + + expect(options).toEqual([ + { label: '$env', value: '$env' }, + { label: '$region', value: '$region' }, + ]); + }); + }); + + describe('processHostTags', () => { + it('deduplicates tags by tag key', () => { + const tags = processHostTags([ + { + host: 'a', + name: 'a', + tags: [ + { tag: 'env', value: 'prod' }, + { tag: 'role', value: 'api' }, + ], + }, + { + host: 'b', + name: 'b', + tags: [ + { tag: 'env', value: 'stage' }, + { tag: 'region', value: 'eu' }, + ], + }, + { host: 'c', name: 'c' }, + ]); + + expect(tags).toEqual([ + { tag: 'env', value: 'prod' }, + { tag: 'role', value: 'api' }, + { tag: 'region', value: 'eu' }, + ]); + }); + }); + + describe('getHostTagOptionLabel', () => { + it('returns pre-7.0 labels for legacy versions', () => { + expect(getHostTagOptionLabel(HostTagOperatorValue.DoesNotExist, '6.4.0')).toBe( + HostTagOperatorLabelBefore70.NotExist + ); + expect(getHostTagOptionLabel(HostTagOperatorValue.DoesNotEqual, '6.0.0')).toBe( + HostTagOperatorLabelBefore70.NotEqual + ); + expect(getHostTagOptionLabel(HostTagOperatorValue.DoesNotContain, '5.0.0')).toBe( + HostTagOperatorLabelBefore70.NotLike + ); + }); + + it('returns current labels for 7.0 and newer', () => { + expect(getHostTagOptionLabel(HostTagOperatorValue.DoesNotExist, '7.0.0')).toBe(HostTagOperatorLabel.DoesNotExist); + expect(getHostTagOptionLabel(HostTagOperatorValue.DoesNotEqual, '7.1.0')).toBe(HostTagOperatorLabel.DoesNotEqual); + expect(getHostTagOptionLabel(HostTagOperatorValue.DoesNotContain, '7.2.0')).toBe( + HostTagOperatorLabel.DoesNotContain + ); + }); + + it('returns empty string for unsupported values', () => { + expect(getHostTagOptionLabel(HostTagOperatorValue.Equals, '7.2.0')).toBe(''); + }); + }); +}); diff --git a/src/datasource/components/QueryEditor/utils.ts b/src/datasource/components/QueryEditor/utils.ts index e47de77..472730c 100644 --- a/src/datasource/components/QueryEditor/utils.ts +++ b/src/datasource/components/QueryEditor/utils.ts @@ -1,4 +1,7 @@ +import { uniqBy } from 'lodash'; import { getTemplateSrv } from '@grafana/runtime'; +import { Host, Tag } from 'datasource/zabbix/types'; +import { HostTagOperatorLabel, HostTagOperatorLabelBefore70, HostTagOperatorValue } from './types'; export const getVariableOptions = () => { const variables = getTemplateSrv() @@ -11,3 +14,28 @@ export const getVariableOptions = () => { label: `$${v.name}`, })); }; + +export function processHostTags(hosts: Host[]): Tag[] { + const hostTags = hosts.map((host) => host.tags || []).flat(); + // deduplicate tags + const uniqueHostTags = uniqBy(hostTags, (tag) => tag.tag); + return uniqueHostTags; +} + +/** + * Get the label for a host tag option + * Zabbix changed some of the operator labels in version 7.0.0 but the value equivalents remained the same. + * this function helps fetch the right label value for those that are different. + */ +export function getHostTagOptionLabel(value: HostTagOperatorValue, version: string): string { + switch (value) { + case HostTagOperatorValue.DoesNotExist: + return version < '7.0.0' ? HostTagOperatorLabelBefore70.NotExist : HostTagOperatorLabel.DoesNotExist; + case HostTagOperatorValue.DoesNotEqual: + return version < '7.0.0' ? HostTagOperatorLabelBefore70.NotEqual : HostTagOperatorLabel.DoesNotEqual; + case HostTagOperatorValue.DoesNotContain: + return version < '7.0.0' ? HostTagOperatorLabelBefore70.NotLike : HostTagOperatorLabel.DoesNotContain; + default: + return ''; + } +} diff --git a/src/datasource/types/query.ts b/src/datasource/types/query.ts index 26856cd..9b476d3 100644 --- a/src/datasource/types/query.ts +++ b/src/datasource/types/query.ts @@ -1,5 +1,6 @@ import { DataQuery } from '@grafana/schema'; import * as c from './../constants'; +import { HostTagOperatorValue } from 'datasource/components/QueryEditor/types'; export type QueryType = | typeof c.MODE_METRICS @@ -24,6 +25,7 @@ export type ZabbixMetricsQuery = { mode: number; itemids: string; useCaptureGroups: boolean; + hostTags?: HostTagFilter[]; proxy?: { filter: string }; trigger?: { filter: string }; itServiceFilter?: string; @@ -108,3 +110,9 @@ export enum ZabbixTagEvalType { AndOr = '0', Or = '2', } + +export interface HostTagFilter { + tag: string; + value: string; + operator: HostTagOperatorValue; +} diff --git a/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.test.ts b/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.test.ts index 05612aa..322bd63 100644 --- a/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.test.ts +++ b/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.test.ts @@ -1,4 +1,6 @@ import { ZabbixAPIConnector } from './zabbixAPIConnector'; +import { HostTagOperatorValue } from '../../../components/QueryEditor/types'; +import { ZabbixTagEvalType } from 'datasource/types/query'; describe('Zabbix API connector', () => { describe('getProxies function', () => { @@ -154,6 +156,80 @@ describe('Zabbix API connector', () => { expect(params.applicationids).toBeUndefined(); }); }); + + describe('getHosts', () => { + it('passes base params and group ids', () => { + const zabbixAPIConnector = new ZabbixAPIConnector(true, true, 123); + zabbixAPIConnector.request = jest.fn(); + + zabbixAPIConnector.getHosts(['1', '2']); + + expect(zabbixAPIConnector.request).toHaveBeenCalledWith('host.get', { + output: ['hostid', 'name', 'host'], + sortfield: 'name', + groupids: ['1', '2'], + }); + }); + + it('requests tags when getHostTags is true', () => { + const zabbixAPIConnector = new ZabbixAPIConnector(true, true, 123); + zabbixAPIConnector.request = jest.fn(); + + zabbixAPIConnector.getHosts(undefined, true); + + expect(zabbixAPIConnector.request).toHaveBeenCalledWith('host.get', { + output: ['hostid', 'name', 'host', 'tags'], + sortfield: 'name', + selectTags: 'extend', + }); + }); + + it('builds tag filters with numeric operator and evaltype', () => { + const zabbixAPIConnector = new ZabbixAPIConnector(true, true, 123); + zabbixAPIConnector.request = jest.fn(); + + zabbixAPIConnector.getHosts( + undefined, + false, + [ + { tag: 'role', value: 'api', operator: HostTagOperatorValue.Contains }, + { tag: '', value: 'ignore me', operator: HostTagOperatorValue.Equals }, + ], + ZabbixTagEvalType.Or + ); + + expect(zabbixAPIConnector.request).toHaveBeenCalledWith('host.get', { + output: ['hostid', 'name', 'host'], + sortfield: 'name', + selectTags: 'extend', + evaltype: 2, + tags: [{ tag: 'role', value: 'api', operator: 0 }], + }); + }); + + it('builds tag filters with numeric operator and default evaltype when using unsupported evalType', () => { + const zabbixAPIConnector = new ZabbixAPIConnector(true, true, 123); + zabbixAPIConnector.request = jest.fn(); + + zabbixAPIConnector.getHosts( + undefined, + false, + [ + { tag: 'role', value: 'api', operator: HostTagOperatorValue.Contains }, + { tag: '', value: 'ignore me', operator: HostTagOperatorValue.Equals }, + ], + '3' as ZabbixTagEvalType + ); + + expect(zabbixAPIConnector.request).toHaveBeenCalledWith('host.get', { + output: ['hostid', 'name', 'host'], + sortfield: 'name', + selectTags: 'extend', + evaltype: 0, + tags: [{ tag: 'role', value: 'api', operator: 0 }], + }); + }); + }); }); const triggers = [ diff --git a/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts b/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts index a0cb700..feb1f3a 100644 --- a/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts +++ b/src/datasource/zabbix/connectors/zabbix_api/zabbixAPIConnector.ts @@ -3,7 +3,7 @@ import semver from 'semver'; import kbn from 'grafana/app/core/utils/kbn'; import * as utils from '../../../utils'; import { MIN_SLA_INTERVAL, ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_NONE } from '../../../constants'; -import { ShowProblemTypes } from '../../../types/query'; +import { HostTagFilter, ShowProblemTypes, ZabbixTagEvalType } from '../../../types/query'; import { ZBXProblem, ZBXTrigger } from '../../../types'; import { APIExecuteScriptResponse, JSONRPCError, ZBXScript } from './types'; import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; @@ -149,7 +149,12 @@ export class ZabbixAPIConnector { return this.request('hostgroup.get', params); } - getHosts(groupids): Promise { + getHosts( + groupids: string[], + getHostTags?: boolean, + hostTagFilters?: HostTagFilter[], + evalType?: ZabbixTagEvalType + ): Promise { const params: any = { output: ['hostid', 'name', 'host'], sortfield: 'name', @@ -158,6 +163,23 @@ export class ZabbixAPIConnector { params.groupids = groupids; } + if (getHostTags) { + params.output.push('tags'); + params.selectTags = 'extend'; + } + + if (hostTagFilters && hostTagFilters.length > 0) { + params.selectTags = 'extend'; + params.evaltype = evalType === ZabbixTagEvalType.Or || evalType === ZabbixTagEvalType.AndOr ? +evalType : 0; + + // ensure only non empty tag keys are being sent + // convert operator to number since that is the expected type in Zabbix. + params.tags = hostTagFilters + .filter((tagFilter) => tagFilter.tag !== '') + .map((tagFilter) => { + return { ...tagFilter, operator: +tagFilter.operator }; + }); + } return this.request('host.get', params); } diff --git a/src/datasource/zabbix/types.ts b/src/datasource/zabbix/types.ts index 8e09d78..6259e54 100644 --- a/src/datasource/zabbix/types.ts +++ b/src/datasource/zabbix/types.ts @@ -50,3 +50,15 @@ export interface ZabbixConnector { supportsApplications: () => boolean; } + +export interface Host { + host: string; + name: string; + hostid?: string; + tags?: Tag[]; +} + +export interface Tag { + tag: string; + value: string; +} diff --git a/src/datasource/zabbix/zabbix.ts b/src/datasource/zabbix/zabbix.ts index 7561d06..ad66ea9 100644 --- a/src/datasource/zabbix/zabbix.ts +++ b/src/datasource/zabbix/zabbix.ts @@ -9,9 +9,9 @@ import { DBConnector } from './connectors/dbConnector'; import { ZabbixAPIConnector } from './connectors/zabbix_api/zabbixAPIConnector'; import { SQLConnector } from './connectors/sql/sqlConnector'; import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector'; -import { ZabbixConnector } from './types'; +import { Host, ZabbixConnector } from './types'; import { joinTriggersWithEvents, joinTriggersWithProblems } from '../problemsHandler'; -import { ZabbixMetricsQuery } from '../types/query'; +import { HostTagFilter, ZabbixMetricsQuery, ZabbixTagEvalType } from '../types/query'; import { ProblemDTO, ZBXApp, ZBXHost, ZBXItem, ZBXItemTag, ZBXTrigger } from '../types'; interface AppsResponse extends Array { @@ -296,6 +296,7 @@ export class Zabbix implements ZabbixConnector { } getAllGroups() { + console.log(this.zabbixAPI.getGroups()); return this.zabbixAPI.getGroups(); } @@ -306,10 +307,15 @@ export class Zabbix implements ZabbixConnector { /** * Get list of host belonging to given groups. */ - getAllHosts(groupFilter): Promise { + getAllHosts( + groupFilter: string, + getHostTags?: boolean, + hostTagFilters?: HostTagFilter[], + evalType?: ZabbixTagEvalType + ): Promise { return this.getGroups(groupFilter).then((groups) => { const groupids = _.map(groups, 'groupid'); - return this.zabbixAPI.getHosts(groupids); + return this.zabbixAPI.getHosts(groupids, getHostTags, hostTagFilters, evalType); }); }