Adds support for host tags (#2140)
## Sumary When dealing with multiple hosts, it can be hard for customers filter through and figure out which host to query metric data from. This PR aims to make this easier by adding support for host tags so that there is another layer of filtering / grouping applied for hosts. ## Detailed explanation - Adds new UI components to allow adding one or more host tag filter, and a switch to choose between `AND/OR` and `OR` operators when using more than one filter following Zabbix's UI: https://github.com/user-attachments/assets/c971f5eb-7e93-4238-bd6b-902cc657c014 https://github.com/user-attachments/assets/5f8996de-684e-4ffa-b98e-8e205c4fc1df - Modifies the existing `getHosts` function to make a call to the backend with a few additional parameters to `extend` (essentially extract) the host tags for a given selected group. No backend changes were required for this. ## Why To make it easier for customers to query metric data when dealing with multiple hosts. ## How to test - Go to explore or a dashboard and create a Zabbix query where the query type is `Metrics` - The easiest way to test is by selecting `/.*/` for Groups, checking the returned `Hosts` they should all be there - Add a host tag filter and change the keys and operators as well as switching from `AND/OR` to `OR` you should see how the values returned for `Host` changes ## Future work Adding variable support for host tags once this is completed. Fixes: https://github.com/orgs/grafana/projects/457/views/40?pane=issue&itemId=3609900134&issue=grafana%7Coss-big-tent-squad%7C126 and https://github.com/grafana/grafana-zabbix/issues/927 --------- Co-authored-by: ismail simsek <ismailsimsek09@gmail.com>
This commit is contained in:
committed by
GitHub
parent
3d0895c008
commit
0d64736e86
5
.changeset/funny-fans-fry.md
Normal file
5
.changeset/funny-fans-fry.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'grafana-zabbix': minor
|
||||
---
|
||||
|
||||
Add support for host tags when querying metrics
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"},
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
@@ -93,6 +93,7 @@ type Host struct {
|
||||
Name string `json:"name"`
|
||||
Host string `json:"host"`
|
||||
ID string `json:"hostid"`
|
||||
Tags []Tag `json:"tags,omitempty"`
|
||||
}
|
||||
|
||||
type Application struct {
|
||||
|
||||
@@ -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])
|
||||
|
||||
151
src/datasource/components/QueryEditor/HostTagQueryEditor.tsx
Normal file
151
src/datasource/components/QueryEditor/HostTagQueryEditor.tsx
Normal file
@@ -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<HostTagFilter[]>([]);
|
||||
const [hostTagValueDrafts, setHostTagValueDrafts] = useState<string[]>([]);
|
||||
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 (
|
||||
<div>
|
||||
<Stack direction="row">
|
||||
<Tooltip content="Add host tag filter">
|
||||
<Button icon="plus" variant="secondary" aria-label="Add new host tag filter" onClick={onAddHostTagFilter} />
|
||||
</Tooltip>
|
||||
{hostTagFilters.length > 0 && (
|
||||
<RadioButtonGroup
|
||||
options={[
|
||||
{ label: 'AND/OR', value: '0' }, // Default
|
||||
{ label: 'OR', value: '2' },
|
||||
]}
|
||||
onChange={onHostTagEvalTypeChange}
|
||||
value={evalTypeValue ?? '0'}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack direction="column">
|
||||
{hostTagFilters.map((filter, index) => {
|
||||
return (
|
||||
<Stack key={`host-tag-filter-${index}`} direction="row">
|
||||
<Combobox
|
||||
value={filter.tag}
|
||||
onChange={(option: ComboboxOption) => setHostTagFilterName(index, option.value)}
|
||||
options={hostTagOptions ?? []}
|
||||
width={19}
|
||||
loading={hostTagOptionsLoading}
|
||||
/>
|
||||
<Combobox
|
||||
value={filter.operator}
|
||||
onChange={(option: ComboboxOption<HostTagOperatorValue>) =>
|
||||
setHostTagFilterOperator(index, option.value)
|
||||
}
|
||||
options={operatorOptions}
|
||||
width={19}
|
||||
/>
|
||||
{filter.operator !== HostTagOperatorValue.Exists &&
|
||||
filter.operator !== HostTagOperatorValue.DoesNotExist && (
|
||||
<Input
|
||||
value={hostTagValueDrafts[index] ?? filter.value}
|
||||
onChange={(evt: FormEvent<HTMLInputElement>) => {
|
||||
const value = evt?.currentTarget?.value ?? '';
|
||||
setHostTagValueDrafts((prevDrafts) => {
|
||||
const nextDrafts = [...prevDrafts];
|
||||
nextDrafts[index] = value;
|
||||
return nextDrafts;
|
||||
});
|
||||
}}
|
||||
onBlur={(evt: FormEvent<HTMLInputElement>) =>
|
||||
setHostTagFilterValue(index, evt?.currentTarget?.value)
|
||||
}
|
||||
width={19}
|
||||
placeholder="Host tag value"
|
||||
/>
|
||||
)}
|
||||
<Tooltip content="Remove host tag filter">
|
||||
<Button
|
||||
key={`remove-host-tag-${index}`}
|
||||
icon="minus"
|
||||
variant="secondary"
|
||||
aria-label="Remove host tag filter"
|
||||
onClick={() => onRemoveHostTagFilter(index)}
|
||||
/>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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<ComboboxOption<string>> = 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<ComboboxOption<string>> = hosts?.map((host) => ({
|
||||
value: host.name,
|
||||
label: host.name,
|
||||
}));
|
||||
options = _.uniqBy(options, (o) => o.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<ComboboxOption<string>> = 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"
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Host tag" labelWidth={12}>
|
||||
<HostTagQueryEditor
|
||||
hostTagOptions={hostTagsOptions}
|
||||
evalTypeValue={query.evaltype}
|
||||
hostTagOptionsLoading={hostTagsLoading}
|
||||
onHostTagFilterChange={onHostTagFilterChange}
|
||||
onHostTagEvalTypeChange={onHostTagEvalTypeChange}
|
||||
version={datasource.zabbix.version}
|
||||
/>
|
||||
</InlineField>
|
||||
<InlineField label="Host" labelWidth={12}>
|
||||
<MetricPicker
|
||||
width={24}
|
||||
|
||||
23
src/datasource/components/QueryEditor/types.ts
Normal file
23
src/datasource/components/QueryEditor/types.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
export enum HostTagOperatorLabel {
|
||||
Exists = 'Exists',
|
||||
Equals = 'Equals',
|
||||
Contains = 'Contains',
|
||||
DoesNotExist = 'Does not exist',
|
||||
DoesNotEqual = 'Does not equal',
|
||||
DoesNotContain = 'Does not contain',
|
||||
}
|
||||
|
||||
export enum HostTagOperatorValue {
|
||||
Contains = '0', // default
|
||||
Equals = '1',
|
||||
DoesNotContain = '2',
|
||||
DoesNotEqual = '3',
|
||||
Exists = '4',
|
||||
DoesNotExist = '5',
|
||||
}
|
||||
|
||||
export enum HostTagOperatorLabelBefore70 {
|
||||
NotExist = 'Not exists',
|
||||
NotEqual = 'Not equal',
|
||||
NotLike = 'Not like',
|
||||
}
|
||||
89
src/datasource/components/QueryEditor/utils.test.ts
Normal file
89
src/datasource/components/QueryEditor/utils.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { getHostTagOptionLabel, getVariableOptions, processHostTags } from './utils';
|
||||
import { HostTagOperatorLabel, HostTagOperatorLabelBefore70, HostTagOperatorValue } from './types';
|
||||
|
||||
jest.mock(
|
||||
'@grafana/runtime',
|
||||
() => ({
|
||||
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('');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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 '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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<any[]> {
|
||||
getHosts(
|
||||
groupids: string[],
|
||||
getHostTags?: boolean,
|
||||
hostTagFilters?: HostTagFilter[],
|
||||
evalType?: ZabbixTagEvalType
|
||||
): Promise<any[]> {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<any> {
|
||||
@@ -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<any[]> {
|
||||
getAllHosts(
|
||||
groupFilter: string,
|
||||
getHostTags?: boolean,
|
||||
hostTagFilters?: HostTagFilter[],
|
||||
evalType?: ZabbixTagEvalType
|
||||
): Promise<Host[]> {
|
||||
return this.getGroups(groupFilter).then((groups) => {
|
||||
const groupids = _.map(groups, 'groupid');
|
||||
return this.zabbixAPI.getHosts(groupids);
|
||||
return this.zabbixAPI.getHosts(groupids, getHostTags, hostTagFilters, evalType);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user