Support item tags, fixes #1258

This commit is contained in:
Alexander Zobnin
2021-08-09 14:57:49 +03:00
parent c6441cccf0
commit 915973829d
15 changed files with 574 additions and 364 deletions

View File

@@ -75,6 +75,7 @@ type QueryModel struct {
Group QueryFilter `json:"group"` Group QueryFilter `json:"group"`
Host QueryFilter `json:"host"` Host QueryFilter `json:"host"`
Application QueryFilter `json:"application"` Application QueryFilter `json:"application"`
ItemTag QueryFilter `json:"itemTag"`
Item QueryFilter `json:"item"` Item QueryFilter `json:"item"`
// Item ID mode // Item ID mode

View File

@@ -47,9 +47,10 @@ func (ds *ZabbixDatasourceInstance) queryNumericItems(ctx context.Context, query
groupFilter := query.Group.Filter groupFilter := query.Group.Filter
hostFilter := query.Host.Filter hostFilter := query.Host.Filter
appFilter := query.Application.Filter appFilter := query.Application.Filter
itemTagFilter := query.ItemTag.Filter
itemFilter := query.Item.Filter itemFilter := query.Item.Filter
items, err := ds.zabbix.GetItems(ctx, groupFilter, hostFilter, appFilter, itemFilter, "num") items, err := ds.zabbix.GetItems(ctx, groupFilter, hostFilter, appFilter, itemTagFilter, itemFilter, "num")
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -2,6 +2,8 @@ package zabbix
import ( import (
"context" "context"
"strconv"
"strings"
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
) )
@@ -78,7 +80,7 @@ func (ds *Zabbix) getTrend(ctx context.Context, itemids []string, timeRange back
return trend, err return trend, err
} }
func (ds *Zabbix) GetItems(ctx context.Context, groupFilter string, hostFilter string, appFilter string, itemFilter string, itemType string) ([]*Item, error) { func (ds *Zabbix) GetItems(ctx context.Context, groupFilter string, hostFilter string, appFilter string, itemTagFilter string, itemFilter string, itemType string) ([]*Item, error) {
hosts, err := ds.GetHosts(ctx, groupFilter, hostFilter) hosts, err := ds.GetHosts(ctx, groupFilter, hostFilter)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -90,7 +92,8 @@ func (ds *Zabbix) GetItems(ctx context.Context, groupFilter string, hostFilter s
apps, err := ds.GetApps(ctx, groupFilter, hostFilter, appFilter) apps, err := ds.GetApps(ctx, groupFilter, hostFilter, appFilter)
// Apps not supported in Zabbix 5.4 and higher // Apps not supported in Zabbix 5.4 and higher
if isAppMethodNotFoundError(err) { isZabbix54orHigher := isAppMethodNotFoundError(err)
if isZabbix54orHigher {
apps = []Application{} apps = []Application{}
} else if err != nil { } else if err != nil {
return nil, err return nil, err
@@ -107,9 +110,50 @@ func (ds *Zabbix) GetItems(ctx context.Context, groupFilter string, hostFilter s
allItems, err = ds.GetAllItems(ctx, nil, appids, itemType) allItems, err = ds.GetAllItems(ctx, nil, appids, itemType)
} }
if isZabbix54orHigher && itemTagFilter != "" {
allItems, err = filterItemsByTag(allItems, itemTagFilter)
if err != nil {
return nil, err
}
}
return filterItemsByQuery(allItems, itemFilter) return filterItemsByQuery(allItems, itemFilter)
} }
func filterItemsByTag(items []*Item, filter string) ([]*Item, error) {
re, err := parseFilter(filter)
if err != nil {
return nil, err
}
var filteredItems []*Item
for _, i := range items {
if len(i.Tags) == 0 && filter == "/.*/" {
filteredItems = append(filteredItems, i)
}
if len(i.Tags) > 0 {
var tags []string
for _, t := range i.Tags {
tags = append(tags, itemTagToString(t))
}
for _, t := range tags {
if re != nil {
if re.MatchString(t) {
filteredItems = append(filteredItems, i)
break
}
} else if t == filter {
filteredItems = append(filteredItems, i)
break
}
}
}
}
return filteredItems, nil
}
func filterItemsByQuery(items []*Item, filter string) ([]*Item, error) { func filterItemsByQuery(items []*Item, filter string) ([]*Item, error) {
re, err := parseFilter(filter) re, err := parseFilter(filter)
if err != nil { if err != nil {
@@ -259,6 +303,10 @@ func (ds *Zabbix) GetAllItems(ctx context.Context, hostids []string, appids []st
filter["value_type"] = []int{1, 2, 4} filter["value_type"] = []int{1, 2, 4}
} }
if ds.version >= 54 {
params["selectTags"] = "extend"
}
result, err := ds.Request(ctx, &ZabbixAPIRequest{Method: "item.get", Params: params}) result, err := ds.Request(ctx, &ZabbixAPIRequest{Method: "item.get", Params: params})
if err != nil { if err != nil {
return nil, err return nil, err
@@ -347,6 +395,23 @@ func (ds *Zabbix) GetAllGroups(ctx context.Context) ([]Group, error) {
return groups, err return groups, err
} }
func (ds *Zabbix) GetVersion(ctx context.Context) (int, error) {
result, err := ds.request(ctx, "apiinfo.version", ZabbixAPIParams{})
if err != nil {
return 0, err
}
var version string
err = convertTo(result, &version)
if err != nil {
return 0, err
}
version = strings.Replace(version[0:3], ".", "", 1)
versionNum, err := strconv.Atoi(version)
return versionNum, err
}
func isAppMethodNotFoundError(err error) bool { func isAppMethodNotFoundError(err error) bool {
if err == nil { if err == nil {
return false return false

View File

@@ -51,6 +51,7 @@ type Item struct {
Delay string `json:"delay,omitempty"` Delay string `json:"delay,omitempty"`
Units string `json:"units,omitempty"` Units string `json:"units,omitempty"`
ValueMapID string `json:"valuemapid,omitempty"` ValueMapID string `json:"valuemapid,omitempty"`
Tags []ItemTag `json:"tags,omitempty"`
} }
type ItemHost struct { type ItemHost struct {
@@ -58,6 +59,11 @@ type ItemHost struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
} }
type ItemTag struct {
Tag string `json:"tag,omitempty"`
Value string `json:"value,omitempty"`
}
type Trend []TrendPoint type Trend []TrendPoint
type TrendPoint struct { type TrendPoint struct {

View File

@@ -85,3 +85,11 @@ func parseFilter(filter string) (*regexp.Regexp, error) {
return regexp.Compile(pattern) return regexp.Compile(pattern)
} }
func itemTagToString(tag ItemTag) string {
if tag.Value != "" {
return fmt.Sprintf("%s: %s", tag.Tag, tag.Value)
} else {
return tag.Tag
}
}

View File

@@ -17,6 +17,7 @@ type Zabbix struct {
api *zabbixapi.ZabbixAPI api *zabbixapi.ZabbixAPI
dsInfo *backend.DataSourceInstanceSettings dsInfo *backend.DataSourceInstanceSettings
cache *ZabbixCache cache *ZabbixCache
version int
logger log.Logger logger log.Logger
} }
@@ -49,6 +50,16 @@ func (ds *Zabbix) Request(ctx context.Context, apiReq *ZabbixAPIRequest) (*simpl
var resultJson *simplejson.Json var resultJson *simplejson.Json
var err error var err error
if ds.version == 0 {
version, err := ds.GetVersion(ctx)
if err != nil {
ds.logger.Error("Error querying Zabbix version", "error", err)
} else {
ds.logger.Debug("Got Zabbix version", "version", version)
ds.version = version
}
}
cachedResult, queryExistInCache := ds.cache.GetAPIRequest(apiReq) cachedResult, queryExistInCache := ds.cache.GetAPIRequest(apiReq)
if !queryExistInCache { if !queryExistInCache {
resultJson, err = ds.request(ctx, apiReq.Method, apiReq.Params) resultJson, err = ds.request(ctx, apiReq.Method, apiReq.Params)

View File

@@ -680,7 +680,7 @@ export class ZabbixDatasource extends DataSourceApi<ZabbixMetricsQuery, ZabbixDS
resultPromise = this.zabbix.getApps(queryModel.group, queryModel.host, queryModel.application); resultPromise = this.zabbix.getApps(queryModel.group, queryModel.host, queryModel.application);
break; break;
case VariableQueryTypes.Item: case VariableQueryTypes.Item:
resultPromise = this.zabbix.getItems(queryModel.group, queryModel.host, queryModel.application, queryModel.item); resultPromise = this.zabbix.getItems(queryModel.group, queryModel.host, queryModel.application, null, queryModel.item);
break; break;
case VariableQueryTypes.ItemValues: case VariableQueryTypes.ItemValues:
const range = options?.range; const range = options?.range;

View File

@@ -68,6 +68,15 @@ function migrateProblemSort(target) {
} }
} }
function migrateApplications(target) {
if (!target.itemTag) {
target.itemTag = { filter: '' };
if (target.application?.filter) {
target.itemTag.filter = `Application: ${target.application?.filter}`;
}
}
}
export function migrate(target) { export function migrate(target) {
target.resultFormat = target.resultFormat || 'time_series'; target.resultFormat = target.resultFormat || 'time_series';
target = fixTargetGroup(target); target = fixTargetGroup(target);
@@ -78,6 +87,7 @@ export function migrate(target) {
migrateQueryType(target); migrateQueryType(target);
migrateSLA(target); migrateSLA(target);
migrateProblemSort(target); migrateProblemSort(target);
migrateApplications(target);
return target; return target;
} }
@@ -97,6 +107,7 @@ function convertToRegex(str) {
} }
export const DS_CONFIG_SCHEMA = 2; export const DS_CONFIG_SCHEMA = 2;
export function migrateDSConfig(jsonData) { export function migrateDSConfig(jsonData) {
if (!jsonData) { if (!jsonData) {
jsonData = {}; jsonData = {};

View File

@@ -14,7 +14,8 @@
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.TEXT"> <div class="gf-form" ng-show="ctrl.target.queryType == editorMode.TEXT">
<label class="gf-form-label query-keyword width-7">Format As</label> <label class="gf-form-label query-keyword width-7">Format As</label>
<div class="gf-form-select-wrapper"> <div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto" ng-model="ctrl.target.resultFormat" ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select> <select class="gf-form-input gf-size-auto" ng-model="ctrl.target.resultFormat"
ng-options="f.value as f.text for f in ctrl.resultFormats" ng-change="ctrl.refresh()"></select>
</div> </div>
</div> </div>
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
@@ -64,7 +65,8 @@
</div> </div>
</div> </div>
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS"> <div class="gf-form-inline"
ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<!-- Select Group --> <!-- Select Group -->
<div class="gf-form max-width-20"> <div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Group</label> <label class="gf-form-label query-keyword width-7">Group</label>
@@ -116,13 +118,13 @@
</div> </div>
</div> </div>
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS"> <div class="gf-form-inline"
ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT || ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<!-- Select Application --> <!-- Select Application -->
<div class="gf-form max-width-20"> <div class="gf-form max-width-20" ng-show="!ctrl.appFilterDisabled()">
<label class="gf-form-label query-keyword width-7">Application</label> <label class="gf-form-label query-keyword width-7">Application</label>
<input type="text" <input type="text"
ng-model="ctrl.target.application.filter" ng-model="ctrl.target.application.filter"
ng-disabled="ctrl.appFilterDisabled()"
bs-typeahead="ctrl.getApplicationNames" bs-typeahead="ctrl.getApplicationNames"
ng-blur="ctrl.onTargetBlur()" ng-blur="ctrl.onTargetBlur()"
data-min-length=0 data-min-length=0
@@ -134,8 +136,25 @@
}"> }">
</div> </div>
<!-- Select item tags -->
<div class="gf-form max-width-20" ng-show="ctrl.appFilterDisabled()">
<label class="gf-form-label query-keyword width-7">Item tag</label>
<input type="text"
ng-model="ctrl.target.itemTag.filter"
bs-typeahead="ctrl.getItemTags"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.itemTag.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.itemTag.filter)
}">
</div>
<!-- Select Item --> <!-- Select Item -->
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT"> <div class="gf-form max-width-20"
ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.TEXT">
<label class="gf-form-label query-keyword width-7">Item</label> <label class="gf-form-label query-keyword width-7">Item</label>
<input type="text" <input type="text"
ng-model="ctrl.target.item.filter" ng-model="ctrl.target.item.filter"
@@ -177,7 +196,8 @@
</div> </div>
</div> </div>
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS"> <div class="gf-form-inline"
ng-show="ctrl.target.queryType == editorMode.TRIGGERS || ctrl.target.queryType == editorMode.PROBLEMS">
<div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS"> <div class="gf-form max-width-20" ng-show="ctrl.target.queryType == editorMode.PROBLEMS">
<label class="gf-form-label query-keyword width-7">Show</label> <label class="gf-form-label query-keyword width-7">Show</label>
<div class="gf-form-select-wrapper max-width-20"> <div class="gf-form-select-wrapper max-width-20">
@@ -227,7 +247,8 @@
ng-blur="ctrl.onTargetBlur()"> ng-blur="ctrl.onTargetBlur()">
</div> </div>
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()"> <gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups"
on-change="ctrl.onTargetBlur()">
</gf-form-switch> </gf-form-switch>
<div class="gf-form gf-form--grow"> <div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div> <div class="gf-form-label gf-form-label--grow"></div>
@@ -257,7 +278,8 @@
</div> </div>
<!-- Metric processing functions --> <!-- Metric processing functions -->
<div class="gf-form-inline" ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.ITEMID || ctrl.target.queryType == editorMode.ITSERVICE"> <div class="gf-form-inline"
ng-show="ctrl.target.queryType == editorMode.METRICS || ctrl.target.queryType == editorMode.ITEMID || ctrl.target.queryType == editorMode.ITSERVICE">
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label query-keyword width-7">Functions</label> <label class="gf-form-label query-keyword width-7">Functions</label>
</div> </div>
@@ -301,7 +323,8 @@
</gf-form-switch> </gf-form-switch>
</div> </div>
<div class="gf-form-group offset-width-7" ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'"> <div class="gf-form-group offset-width-7"
ng-show="ctrl.target.queryType === editorMode.TEXT && ctrl.target.resultFormat === 'table'">
<div class="gf-form"> <div class="gf-form">
<gf-form-switch class="gf-form" label-class="width-10" <gf-form-switch class="gf-form" label-class="width-10"
label="Skip empty values" label="Skip empty values"
@@ -311,14 +334,16 @@
</div> </div>
</div> </div>
<div class="gf-form-group" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS"> <div class="gf-form-group"
ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
<gf-form-switch class="gf-form" ng-show="ctrl.target.queryType == editorMode.TRIGGERS" <gf-form-switch class="gf-form" ng-show="ctrl.target.queryType == editorMode.TRIGGERS"
label-class="width-9" label-class="width-9"
label="Count" label="Count"
checked="ctrl.target.triggers.count" checked="ctrl.target.triggers.count"
on-change="ctrl.onTargetBlur()"> on-change="ctrl.onTargetBlur()">
</gf-form-switch> </gf-form-switch>
<div class="gf-form" ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS"> <div class="gf-form"
ng-show="ctrl.target.queryType == editorMode.PROBLEMS || ctrl.target.queryType == editorMode.TRIGGERS">
<label class="gf-form-label width-9">Acknowledged</label> <label class="gf-form-label width-9">Acknowledged</label>
<div class="gf-form-select-wrapper width-12"> <div class="gf-form-select-wrapper width-12">
<select class="gf-form-input" <select class="gf-form-input"

View File

@@ -2,9 +2,10 @@ import { QueryCtrl } from 'grafana/app/plugins/sdk';
import _ from 'lodash'; import _ from 'lodash';
import * as c from './constants'; import * as c from './constants';
import * as utils from './utils'; import * as utils from './utils';
import { itemTagToString } from './utils';
import * as metricFunctions from './metricFunctions'; import * as metricFunctions from './metricFunctions';
import * as migrations from './migrations'; import * as migrations from './migrations';
import { ShowProblemTypes } from './types'; import { ShowProblemTypes, ZBXItem, ZBXItemTag } from './types';
import { CURRENT_SCHEMA_VERSION } from '../panel-triggers/migrations'; import { CURRENT_SCHEMA_VERSION } from '../panel-triggers/migrations';
import { getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { getTemplateSrv, TemplateSrv } from '@grafana/runtime';
@@ -15,6 +16,7 @@ function getTargetDefaults() {
group: { 'filter': "" }, group: { 'filter': "" },
host: { 'filter': "" }, host: { 'filter': "" },
application: { 'filter': "" }, application: { 'filter': "" },
itemTag: { 'filter': "" },
item: { 'filter': "" }, item: { 'filter': "" },
functions: [], functions: [],
triggers: { triggers: {
@@ -274,7 +276,11 @@ export class ZabbixQueryController extends QueryCtrl {
promises.push(this.suggestProxies()); promises.push(this.suggestProxies());
} }
return Promise.all(promises); return Promise.all(promises).then(() => {
if (this.zabbix.isZabbix54OrHigher()) {
this.suggestItemTags();
}
});
} }
initDefaultQueryMode(target) { initDefaultQueryMode(target) {
@@ -304,12 +310,23 @@ export class ZabbixQueryController extends QueryCtrl {
return metrics; return metrics;
} }
getItemTags = () => {
if (!this.metric?.tagList) {
return [];
}
return this.metric.tagList.map(t => itemTagToString(t));
};
getTemplateVariables() { getTemplateVariables() {
return _.map(this.templateSrv.getVariables(), variable => { return _.map(this.templateSrv.getVariables(), variable => {
return '$' + variable.name; return '$' + variable.name;
}); });
} }
isZabbix54OrHigher() {
return this.zabbix.isZabbix54OrHigher();
}
suggestGroups() { suggestGroups() {
return this.zabbix.getAllGroups() return this.zabbix.getAllGroups()
.then(groups => { .then(groups => {
@@ -341,19 +358,34 @@ export class ZabbixQueryController extends QueryCtrl {
const groupFilter = this.replaceTemplateVars(this.target.group.filter); const groupFilter = this.replaceTemplateVars(this.target.group.filter);
const hostFilter = this.replaceTemplateVars(this.target.host.filter); const hostFilter = this.replaceTemplateVars(this.target.host.filter);
const appFilter = this.replaceTemplateVars(this.target.application.filter); const appFilter = this.replaceTemplateVars(this.target.application.filter);
const itemTagFilter = this.replaceTemplateVars(this.target.itemTag.filter);
const options = { const options = {
itemtype: itemtype, itemtype: itemtype,
showDisabledItems: this.target.options.showDisabledItems showDisabledItems: this.target.options.showDisabledItems
}; };
return this.zabbix return this.zabbix
.getAllItems(groupFilter, hostFilter, appFilter, options) .getAllItems(groupFilter, hostFilter, appFilter, itemTagFilter, options)
.then(items => { .then(items => {
this.metric.itemList = items; this.metric.itemList = items;
return items; return items;
}); });
} }
async suggestItemTags() {
const groupFilter = this.replaceTemplateVars(this.target.group.filter);
const hostFilter = this.replaceTemplateVars(this.target.host.filter);
const items = await this.zabbix.getAllItems(groupFilter, hostFilter, null, null, {});
const tags: ZBXItemTag[] = _.flatten(items.map((item: ZBXItem) => {
if (item.tags) {
return item.tags;
} else {
return [];
}
}));
this.metric.tagList = _.uniqBy(tags, t => t.tag + t.value || '');
}
suggestITServices() { suggestITServices() {
return this.zabbix.getITService() return this.zabbix.getITService()
.then(itservices => { .then(itservices => {

View File

@@ -1,4 +1,4 @@
import { SelectableValue, DataQuery, DataSourceJsonData } from "@grafana/data"; import { DataQuery, DataSourceJsonData, SelectableValue } from "@grafana/data";
export interface ZabbixDSOptions extends DataSourceJsonData { export interface ZabbixDSOptions extends DataSourceJsonData {
username: string; username: string;
@@ -40,6 +40,7 @@ export interface ZabbixMetricsQuery extends DataQuery {
group: { filter: string; name?: string; }; group: { filter: string; name?: string; };
host: { filter: string; name?: string; }; host: { filter: string; name?: string; };
application: { filter: string; name?: string; }; application: { filter: string; name?: string; };
itemTag: { filter: string; name?: string; };
item: { filter: string; name?: string; }; item: { filter: string; name?: string; };
textFilter: string; textFilter: string;
mode: number; mode: number;
@@ -87,7 +88,9 @@ export interface TemplateSrv {
variables: { variables: {
name: string; name: string;
}; };
highlightVariablesAsHtml(str: any): any; highlightVariablesAsHtml(str: any): any;
replace(target: any, scopedVars?: any, format?: any): any; replace(target: any, scopedVars?: any, format?: any): any;
} }
@@ -293,6 +296,12 @@ export interface ZBXItem {
name: string; name: string;
key_: string; key_: string;
lastvalue?: string; lastvalue?: string;
tags?: ZBXItemTag[];
}
export interface ZBXItemTag {
tag: string;
value?: string;
} }
export interface ZBXEvent { export interface ZBXEvent {

View File

@@ -1,7 +1,7 @@
import _ from 'lodash'; import _ from 'lodash';
import moment from 'moment'; import moment from 'moment';
import * as c from './constants'; import * as c from './constants';
import { VariableQuery, VariableQueryTypes } from './types'; import { VariableQuery, VariableQueryTypes, ZBXItemTag } from './types';
import { DataFrame, FieldType, getValueFormats, MappingType, rangeUtil, ValueMapping } from '@grafana/data'; import { DataFrame, FieldType, getValueFormats, MappingType, rangeUtil, ValueMapping } from '@grafana/data';
/* /*
@@ -405,6 +405,21 @@ export function parseTags(tagStr: string): any[] {
return tags; return tags;
} }
// Parses string representation of tag into the object
export function parseItemTag(tagStr: string): ZBXItemTag {
const itemTag: ZBXItemTag = { tag: '', value: '' };
const tagParts = tagStr.split(': ');
itemTag.tag = tagParts[0];
if (tagParts[1]) {
itemTag.value = tagParts[1];
}
return itemTag;
}
export function itemTagToString(t: ZBXItemTag): string {
return t.value ? `${t.tag}: ${t.value}` : t.tag;
}
export function mustArray(result: any): any[] { export function mustArray(result: any): any[] {
return result || []; return result || [];
} }

View File

@@ -2,9 +2,9 @@ import _ from 'lodash';
import semver from 'semver'; import semver from 'semver';
import kbn from 'grafana/app/core/utils/kbn'; import kbn from 'grafana/app/core/utils/kbn';
import * as utils from '../../../utils'; import * as utils from '../../../utils';
import { ZBX_ACK_ACTION_NONE, ZBX_ACK_ACTION_ADD_MESSAGE, MIN_SLA_INTERVAL } from '../../../constants'; import { MIN_SLA_INTERVAL, ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_NONE } from '../../../constants';
import { ShowProblemTypes, ZBXProblem } from '../../../types'; import { ShowProblemTypes, ZBXProblem } from '../../../types';
import { JSONRPCError, ZBXScript, APIExecuteScriptResponse } from './types'; import { APIExecuteScriptResponse, JSONRPCError, ZBXScript } from './types';
import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime'; import { BackendSrvRequest, getBackendSrv } from '@grafana/runtime';
import { rangeUtil } from '@grafana/data'; import { rangeUtil } from '@grafana/data';
@@ -106,6 +106,10 @@ export class ZabbixAPIConnector {
return this.getVersionPromise; return this.getVersionPromise;
} }
isZabbix54OrHigher() {
return semver.gte(this.version, '5.4.0');
}
//////////////////////////////// ////////////////////////////////
// Zabbix API method wrappers // // Zabbix API method wrappers //
//////////////////////////////// ////////////////////////////////
@@ -151,7 +155,7 @@ export class ZabbixAPIConnector {
} }
async getApps(hostids): Promise<any[]> { async getApps(hostids): Promise<any[]> {
if (semver.gte(this.version, '5.4.0')) { if (this.isZabbix54OrHigher()) {
return []; return [];
} }
@@ -203,12 +207,16 @@ export class ZabbixAPIConnector {
params.filter.value_type = [1, 2, 4]; params.filter.value_type = [1, 2, 4];
} }
if (this.isZabbix54OrHigher()) {
params.selectTags = 'extend';
}
return this.request('item.get', params) return this.request('item.get', params)
.then(utils.expandItems); .then(utils.expandItems);
} }
getItemsByIDs(itemids) { getItemsByIDs(itemids) {
const params = { const params: any = {
itemids: itemids, itemids: itemids,
output: [ output: [
'name', 'name',
@@ -225,6 +233,10 @@ export class ZabbixAPIConnector {
selectHosts: ['hostid', 'name'] selectHosts: ['hostid', 'name']
}; };
if (this.isZabbix54OrHigher()) {
params.selectTags = 'extend';
}
return this.request('item.get', params) return this.request('item.get', params)
.then(items => utils.expandItems(items)); .then(items => utils.expandItems(items));
} }

View File

@@ -17,7 +17,7 @@ export interface ZabbixConnector {
getGroups: (groupFilter?) => any; getGroups: (groupFilter?) => any;
getHosts: (groupFilter?, hostFilter?) => any; getHosts: (groupFilter?, hostFilter?) => any;
getApps: (groupFilter?, hostFilter?, appFilter?) => any; getApps: (groupFilter?, hostFilter?, appFilter?) => any;
getItems: (groupFilter?, hostFilter?, appFilter?, itemFilter?, options?) => any; getItems: (groupFilter?, hostFilter?, appFilter?, itemTagFilter?, itemFilter?, options?) => any;
getSLA: (itservices, timeRange, target, options?) => any; getSLA: (itservices, timeRange, target, options?) => any;
supportsApplications: () => boolean; supportsApplications: () => boolean;

View File

@@ -2,6 +2,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 { itemTagToString } from '../utils';
import responseHandler from '../responseHandler'; import responseHandler from '../responseHandler';
import { CachingProxy } from './proxy/cachingProxy'; import { CachingProxy } from './proxy/cachingProxy';
import { DBConnector } from './connectors/dbConnector'; import { DBConnector } from './connectors/dbConnector';
@@ -10,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 } from '../types'; import { ProblemDTO, ZBXItemTag } from '../types';
interface AppsResponse extends Array<any> { interface AppsResponse extends Array<any> {
appFilterEmpty?: boolean; appFilterEmpty?: boolean;
@@ -30,7 +31,7 @@ const REQUESTS_TO_CACHE = [
const REQUESTS_TO_BIND = [ const REQUESTS_TO_BIND = [
'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts', 'getHistory', 'getTrend', 'getMacros', 'getItemsByIDs', 'getEvents', 'getAlerts', 'getHostAlerts',
'getAcknowledges', 'getITService', 'acknowledgeEvent', 'getProxies', 'getEventAlerts', 'getAcknowledges', 'getITService', 'acknowledgeEvent', 'getProxies', 'getEventAlerts',
'getExtendedEventData', 'getScripts', 'executeScript', 'getValueMappings' 'getExtendedEventData', 'getScripts', 'executeScript', 'getValueMappings', 'isZabbix54OrHigher'
]; ];
export class Zabbix implements ZabbixConnector { export class Zabbix implements ZabbixConnector {
@@ -56,6 +57,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>;
isZabbix54OrHigher: () => boolean;
constructor(options) { constructor(options) {
const { const {
@@ -180,7 +182,7 @@ export class Zabbix implements ZabbixConnector {
} }
getItemsFromTarget(target, options) { getItemsFromTarget(target, options) {
const parts = ['group', 'host', 'application', 'item']; const parts = ['group', 'host', 'application', 'itemTag', 'item'];
const filters = _.map(parts, p => target[p].filter); const filters = _.map(parts, p => target[p].filter);
return this.getItems(...filters, options); return this.getItems(...filters, options);
} }
@@ -261,24 +263,36 @@ export class Zabbix implements ZabbixConnector {
}); });
} }
getAllItems(groupFilter, hostFilter, appFilter, options: any = {}) { async getAllItems(groupFilter, hostFilter, appFilter, itemTagFilter, options: any = {}) {
return this.getApps(groupFilter, hostFilter, appFilter) const apps = await this.getApps(groupFilter, hostFilter, appFilter);
.then(apps => { let items: any[];
if (this.isZabbix54OrHigher()) {
items = await this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype);
if (itemTagFilter) {
items = items.filter(item => {
if (item.tags) {
const tags: ZBXItemTag[] = item.tags.map(t => itemTagToString(t));
return tags.includes(itemTagFilter);
} else {
return false;
}
});
}
} else {
if (apps.appFilterEmpty) { if (apps.appFilterEmpty) {
return this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype); items = await this.zabbixAPI.getItems(apps.hostids, undefined, options.itemtype);
} else { } else {
const appids = _.map(apps, 'applicationid'); const appids = _.map(apps, 'applicationid');
return this.zabbixAPI.getItems(undefined, appids, options.itemtype); items = await this.zabbixAPI.getItems(undefined, appids, options.itemtype);
} }
}) }
.then(items => {
if (!options.showDisabledItems) { if (!options.showDisabledItems) {
items = _.filter(items, { 'status': '0' }); items = _.filter(items, { 'status': '0' });
} }
return items; return await this.expandUserMacro(items, false);
})
.then(this.expandUserMacro.bind(this));
} }
expandUserMacro(items, isTriggerItem) { expandUserMacro(items, isTriggerItem) {
@@ -298,13 +312,13 @@ export class Zabbix implements ZabbixConnector {
}); });
} }
getItems(groupFilter?, hostFilter?, appFilter?, itemFilter?, options = {}) { getItems(groupFilter?, hostFilter?, appFilter?, itemTagFilter?, itemFilter?, options = {}) {
return this.getAllItems(groupFilter, hostFilter, appFilter, options) return this.getAllItems(groupFilter, hostFilter, appFilter, itemTagFilter, options)
.then(items => filterByQuery(items, itemFilter)); .then(items => filterByQuery(items, itemFilter));
} }
getItemValues(groupFilter?, hostFilter?, appFilter?, itemFilter?, options: any = {}) { getItemValues(groupFilter?, hostFilter?, appFilter?, itemFilter?, options: any = {}) {
return this.getItems(groupFilter, hostFilter, appFilter, itemFilter, options).then(items => { return this.getItems(groupFilter, hostFilter, appFilter, null, itemFilter, options).then(items => {
let timeRange = [moment().subtract(2, 'h').unix(), moment().unix()]; let timeRange = [moment().subtract(2, 'h').unix(), moment().unix()];
if (options.range) { if (options.range) {
timeRange = [options.range.from.unix(), options.range.to.unix()]; timeRange = [options.range.from.unix(), options.range.to.unix()];