SLA support in Zabbix 6.0 (#1547)

This commit is contained in:
Alexander Zobnin
2022-12-29 15:32:39 +03:00
committed by GitHub
parent 42281a6577
commit 1cee6f0ae3
8 changed files with 316 additions and 117 deletions

View File

@@ -11,7 +11,7 @@ import { QueryOptionsEditor } from './QueryEditor/QueryOptionsEditor';
import { TextMetricsQueryEditor } from './QueryEditor/TextMetricsQueryEditor'; import { TextMetricsQueryEditor } from './QueryEditor/TextMetricsQueryEditor';
import { ProblemsQueryEditor } from './QueryEditor/ProblemsQueryEditor'; import { ProblemsQueryEditor } from './QueryEditor/ProblemsQueryEditor';
import { ItemIdQueryEditor } from './QueryEditor/ItemIdQueryEditor'; import { ItemIdQueryEditor } from './QueryEditor/ItemIdQueryEditor';
import { ITServicesQueryEditor } from './QueryEditor/ITServicesQueryEditor'; import { ServicesQueryEditor } from './QueryEditor/ServicesQueryEditor';
import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor'; import { TriggersQueryEditor } from './QueryEditor/TriggersQueryEditor';
const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [ const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
@@ -27,8 +27,8 @@ const zabbixQueryTypeOptions: Array<SelectableValue<string>> = [
}, },
{ {
value: c.MODE_ITSERVICE, value: c.MODE_ITSERVICE,
label: 'IT Services', label: 'Services',
description: 'Query IT Services data', description: 'Query services SLA',
}, },
{ {
value: c.MODE_ITEMID, value: c.MODE_ITEMID,
@@ -75,9 +75,10 @@ const getDefaultQuery: () => Partial<ZabbixMetricsQuery> = () => ({
}, },
}); });
function getSLAQueryDefaults() { function getSLAQueryDefaults(): Partial<ZabbixMetricsQuery> {
return { return {
itServiceFilter: '', itServiceFilter: '',
slaFilter: '',
slaProperty: 'sla', slaProperty: 'sla',
slaInterval: 'none', slaInterval: 'none',
}; };
@@ -166,7 +167,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: ZabbixQ
const renderITServicesEditor = () => { const renderITServicesEditor = () => {
return ( return (
<> <>
<ITServicesQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} /> <ServicesQueryEditor query={query} datasource={datasource} onChange={onChangeInternal} />
<QueryFunctionsEditor query={query} onChange={onChangeInternal} /> <QueryFunctionsEditor query={query} onChange={onChangeInternal} />
</> </>
); );

View File

@@ -1,102 +0,0 @@
import _ from 'lodash';
import React, { useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, Select } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const slaPropertyList: Array<SelectableValue<string>> = [
{ label: 'Status', value: 'status' },
{ label: 'SLA', value: 'sla' },
{ label: 'OK time', value: 'okTime' },
{ label: 'Problem time', value: 'problemTime' },
{ label: 'Down time', value: 'downtimeTime' },
];
const slaIntervals: Array<SelectableValue<string>> = [
{ label: 'No interval', value: 'none' },
{ label: 'Auto', value: 'auto' },
{ label: '1 hour', value: '1h' },
{ label: '12 hours', value: '12h' },
{ label: '24 hours', value: '1d' },
{ label: '1 week', value: '1w' },
{ label: '1 month', value: '1M' },
];
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const ITServicesQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadITServiceOptions = async () => {
const services = await datasource.zabbix.getITService();
const options = services?.map((s) => ({
value: s.name,
label: s.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: itServicesLoading, value: itServicesOptions }, fetchITServices] = useAsyncFn(async () => {
const options = await loadITServiceOptions();
return options;
}, []);
useEffect(() => {
fetchITServices();
}, []);
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value) {
onChange({ ...query, [prop]: option.value });
}
};
};
const onITServiceChange = (value: string) => {
if (value !== null) {
onChange({ ...query, itServiceFilter: value });
}
};
return (
<QueryEditorRow>
<InlineField label="IT Service" labelWidth={12}>
<MetricPicker
width={24}
value={query.itServiceFilter}
options={itServicesOptions}
isLoading={itServicesLoading}
onChange={onITServiceChange}
/>
</InlineField>
<InlineField label="Property" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.slaProperty}
options={slaPropertyList}
onChange={onPropChange('slaProperty')}
/>
</InlineField>
<InlineField label="Interval" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.slaInterval}
options={slaIntervals}
onChange={onPropChange('slaInterval')}
/>
</InlineField>
</QueryEditorRow>
);
};

View File

@@ -0,0 +1,133 @@
import _ from 'lodash';
import React, { useEffect } from 'react';
import { useAsyncFn } from 'react-use';
import { SelectableValue } from '@grafana/data';
import { InlineField, Select } from '@grafana/ui';
import { QueryEditorRow } from './QueryEditorRow';
import { MetricPicker } from '../../../components';
import { getVariableOptions } from './utils';
import { ZabbixDatasource } from '../../datasource';
import { ZabbixMetricsQuery } from '../../types';
const slaPropertyList: Array<SelectableValue<string>> = [
{ label: 'Status', value: 'status' },
{ label: 'SLI', value: 'sli' },
{ label: 'Uptime', value: 'uptime' },
{ label: 'Downtime', value: 'downtime' },
{ label: 'Error budget', value: 'error_budget' },
];
const slaIntervals: Array<SelectableValue<string>> = [
{ label: 'No interval', value: 'none' },
{ label: 'Auto', value: 'auto' },
{ label: '1 hour', value: '1h' },
{ label: '12 hours', value: '12h' },
{ label: '24 hours', value: '1d' },
{ label: '1 week', value: '1w' },
{ label: '1 month', value: '1M' },
];
export interface Props {
query: ZabbixMetricsQuery;
datasource: ZabbixDatasource;
onChange: (query: ZabbixMetricsQuery) => void;
}
export const ServicesQueryEditor = ({ query, datasource, onChange }: Props) => {
const loadITServiceOptions = async () => {
const services = await datasource.zabbix.getITService();
const options = services?.map((s) => ({
value: s.name,
label: s.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: itServicesLoading, value: itServicesOptions }, fetchITServices] = useAsyncFn(async () => {
const options = await loadITServiceOptions();
return options;
}, []);
const loadSLAOptions = async () => {
const slaOptions = await datasource.zabbix.getSLAList();
const options = slaOptions?.map((s) => ({
value: s.name,
label: s.name,
}));
options.unshift(...getVariableOptions());
return options;
};
const [{ loading: slaLoading, value: slaOptions }, fetchSLAOptions] = useAsyncFn(async () => {
const options = await loadSLAOptions();
return options;
}, []);
useEffect(() => {
fetchITServices();
fetchSLAOptions();
}, []);
const onPropChange = (prop: string) => {
return (option: SelectableValue) => {
if (option.value) {
onChange({ ...query, [prop]: option.value });
}
};
};
const onStringPropChange = (prop: string) => {
return (value: string) => {
if (value !== undefined) {
onChange({ ...query, [prop]: value });
}
};
};
return (
<>
<QueryEditorRow>
<InlineField label="Service" labelWidth={12}>
<MetricPicker
width={24}
value={query.itServiceFilter}
options={itServicesOptions}
isLoading={itServicesLoading}
onChange={onStringPropChange('itServiceFilter')}
/>
</InlineField>
<InlineField label="SLA" labelWidth={12}>
<MetricPicker
width={24}
value={query.slaFilter}
options={slaOptions}
isLoading={slaLoading}
onChange={onStringPropChange('slaFilter')}
/>
</InlineField>
</QueryEditorRow>
<QueryEditorRow>
<InlineField label="Property" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.slaProperty}
options={slaPropertyList}
onChange={onPropChange('slaProperty')}
/>
</InlineField>
<InlineField label="Interval" labelWidth={12}>
<Select
isSearchable={false}
width={24}
value={query.slaInterval}
options={slaIntervals}
onChange={onPropChange('slaInterval')}
/>
</InlineField>
</QueryEditorRow>
</>
);
};

View File

@@ -470,14 +470,14 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
/** /**
* Query target data for IT Services * Query target data for IT Services
*/ */
async queryITServiceData(target, timeRange, request) { async queryITServiceData(target: ZabbixMetricsQuery, timeRange, request) {
// Don't show undefined and hidden targets // Don't show undefined and hidden targets
if (target.hide || (!target.itservice && !target.itServiceFilter) || !target.slaProperty) { if (target.hide || (!(target as any).itservice && !target.itServiceFilter) || !target.slaProperty) {
return []; return [];
} }
let itServiceFilter; let itServiceFilter;
request.isOldVersion = target.itservice && !target.itServiceFilter; request.isOldVersion = (target as any).itservice && !target.itServiceFilter;
if (request.isOldVersion) { if (request.isOldVersion) {
// Backward compatibility // Backward compatibility
@@ -490,7 +490,13 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
let itservices = await this.zabbix.getITServices(itServiceFilter); let itservices = await this.zabbix.getITServices(itServiceFilter);
if (request.isOldVersion) { if (request.isOldVersion) {
itservices = _.filter(itservices, { serviceid: target.itservice?.serviceid }); itservices = _.filter(itservices, { serviceid: (target as any).itservice?.serviceid });
}
if (target.slaFilter !== undefined) {
const slaFilter = this.replaceTemplateVars(target.slaFilter, request.scopedVars);
const slas = await this.zabbix.getSLAs(slaFilter);
const result = await this.zabbix.getSLI(itservices, slas, timeRange, target, request);
return result;
} }
const itservicesdp = await this.zabbix.getSLA(itservices, timeRange, target, request); const itservicesdp = await this.zabbix.getSLA(itservices, timeRange, target, request);
const backendRequest = responseHandler.itServiceResponseToTimeSeries(itservicesdp, target.slaInterval); const backendRequest = responseHandler.itServiceResponseToTimeSeries(itservicesdp, target.slaInterval);

View File

@@ -467,7 +467,105 @@ function extractText(str, pattern, useCaptureGroups) {
return ''; return '';
} }
function handleSLAResponse(itservice, slaProperty, slaObject) { export function handleSLIResponse(response: any, itservices: any[], target: ZabbixMetricsQuery) {
const timestamps = [];
for (let i = 0; i < response?.periods?.length; i++) {
const period = response.periods[i];
if (i === 0) {
timestamps.push(period.period_from * 1000);
}
timestamps.push(period.period_to * 1000);
}
const timeFiled: Field = {
name: TIME_SERIES_TIME_FIELD_NAME,
type: FieldType.time,
config: {
custom: {},
},
values: new ArrayVector<number>(timestamps),
};
const valueFields: Field[] = [];
const values: number[][] = [];
let slaProperty = mapLegacySLAProperty(target.slaProperty);
for (let i = 0; i < response?.sli?.length; i++) {
const slis = response.sli[i];
for (let j = 0; j < slis.length; j++) {
const sli = slis[j];
const value = sli[slaProperty];
if (!values[j]) {
values[j] = [];
}
if (i === 0) {
values[j].push(value);
}
values[j].push(value);
}
}
for (let i = 0; i < response?.serviceids?.length; i++) {
const serviceId = response?.serviceids[i].toString();
const service = itservices.find((s) => s.serviceid === serviceId);
valueFields.push({
name: service ? service.name : serviceId,
type: FieldType.number,
config: {},
values: new ArrayVector<number>(values[i]),
});
}
return new MutableDataFrame({
refId: target.refId,
name: 'SLI',
fields: [timeFiled, ...valueFields],
});
}
function mapLegacySLAProperty(property: string) {
switch (property) {
case 'sla':
return 'sli';
case 'okTime':
return 'uptime';
case 'downtimeTime':
return 'downtime';
default:
return property;
}
}
export function handleServiceResponse(response: any, itservices: any[], target: ZabbixMetricsQuery) {
const valueFields: Field[] = [];
for (let i = 0; i < response?.length; i++) {
const service = response[i];
const status = Number(service.status);
valueFields.push({
name: service ? service.name : i,
type: FieldType.number,
config: {},
values: new ArrayVector<number>([status]),
});
}
const timeFiled: Field = {
name: TIME_SERIES_TIME_FIELD_NAME,
type: FieldType.time,
config: {
custom: {},
},
values: new ArrayVector<number>([Date.now()]),
};
return new MutableDataFrame({
refId: target.refId,
name: 'Service status',
fields: [timeFiled, ...valueFields],
});
}
export function handleSLAResponse(itservice, slaProperty, slaObject) {
const targetSLA = slaObject[itservice.serviceid].sla; const targetSLA = slaObject[itservice.serviceid].sla;
if (slaProperty === 'status') { if (slaProperty === 'status') {
const targetStatus = parseInt(slaObject[itservice.serviceid].status, 10); const targetStatus = parseInt(slaObject[itservice.serviceid].status, 10);

View File

@@ -48,6 +48,7 @@ export interface ZabbixMetricsQuery extends DataQuery {
proxy?: { filter: string }; proxy?: { filter: string };
trigger?: { filter: string }; trigger?: { filter: string };
itServiceFilter?: string; itServiceFilter?: string;
slaFilter?: string;
slaProperty?: any; slaProperty?: any;
slaInterval?: string; slaInterval?: string;
tags?: { filter: string }; tags?: { filter: string };

View File

@@ -330,7 +330,7 @@ export class ZabbixAPIConnector {
return self.request('trend.get', params); return self.request('trend.get', params);
} }
getITService(serviceids?) { getITService(serviceids?: any[]) {
const params = { const params = {
output: 'extend', output: 'extend',
serviceids: serviceids, serviceids: serviceids,
@@ -338,6 +338,22 @@ export class ZabbixAPIConnector {
return this.request('service.get', params); return this.request('service.get', params);
} }
// Returns services. Non-cached method (for getting actual service status).
getServices(serviceids?: any[]) {
const params = {
output: 'extend',
serviceids: serviceids,
};
return this.request('service.get', params);
}
getSLAList() {
const params = {
output: 'extend',
};
return this.request('sla.get', params);
}
getSLA(serviceids, timeRange, options) { getSLA(serviceids, timeRange, options) {
const [timeFrom, timeTo] = timeRange; const [timeFrom, timeTo] = timeRange;
let intervals = [{ from: timeFrom, to: timeTo }]; let intervals = [{ from: timeFrom, to: timeTo }];
@@ -416,6 +432,29 @@ export class ZabbixAPIConnector {
return slaLikeResponse; return slaLikeResponse;
} }
async getSLI(slaid, serviceids, timeRange, options) {
const [timeFrom, timeTo] = timeRange;
let intervals = [{ from: timeFrom, to: timeTo }];
if (options.slaInterval === 'auto') {
const interval = getSLAInterval(options.intervalMs);
intervals = buildSLAIntervals(timeRange, interval);
} else if (options.slaInterval !== 'none') {
const interval = utils.parseInterval(options.slaInterval) / 1000;
intervals = buildSLAIntervals(timeRange, interval);
}
const sliParams: any = {
slaid,
serviceids,
period_from: timeFrom,
period_to: timeTo,
periods: Math.min(intervals.length, 100),
};
const sliResponse = await this.request('sla.getsli', sliParams);
return sliResponse;
}
getProblems(groupids, hostids, applicationids, options): Promise<ZBXProblem[]> { getProblems(groupids, hostids, applicationids, options): Promise<ZBXProblem[]> {
const { timeFrom, timeTo, recent, severities, limit, acknowledged, tags } = options; const { timeFrom, timeTo, recent, severities, limit, acknowledged, tags } = options;

View File

@@ -3,7 +3,7 @@ import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import semver from 'semver'; import semver from 'semver';
import * as utils from '../utils'; import * as utils from '../utils';
import responseHandler from '../responseHandler'; import responseHandler, { handleServiceResponse, handleSLIResponse } from '../responseHandler';
import { CachingProxy } from './proxy/cachingProxy'; import { CachingProxy } from './proxy/cachingProxy';
import { DBConnector } from './connectors/dbConnector'; import { DBConnector } from './connectors/dbConnector';
import { ZabbixAPIConnector } from './connectors/zabbix_api/zabbixAPIConnector'; import { ZabbixAPIConnector } from './connectors/zabbix_api/zabbixAPIConnector';
@@ -11,7 +11,7 @@ import { SQLConnector } from './connectors/sql/sqlConnector';
import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector'; import { InfluxDBConnector } from './connectors/influxdb/influxdbConnector';
import { ZabbixConnector } from './types'; import { ZabbixConnector } from './types';
import { joinTriggersWithEvents, joinTriggersWithProblems } from '../problemsHandler'; import { joinTriggersWithEvents, joinTriggersWithProblems } from '../problemsHandler';
import { ProblemDTO, ZBXItem, ZBXItemTag } from '../types'; import { ProblemDTO, ZabbixMetricsQuery, ZBXItem, ZBXItemTag } from '../types';
interface AppsResponse extends Array<any> { interface AppsResponse extends Array<any> {
appFilterEmpty?: boolean; appFilterEmpty?: boolean;
@@ -41,6 +41,7 @@ const REQUESTS_TO_PROXYFY = [
'getTriggersByIds', 'getTriggersByIds',
'getScripts', 'getScripts',
'getValueMappings', 'getValueMappings',
'getSLAList',
]; ];
const REQUESTS_TO_CACHE = [ const REQUESTS_TO_CACHE = [
@@ -53,6 +54,7 @@ const REQUESTS_TO_CACHE = [
'getITService', 'getITService',
'getProxies', 'getProxies',
'getValueMappings', 'getValueMappings',
'getSLAList',
]; ];
const REQUESTS_TO_BIND = [ const REQUESTS_TO_BIND = [
@@ -72,6 +74,7 @@ const REQUESTS_TO_BIND = [
'getScripts', 'getScripts',
'executeScript', 'executeScript',
'getValueMappings', 'getValueMappings',
'getSLAList',
]; ];
export class Zabbix implements ZabbixConnector { export class Zabbix implements ZabbixConnector {
@@ -97,6 +100,7 @@ export class Zabbix implements ZabbixConnector {
getExtendedEventData: (eventids) => Promise<any>; getExtendedEventData: (eventids) => Promise<any>;
getMacros: (hostids: any[]) => Promise<any>; getMacros: (hostids: any[]) => Promise<any>;
getValueMappings: () => Promise<any>; getValueMappings: () => Promise<any>;
getSLAList: () => Promise<any>;
constructor(options) { constructor(options) {
const { const {
@@ -396,8 +400,14 @@ export class Zabbix implements ZabbixConnector {
}); });
} }
getITServices(itServiceFilter) { async getITServices(itServiceFilter: string) {
return this.zabbixAPI.getITService().then((itServices) => findByFilter(itServices, itServiceFilter)); const itServices = await this.zabbixAPI.getITService();
return findByFilter(itServices, itServiceFilter);
}
async getSLAs(slaFilter: string) {
const slas = await this.zabbixAPI.getSLAList();
return findByFilter(slas, slaFilter);
} }
getProblems(groupFilter, hostFilter, appFilter, proxyFilter?, options?): Promise<ProblemDTO[]> { getProblems(groupFilter, hostFilter, appFilter, proxyFilter?, options?): Promise<ProblemDTO[]> {
@@ -538,6 +548,19 @@ export class Zabbix implements ZabbixConnector {
} }
} }
async getSLI(itservices: any[], slas: any[], timeRange, target: ZabbixMetricsQuery, options) {
const itServiceIds = itservices.map((s) => s.serviceid);
if (target.slaProperty === 'status') {
const res = await this.zabbixAPI.getServices(itServiceIds);
return handleServiceResponse(res, itservices, target);
}
const slaIds = slas.map((s) => s.slaid);
const slaId = slaIds?.length > 0 ? slaIds[0] : undefined;
const result = await this.zabbixAPI.getSLI(slaId, itServiceIds, timeRange, options);
return handleSLIResponse(result, itservices, target);
}
async getSLA(itservices, timeRange, target, options) { async getSLA(itservices, timeRange, target, options) {
const itServiceIds = _.map(itservices, 'serviceid'); const itServiceIds = _.map(itservices, 'serviceid');
if (this.supportSLA()) { if (this.supportSLA()) {