Merge branch 'master' into metric-functions

This commit is contained in:
Alexander Zobnin
2017-07-26 11:53:49 +03:00
74 changed files with 2340 additions and 531 deletions

View File

@@ -0,0 +1,28 @@
import _ from 'lodash';
const SUPPORTED_SQL_DS = ['mysql'];
const defaultConfig = {
dbConnection: {
enable: false,
}
};
export class ZabbixDSConfigController {
/** @ngInject */
constructor($scope, $injector, datasourceSrv) {
this.datasourceSrv = datasourceSrv;
_.defaults(this.current.jsonData, defaultConfig);
this.sqlDataSources = this.getSupportedSQLDataSources();
}
getSupportedSQLDataSources() {
let datasources = this.datasourceSrv.getAll();
return _.filter(datasources, ds => {
return _.includes(SUPPORTED_SQL_DS, ds.type);
});
}
}
ZabbixDSConfigController.templateUrl = 'datasource-zabbix/partials/config.html';

View File

@@ -1,7 +1,8 @@
// Editor modes
export const MODE_METRICS = 0;
export const MODE_TEXT = 2;
export const MODE_ITSERVICE = 1;
export const MODE_TEXT = 2;
export const MODE_ITEMID = 3;
// Triggers severity
export const SEV_NOT_CLASSIFIED = 0;

View File

@@ -19,6 +19,9 @@ class ZabbixAPIDatasource {
this.dashboardSrv = dashboardSrv;
this.zabbixAlertingSrv = zabbixAlertingSrv;
// Use custom format for template variables
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
// General data source settings
this.name = instanceSettings.name;
this.url = instanceSettings.url;
@@ -43,10 +46,21 @@ class ZabbixAPIDatasource {
this.addThresholds = instanceSettings.jsonData.addThresholds;
this.alertingMinSeverity = instanceSettings.jsonData.alertingMinSeverity || c.SEV_WARNING;
this.zabbix = new Zabbix(this.url, this.username, this.password, this.basicAuth, this.withCredentials, this.cacheTTL);
// Direct DB Connection options
this.enableDirectDBConnection = instanceSettings.jsonData.dbConnection.enable;
this.sqlDatasourceId = instanceSettings.jsonData.dbConnection.datasourceId;
// Use custom format for template variables
this.replaceTemplateVars = _.partial(replaceTemplateVars, this.templateSrv);
let zabbixOptions = {
username: this.username,
password: this.password,
basicAuth: this.basicAuth,
withCredentials: this.withCredentials,
cacheTTL: this.cacheTTL,
enableDirectDBConnection: this.enableDirectDBConnection,
sqlDatasourceId: this.sqlDatasourceId
};
this.zabbix = new Zabbix(this.url, zabbixOptions);
}
////////////////////////
@@ -75,6 +89,11 @@ class ZabbixAPIDatasource {
// Create request for each target
let promises = _.map(options.targets, t => {
// Don't request undefined and hidden targets
if (t.hide) {
return [];
}
let timeFrom = Math.ceil(dateMath.parse(options.range.from) / 1000);
let timeTo = Math.ceil(dateMath.parse(options.range.to) / 1000);
@@ -94,7 +113,7 @@ class ZabbixAPIDatasource {
let useTrends = this.isUseTrends(timeRange);
// Metrics or Text query mode
if (target.mode !== c.MODE_ITSERVICE) {
if (target.mode === c.MODE_METRICS || target.mode === c.MODE_TEXT || target.mode === c.MODE_ITEMID) {
// Migrate old targets
target = migrations.migrate(target);
@@ -107,20 +126,12 @@ class ZabbixAPIDatasource {
return this.queryNumericData(target, timeRange, useTrends, options);
} else if (target.mode === c.MODE_TEXT) {
return this.queryTextData(target, timeRange);
} else if (target.mode === c.MODE_ITEMID) {
return this.queryItemIdData(target, timeRange, useTrends, options);
}
}
// IT services mode
else if (target.mode === c.MODE_ITSERVICE) {
// Don't show undefined and hidden targets
if (target.hide || !target.itservice || !target.slaProperty) {
return [];
}
return this.zabbix.getSLA(target.itservice.serviceid, timeRange)
.then(slaObject => {
return responseHandler.handleSLAResponse(target.itservice, target.slaProperty, slaObject);
});
} else if (target.mode === c.MODE_ITSERVICE) {
// IT services mode
return this.queryITServiceData(target, timeRange, options);
}
});
@@ -132,39 +143,55 @@ class ZabbixAPIDatasource {
});
}
/**
* Query target data for Metrics mode
*/
queryNumericData(target, timeRange, useTrends, options) {
let [timeFrom, timeTo] = timeRange;
let getItemOptions = {
itemtype: 'num'
};
return this.zabbix.getItemsFromTarget(target, getItemOptions)
.then(items => {
let getHistoryPromise;
return this.queryNumericDataForItems(items, target, timeRange, useTrends, options);
});
}
if (useTrends) {
/**
* Query history for numeric items
*/
queryNumericDataForItems(items, target, timeRange, useTrends, options) {
let [timeFrom, timeTo] = timeRange;
let getHistoryPromise;
options.consolidateBy = getConsolidateBy(target);
if (useTrends) {
if (this.enableDirectDBConnection) {
getHistoryPromise = this.zabbix.getTrendsDB(items, timeFrom, timeTo, options)
.then(history => this.zabbix.dbConnector.handleGrafanaTSResponse(history, items));
} else {
let valueType = this.getTrendValueType(target);
getHistoryPromise = this.zabbix.getTrend(items, timeFrom, timeTo)
.then(history => {
return responseHandler.handleTrends(history, items, valueType);
})
.then(history => responseHandler.handleTrends(history, items, valueType))
.then(timeseries => {
// Sort trend data, issue #202
_.forEach(timeseries, series => {
series.datapoints = _.sortBy(series.datapoints, point => point[c.DATAPOINT_TS]);
});
return timeseries;
});
} else {
// Use history
getHistoryPromise = this.zabbix.getHistory(items, timeFrom, timeTo)
.then(history => {
return responseHandler.handleHistory(history, items);
});
}
} else {
// Use history
if (this.enableDirectDBConnection) {
getHistoryPromise = this.zabbix.getHistoryDB(items, timeFrom, timeTo, options)
.then(history => this.zabbix.dbConnector.handleGrafanaTSResponse(history, items));
} else {
getHistoryPromise = this.zabbix.getHistory(items, timeFrom, timeTo)
.then(history => responseHandler.handleHistory(history, items));
}
}
return getHistoryPromise;
})
return getHistoryPromise
.then(timeseries => this.applyDataProcessingFunctions(timeseries, target))
.then(timeseries => downsampleSeries(timeseries, options))
.catch(error => {
@@ -238,6 +265,9 @@ class ZabbixAPIDatasource {
}
}
/**
* Query target data for Text mode
*/
queryTextData(target, timeRange) {
let [timeFrom, timeTo] = timeRange;
let options = {
@@ -256,6 +286,66 @@ class ZabbixAPIDatasource {
});
}
/**
* Query target data for Item ID mode
*/
queryItemIdData(target, timeRange, useTrends, options) {
let itemids = target.itemids;
itemids = this.templateSrv.replace(itemids, options.scopedVars, zabbixItemIdsTemplateFormat);
itemids = _.map(itemids.split(','), itemid => itemid.trim());
if (!itemids) {
return [];
}
return this.zabbix.getItemsByIDs(itemids)
.then(items => {
return this.queryNumericDataForItems(items, target, timeRange, useTrends, options);
});
}
/**
* Query target data for IT Services mode
*/
queryITServiceData(target, timeRange, options) {
// Don't show undefined and hidden targets
if (target.hide || (!target.itservice && !target.itServiceFilter) || !target.slaProperty) {
return [];
}
let itServiceIds = [];
let itServices = [];
let itServiceFilter;
let isOldVersion = target.itservice && !target.itServiceFilter;
if (isOldVersion) {
// Backward compatibility
itServiceFilter = '/.*/';
} else {
itServiceFilter = this.replaceTemplateVars(target.itServiceFilter, options.scopedVars);
}
return this.zabbix.getITServices(itServiceFilter)
.then(itservices => {
itServices = itservices;
if (isOldVersion) {
itServices = _.filter(itServices, {'serviceid': target.itservice.serviceid});
}
itServiceIds = _.map(itServices, 'serviceid');
return itServiceIds;
})
.then(serviceids => {
return this.zabbix.getSLA(serviceids, timeRange);
})
.then(slaResponse => {
return _.map(itServiceIds, serviceid => {
let itservice = _.find(itServices, {'serviceid': serviceid});
return responseHandler.handleSLAResponse(itservice, target.slaProperty, slaResponse);
});
});
}
/**
* Test connection to Zabbix API
* @return {object} Connection status and Zabbix API version
@@ -267,6 +357,13 @@ class ZabbixAPIDatasource {
zabbixVersion = version;
return this.zabbix.login();
})
.then(() => {
if (this.enableDirectDBConnection) {
return this.zabbix.dbConnector.testSQLDataSource();
} else {
return Promise.resolve();
}
})
.then(() => {
return {
status: "success",
@@ -281,6 +378,12 @@ class ZabbixAPIDatasource {
title: error.message,
message: error.data
};
} else if (error.data && error.data.message) {
return {
status: "error",
title: "Connection failed",
message: error.data.message
};
} else {
return {
status: "error",
@@ -367,13 +470,14 @@ class ZabbixAPIDatasource {
return getTriggers.then(triggers => {
// Filter triggers by description
if (utils.isRegex(annotation.trigger)) {
let triggerName = this.replaceTemplateVars(annotation.trigger, {});
if (utils.isRegex(triggerName)) {
triggers = _.filter(triggers, trigger => {
return utils.buildRegex(annotation.trigger).test(trigger.description);
return utils.buildRegex(triggerName).test(trigger.description);
});
} else if (annotation.trigger) {
} else if (triggerName) {
triggers = _.filter(triggers, trigger => {
return trigger.description === annotation.trigger;
return trigger.description === triggerName;
});
}
@@ -424,7 +528,9 @@ class ZabbixAPIDatasource {
*/
alertQuery(options) {
let enabled_targets = filterEnabledTargets(options.targets);
let getPanelItems = _.map(enabled_targets, target => {
let getPanelItems = _.map(enabled_targets, t => {
let target = _.cloneDeep(t);
this.replaceTargetVariables(target, options);
return this.zabbix.getItemsFromTarget(target, {itemtype: 'num'});
});
@@ -508,11 +614,24 @@ function bindFunctionDefs(functionDefs, category) {
});
}
function getConsolidateBy(target) {
let consolidateBy = 'avg';
let funcDef = _.find(target.functions, func => {
return func.def.name === 'consolidateBy';
});
if (funcDef && funcDef.params && funcDef.params.length) {
consolidateBy = funcDef.params[0];
}
return consolidateBy;
}
function downsampleSeries(timeseries_data, options) {
let defaultAgg = dataProcessor.aggregationFunctions['avg'];
let consolidateByFunc = dataProcessor.aggregationFunctions[options.consolidateBy] || defaultAgg;
return _.map(timeseries_data, timeseries => {
if (timeseries.datapoints.length > options.maxDataPoints) {
timeseries.datapoints = dataProcessor
.groupBy(options.interval, dataProcessor.AVERAGE, timeseries.datapoints);
.groupBy(options.interval, consolidateByFunc, timeseries.datapoints);
}
return timeseries;
});
@@ -544,6 +663,13 @@ function zabbixTemplateFormat(value) {
return '(' + escapedValues.join('|') + ')';
}
function zabbixItemIdsTemplateFormat(value) {
if (typeof value === 'string') {
return value;
}
return value.join(',');
}
/**
* If template variables are used in request, replace it using regex format
* and wrap with '/' for proper multi-value work. Example:

View File

@@ -8,7 +8,8 @@ var categories = {
Filter: [],
Trends: [],
Time: [],
Alias: []
Alias: [],
Special: []
};
function addFuncDef(funcDef) {
@@ -222,6 +223,16 @@ addFuncDef({
defaultParams: ['/(.*)/', '$1']
});
// Special
addFuncDef({
name: 'consolidateBy',
category: 'Special',
params: [
{ name: 'type', type: 'string', options: ['avg', 'min', 'max', 'sum', 'count'] }
],
defaultParams: ['avg'],
});
_.each(categories, function(funcList, catName) {
categories[catName] = _.sortBy(funcList, 'name');
});

View File

@@ -1,8 +1,6 @@
import {ZabbixAPIDatasource} from './datasource';
import {ZabbixQueryController} from './query.controller';
class ZabbixConfigController {}
ZabbixConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
import {ZabbixDSConfigController} from './config.controller';
class ZabbixQueryOptionsController {}
ZabbixQueryOptionsController.templateUrl = 'datasource-zabbix/partials/query.options.html';
@@ -12,7 +10,7 @@ ZabbixAnnotationsQueryController.templateUrl = 'datasource-zabbix/partials/annot
export {
ZabbixAPIDatasource as Datasource,
ZabbixConfigController as ConfigCtrl,
ZabbixDSConfigController as ConfigCtrl,
ZabbixQueryController as QueryCtrl,
ZabbixQueryOptionsController as QueryOptionsCtrl,
ZabbixAnnotationsQueryController as AnnotationsQueryCtrl

View File

@@ -76,24 +76,53 @@
</div>
<div class="gf-form-group">
<h3 class="page-heading">Alerting</h3>
<gf-form-switch class="gf-form" label-class="width-9"
label="Enable alerting"
checked="ctrl.current.jsonData.alerting">
<h3 class="page-heading">Direct DB Connection</h3>
<gf-form-switch class="gf-form" label-class="width-12"
label="Enable"
checked="ctrl.current.jsonData.dbConnection.enable">
</gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-9"
label="Add thresholds"
checked="ctrl.current.jsonData.addThresholds">
</gf-form-switch>
<div class="gf-form max-width-20">
<span class="gf-form-label width-9">Min severity</span>
<div ng-if="ctrl.current.jsonData.dbConnection.enable">
<div class="gf-form max-width-20">
<span class="gf-form-label width-12">
SQL Data Source
<info-popover mode="right-normal">
Select SQL Data Source for Zabbix database.
In order to use this feature you should <a href="/datasources/new" target="_blank">create</a> and
configure it first. Zabbix plugin uses this data source for querying history data directly from database.
This way usually faster than pulling data from Zabbix API, especially on the wide time ranges, and reduces
amount of data transfered.
</info-popover>
</span>
<div class="gf-form-select-wrapper max-width-16">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.alertingMinSeverity"
ng-options="s.val as s.text for s in [
{val: 0, text: 'Not classified'}, {val: 1, text:'Information'},
{val: 2, text: 'Warning'}, {val: 3, text: 'Average'},
{val: 4, text: 'High'}, {val: 5, text: 'Disaster'}]">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.dbConnection.datasourceId"
ng-options="ds.id as ds.name for ds in ctrl.sqlDataSources">
</select>
</div>
</div>
</div>
</div>
<div class="gf-form-group">
<h3 class="page-heading">Alerting</h3>
<gf-form-switch class="gf-form" label-class="width-12"
label="Enable alerting"
checked="ctrl.current.jsonData.alerting">
</gf-form-switch>
<div ng-if="ctrl.current.jsonData.alerting">
<gf-form-switch class="gf-form" label-class="width-12"
label="Add thresholds"
checked="ctrl.current.jsonData.addThresholds">
</gf-form-switch>
<div class="gf-form max-width-20">
<span class="gf-form-label width-12">Min severity</span>
<div class="gf-form-select-wrapper max-width-16">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.alertingMinSeverity"
ng-options="s.val as s.text for s in [
{val: 0, text: 'Not classified'}, {val: 1, text:'Information'},
{val: 2, text: 'Warning'}, {val: 3, text: 'Average'},
{val: 4, text: 'High'}, {val: 5, text: 'Disaster'}]">
</select>
</div>
</div>
</div>
</div>

View File

@@ -7,7 +7,7 @@
<select class="gf-form-input"
ng-change="ctrl.switchEditorMode(ctrl.target.mode)"
ng-model="ctrl.target.mode"
ng-options="v.mode as v.text for (k, v) in ctrl.editorModes">
ng-options="m.mode as m.text for m in ctrl.editorModes">
</select>
</div>
</div>
@@ -17,27 +17,29 @@
</div>
<!-- IT Service editor -->
<div class="gf-form-inline" ng-show="ctrl.target.mode == 1">
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.ITSERVICE">
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">IT Service</label>
<div class="gf-form-select-wrapper max-width-20">
<select class="gf-form-input"
ng-change="ctrl.selectITService()"
ng-model="ctrl.target.itservice"
bs-tooltip="ctrl.target.itservice.name.length > 25 ? ctrl.target.itservice.name : ''"
ng-options="itservice.name for itservice in ctrl.itserviceList track by itservice.name">
<option value="">-- Select IT service --</option>
</select>
</div>
<input type="text"
ng-model="ctrl.target.itServiceFilter"
bs-typeahead="ctrl.getITServices"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.itServiceFilter),
'zbx-regex': ctrl.isRegex(ctrl.target.itServiceFilter)
}">
</input>
</div>
<div class="gf-form">
<label class="gf-form-label query-keyword">IT service property</label>
<label class="gf-form-label query-keyword">Property</label>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-change="ctrl.selectITService()"
ng-model="ctrl.target.slaProperty"
ng-options="slaProperty.name for slaProperty in ctrl.slaPropertyList track by slaProperty.name">
<option value="">-- Property --</option>
ng-change="ctrl.onTargetBlur()"
ng-model="ctrl.target.slaProperty"
ng-options="slaProperty.name for slaProperty in ctrl.slaPropertyList track by slaProperty.name">
</select>
</div>
</div>
@@ -46,7 +48,7 @@
</div>
</div>
<div class="gf-form-inline" ng-hide="ctrl.target.mode == 1">
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.TEXT">
<!-- Select Group -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Group</label>
@@ -83,7 +85,7 @@
</div>
</div>
<div class="gf-form-inline" ng-hide="ctrl.target.mode == 1">
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.TEXT">
<!-- Select Application -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Application</label>
@@ -129,7 +131,7 @@
<!-- Query options -->
<div class="gf-form-group" ng-if="ctrl.showQueryOptions">
<div class="gf-form offset-width-7">
<gf-form-switch class="gf-form" ng-hide="ctrl.target.mode == 2"
<gf-form-switch class="gf-form"
label="Show disabled items"
checked="ctrl.target.options.showDisabledItems"
on-change="ctrl.onQueryOptionChange()">
@@ -137,8 +139,30 @@
</div>
</div>
<!-- Item IDs editor mode -->
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.ITEMID">
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Item IDs</label>
<input type="text"
ng-model="ctrl.target.itemids"
bs-typeahead="ctrl.getVariables"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="gf-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.itServiceFilter),
'zbx-regex': ctrl.isRegex(ctrl.target.itServiceFilter)
}">
</input>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
<!-- Metric processing functions -->
<div class="gf-form-inline" ng-hide="ctrl.target.mode">
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.METRICS || ctrl.target.mode == editorMode.ITEMID">
<div class="gf-form">
<label class="gf-form-label query-keyword width-7">Functions</label>
<div ng-repeat="func in ctrl.target.functions" class="gf-form-label query-part" metric-function-editor></div>
@@ -151,7 +175,7 @@
</div>
<!-- Text mode options -->
<div class="gf-form-inline" ng-show="ctrl.target.mode == 2">
<div class="gf-form-inline" ng-show="ctrl.target.mode == editorMode.TEXT">
<!-- Text metric regex -->
<div class="gf-form max-width-20">
<label class="gf-form-label query-keyword width-7">Text filter</label>
@@ -165,6 +189,8 @@
<gf-form-switch class="gf-form" label="Use capture groups" checked="ctrl.target.useCaptureGroups" on-change="ctrl.onTargetBlur()">
</gf-form-switch>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
</div>
</query-editor-row>

View File

@@ -1,5 +1,4 @@
import {QueryCtrl} from 'app/plugins/sdk';
import angular from 'angular';
import _ from 'lodash';
import * as c from './constants';
import * as utils from './utils';
@@ -22,17 +21,35 @@ export class ZabbixQueryController extends QueryCtrl {
this.replaceTemplateVars = this.datasource.replaceTemplateVars;
this.templateSrv = templateSrv;
this.editorModes = {
0: {value: 'num', text: 'Metrics', mode: c.MODE_METRICS},
1: {value: 'itservice', text: 'IT Services', mode: c.MODE_ITSERVICE},
2: {value: 'text', text: 'Text', mode: c.MODE_TEXT}
this.editorModes = [
{value: 'num', text: 'Metrics', mode: c.MODE_METRICS},
{value: 'text', text: 'Text', mode: c.MODE_TEXT},
{value: 'itservice', text: 'IT Services', mode: c.MODE_ITSERVICE},
{value: 'itemid', text: 'Item ID', mode: c.MODE_ITEMID}
];
this.$scope.editorMode = {
METRICS: c.MODE_METRICS,
TEXT: c.MODE_TEXT,
ITSERVICE: c.MODE_ITSERVICE,
ITEMID: c.MODE_ITEMID
};
this.slaPropertyList = [
{name: "Status", property: "status"},
{name: "SLA", property: "sla"},
{name: "OK time", property: "okTime"},
{name: "Problem time", property: "problemTime"},
{name: "Down time", property: "downtimeTime"}
];
// Map functions for bs-typeahead
this.getGroupNames = _.bind(this.getMetricNames, this, 'groupList');
this.getHostNames = _.bind(this.getMetricNames, this, 'hostList', true);
this.getApplicationNames = _.bind(this.getMetricNames, this, 'appList');
this.getItemNames = _.bind(this.getMetricNames, this, 'itemList');
this.getITServices = _.bind(this.getMetricNames, this, 'itServiceList');
this.getVariables = _.bind(this.getTemplateVariables, this);
// Update metric suggestion when template variable was changed
$rootScope.$on('template-variable-value-updated', () => this.onVariableChange());
@@ -57,14 +74,14 @@ export class ZabbixQueryController extends QueryCtrl {
// Load default values
var targetDefaults = {
mode: c.MODE_METRICS,
group: { filter: "" },
host: { filter: "" },
application: { filter: "" },
item: { filter: "" },
functions: [],
options: {
showDisabledItems: false
'mode': c.MODE_METRICS,
'group': { 'filter': "" },
'host': { 'filter': "" },
'application': { 'filter': "" },
'item': { 'filter': "" },
'functions': [],
'options': {
'showDisabledItems': false
}
};
_.defaults(target, targetDefaults);
@@ -77,26 +94,11 @@ export class ZabbixQueryController extends QueryCtrl {
if (target.mode === c.MODE_METRICS ||
target.mode === c.MODE_TEXT) {
this.downsampleFunctionList = [
{name: "avg", value: "avg"},
{name: "min", value: "min"},
{name: "max", value: "max"},
{name: "sum", value: "sum"},
{name: "count", value: "count"}
];
this.initFilters();
}
else if (target.mode === c.MODE_ITSERVICE) {
this.slaPropertyList = [
{name: "Status", property: "status"},
{name: "SLA", property: "sla"},
{name: "OK time", property: "okTime"},
{name: "Problem time", property: "problemTime"},
{name: "Down time", property: "downtimeTime"}
];
this.itserviceList = [{name: "test"}];
this.updateITServiceList();
_.defaults(target, {slaProperty: {name: "SLA", property: "sla"}});
this.suggestITServices();
}
};
@@ -104,7 +106,8 @@ export class ZabbixQueryController extends QueryCtrl {
}
initFilters() {
let itemtype = this.editorModes[this.target.mode].value;
let itemtype = _.find(this.editorModes, {'mode': this.target.mode});
itemtype = itemtype ? itemtype.value : null;
return Promise.all([
this.suggestGroups(),
this.suggestHosts(),
@@ -129,6 +132,12 @@ export class ZabbixQueryController extends QueryCtrl {
return metrics;
}
getTemplateVariables() {
return _.map(this.templateSrv.variables, variable => {
return '$' + variable.name;
});
}
suggestGroups() {
return this.zabbix.getAllGroups()
.then(groups => {
@@ -173,6 +182,14 @@ export class ZabbixQueryController extends QueryCtrl {
});
}
suggestITServices() {
return this.zabbix.getITService()
.then(itservices => {
this.metric.itServiceList = itservices;
return itservices;
});
}
isRegex(str) {
return utils.isRegex(str);
}
@@ -292,30 +309,7 @@ export class ZabbixQueryController extends QueryCtrl {
switchEditorMode(mode) {
this.target.mode = mode;
this.init();
}
/////////////////
// IT Services //
/////////////////
/**
* Update list of IT services
*/
updateITServiceList() {
this.zabbix.getITService().then((iteservices) => {
this.itserviceList = [];
this.itserviceList = this.itserviceList.concat(iteservices);
});
}
/**
* Call when IT service is selected.
*/
selectITService() {
if (!_.isEqual(this.oldTarget, this.target) && _.isEmpty(this.target.errors)) {
this.oldTarget = angular.copy(this.target);
this.panelCtrl.refresh();
}
this.targetChanged();
}
}

View File

@@ -15,7 +15,10 @@ describe('ZabbixDatasource', () => {
password: 'zabbix',
trends: true,
trendsFrom: '14d',
trendsRange: '7d'
trendsRange: '7d',
dbConnection: {
enabled: false
}
}
};
ctx.templateSrv = {};

View File

@@ -88,4 +88,54 @@ describe('Utils', () => {
done();
});
});
describe('splitTemplateQuery()', () => {
// Backward compatibility
it('should properly split query in old format', (done) => {
let test_cases = [
{
query: `/alu/./tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9{2}/`,
expected: ['/alu/', '/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9{2}/']
},
{
query: `a.b.c.d`,
expected: ['a', 'b', 'c', 'd']
}
];
_.each(test_cases, test_case => {
let splitQuery = utils.splitTemplateQuery(test_case.query);
expect(splitQuery).to.eql(test_case.expected);
});
done();
});
it('should properly split query', (done) => {
let test_cases = [
{
query: `{alu}{/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9]*/}`,
expected: ['alu', '/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9]*/']
},
{
query: `{alu}{/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9]{2}/}`,
expected: ['alu', '/tw-(nyc|que|brx|dwt|brk)-sta_(\w|\d)*-alu-[0-9]{2}/']
},
{
query: `{a}{b}{c}{d}`,
expected: ['a', 'b', 'c', 'd']
},
{
query: `{a}{b.c.d}`,
expected: ['a', 'b.c.d']
}
];
_.each(test_cases, test_case => {
let splitQuery = utils.splitTemplateQuery(test_case.query);
expect(splitQuery).to.eql(test_case.expected);
});
done();
});
});
});

View File

@@ -22,6 +22,15 @@ export function expandItemName(name, key) {
return name;
}
export function expandItems(items) {
_.forEach(items, item => {
item.item = item.name;
item.name = expandItemName(item.item, item.key_);
return item;
});
return items;
}
function splitKeyParams(paramStr) {
let params = [];
let quoted = false;
@@ -93,7 +102,7 @@ function escapeMacro(macro) {
* {group}{host.com} -> [group, host.com]
*/
export function splitTemplateQuery(query) {
let splitPattern = /{[^{}]*}/g;
let splitPattern = /\{[^\{\}]*\}|\{\/.*\/\}/g;
let split;
if (isContainsBraces(query)) {
@@ -109,7 +118,8 @@ export function splitTemplateQuery(query) {
}
function isContainsBraces(query) {
return query.includes('{') && query.includes('}');
let bracesPattern = /^\{.+\}$/;
return bracesPattern.test(query);
}
// Pattern for testing regex

View File

@@ -3,30 +3,45 @@ import _ from 'lodash';
import * as utils from './utils';
import './zabbixAPI.service.js';
import './zabbixCachingProxy.service.js';
import './zabbixDBConnector';
// Use factory() instead service() for multiple data sources support.
// Each Zabbix data source instance should initialize its own API instance.
/** @ngInject */
function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) {
function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy, ZabbixDBConnector) {
class Zabbix {
constructor(url, username, password, basicAuth, withCredentials, cacheTTL) {
constructor(url, options) {
let {
username, password, basicAuth, withCredentials, cacheTTL,
enableDirectDBConnection, sqlDatasourceId
} = options;
// Initialize Zabbix API
var ZabbixAPI = zabbixAPIService;
this.zabbixAPI = new ZabbixAPI(url, username, password, basicAuth, withCredentials);
if (enableDirectDBConnection) {
this.dbConnector = new ZabbixDBConnector(sqlDatasourceId);
}
// Initialize caching proxy for requests
let cacheOptions = {
enabled: true,
ttl: cacheTTL
};
this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, cacheOptions);
this.cachingProxy = new ZabbixCachingProxy(this.zabbixAPI, this.dbConnector, cacheOptions);
// Proxy methods
this.getHistory = this.cachingProxy.getHistory.bind(this.cachingProxy);
this.getMacros = this.cachingProxy.getMacros.bind(this.cachingProxy);
this.getItemsByIDs = this.cachingProxy.getItemsByIDs.bind(this.cachingProxy);
if (enableDirectDBConnection) {
this.getHistoryDB = this.cachingProxy.getHistoryDB.bind(this.cachingProxy);
this.getTrendsDB = this.cachingProxy.getTrendsDB.bind(this.cachingProxy);
}
this.getTrend = this.zabbixAPI.getTrend.bind(this.zabbixAPI);
this.getEvents = this.zabbixAPI.getEvents.bind(this.zabbixAPI);
@@ -134,6 +149,11 @@ function ZabbixFactory(zabbixAPIService, ZabbixCachingProxy) {
.then(items => filterByQuery(items, itemFilter));
}
getITServices(itServiceFilter) {
return this.cachingProxy.getITServices()
.then(itServices => findByFilter(itServices, itServiceFilter));
}
/**
* Build query - convert target filters to array of Zabbix items
*/

View File

@@ -165,10 +165,7 @@ function ZabbixAPIServiceFactory(alertSrv, zabbixAPICoreService) {
sortfield: 'name',
webitems: true,
filter: {},
selectHosts: [
'hostid',
'name'
]
selectHosts: ['hostid', 'name']
};
if (hostids) {
params.hostids = hostids;
@@ -186,16 +183,25 @@ function ZabbixAPIServiceFactory(alertSrv, zabbixAPICoreService) {
}
return this.request('item.get', params)
.then(expandItems);
.then(utils.expandItems);
}
function expandItems(items) {
_.forEach(items, item => {
item.item = item.name;
item.name = utils.expandItemName(item.item, item.key_);
return item;
});
return items;
}
getItemsByIDs(itemids) {
var params = {
itemids: itemids,
output: [
'name', 'key_',
'value_type',
'hostid',
'status',
'state'
],
webitems: true,
selectHosts: ['hostid', 'name']
};
return this.request('item.get', params)
.then(utils.expandItems);
}
getMacros(hostids) {

View File

@@ -8,8 +8,9 @@ import _ from 'lodash';
function ZabbixCachingProxyFactory() {
class ZabbixCachingProxy {
constructor(zabbixAPI, cacheOptions) {
constructor(zabbixAPI, zabbixDBConnector, cacheOptions) {
this.zabbixAPI = zabbixAPI;
this.dbConnector = zabbixDBConnector;
this.cacheEnabled = cacheOptions.enabled;
this.ttl = cacheOptions.ttl || 600000; // 10 minutes by default
@@ -22,7 +23,8 @@ function ZabbixCachingProxyFactory() {
history: {},
trends: {},
macros: {},
globalMacros: {}
globalMacros: {},
itServices: {}
};
this.historyPromises = {};
@@ -31,6 +33,13 @@ function ZabbixCachingProxyFactory() {
this.getHistory = callAPIRequestOnce(_.bind(this.zabbixAPI.getHistory, this.zabbixAPI),
this.historyPromises, getHistoryRequestHash);
if (this.dbConnector) {
this.getHistoryDB = callAPIRequestOnce(_.bind(this.dbConnector.getHistory, this.dbConnector),
this.historyPromises, getDBQueryHash);
this.getTrendsDB = callAPIRequestOnce(_.bind(this.dbConnector.getTrends, this.dbConnector),
this.historyPromises, getDBQueryHash);
}
// Don't run duplicated requests
this.groupPromises = {};
this.getGroupsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getGroups, this.zabbixAPI),
@@ -48,6 +57,14 @@ function ZabbixCachingProxyFactory() {
this.getItemsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getItems, this.zabbixAPI),
this.itemPromises, getRequestHash);
this.itemByIdPromises = {};
this.getItemsByIdOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getItemsByIDs, this.zabbixAPI),
this.itemPromises, getRequestHash);
this.itServicesPromises = {};
this.getITServicesOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getITService, this.zabbixAPI),
this.itServicesPromises, getRequestHash);
this.macroPromises = {};
this.getMacrosOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getMacros, this.zabbixAPI),
this.macroPromises, getRequestHash);
@@ -103,6 +120,15 @@ function ZabbixCachingProxyFactory() {
return this.proxyRequest(this.getItemsOnce, params, this.cache.items);
}
getItemsByIDs(itemids) {
let params = [itemids];
return this.proxyRequest(this.getItemsByIdOnce, params, this.cache.items);
}
getITServices() {
return this.proxyRequest(this.getITServicesOnce, [], this.cache.itServices);
}
getMacros(hostids) {
// Merge global macros and host macros
let promises = [
@@ -195,6 +221,14 @@ function getHistoryRequestHash(args) {
return stamp.getHash();
}
function getDBQueryHash(args) {
let itemids = _.map(args[0], 'itemid');
let consolidateBy = args[3].consolidateBy;
let intervalMs = args[3].intervalMs;
let stamp = itemids.join() + args[1] + args[2] + consolidateBy + intervalMs;
return stamp.getHash();
}
String.prototype.getHash = function() {
var hash = 0, i, chr, len;
if (this.length !== 0) {

View File

@@ -0,0 +1,192 @@
import angular from 'angular';
import _ from 'lodash';
const DEFAULT_QUERY_LIMIT = 10000;
const HISTORY_TO_TABLE_MAP = {
'0': 'history',
'1': 'history_str',
'2': 'history_log',
'3': 'history_uint',
'4': 'history_text'
};
const TREND_TO_TABLE_MAP = {
'0': 'trends',
'3': 'trends_uint'
};
const consolidateByFunc = {
'avg': 'AVG',
'min': 'MIN',
'max': 'MAX',
'sum': 'SUM',
'count': 'COUNT'
};
const consolidateByTrendColumns = {
'avg': 'value_avg',
'min': 'value_min',
'max': 'value_max'
};
/** @ngInject */
function ZabbixDBConnectorFactory(datasourceSrv, backendSrv) {
class ZabbixDBConnector {
constructor(sqlDataSourceId, options = {}) {
let {limit} = options;
this.sqlDataSourceId = sqlDataSourceId;
this.limit = limit || DEFAULT_QUERY_LIMIT;
}
/**
* Try to load DS with given id to check it's exist.
* @param {*} datasourceId ID of SQL data source
*/
loadSQLDataSource(datasourceId) {
let ds = _.find(datasourceSrv.getAll(), {'id': datasourceId});
if (ds) {
return datasourceSrv.loadDatasource(ds.name)
.then(ds => {
console.log('SQL data source loaded', ds);
});
} else {
return Promise.reject(`SQL Data Source with ID ${datasourceId} not found`);
}
}
/**
* Try to invoke test query for one of Zabbix database tables.
*/
testSQLDataSource() {
let testQuery = `SELECT itemid AS metric, clock AS time_sec, value_avg AS value FROM trends_uint LIMIT 1`;
return this.invokeSQLQuery(testQuery);
}
getHistory(items, timeFrom, timeTill, options) {
let {intervalMs, consolidateBy} = options;
let intervalSec = Math.ceil(intervalMs / 1000);
consolidateBy = consolidateBy || 'avg';
let aggFunction = consolidateByFunc[consolidateBy];
// Group items by value type and perform request for each value type
let grouped_items = _.groupBy(items, 'value_type');
let promises = _.map(grouped_items, (items, value_type) => {
let itemids = _.map(items, 'itemid').join(', ');
let table = HISTORY_TO_TABLE_MAP[value_type];
let query = `
SELECT itemid AS metric, clock AS time_sec, ${aggFunction}(value) AS value
FROM ${table}
WHERE itemid IN (${itemids})
AND clock > ${timeFrom} AND clock < ${timeTill}
GROUP BY time_sec DIV ${intervalSec}, metric
`;
query = compactSQLQuery(query);
return this.invokeSQLQuery(query);
});
return Promise.all(promises).then(results => {
return _.flatten(results);
});
}
getTrends(items, timeFrom, timeTill, options) {
let {intervalMs, consolidateBy} = options;
let intervalSec = Math.ceil(intervalMs / 1000);
consolidateBy = consolidateBy || 'avg';
let aggFunction = consolidateByFunc[consolidateBy];
// Group items by value type and perform request for each value type
let grouped_items = _.groupBy(items, 'value_type');
let promises = _.map(grouped_items, (items, value_type) => {
let itemids = _.map(items, 'itemid').join(', ');
let table = TREND_TO_TABLE_MAP[value_type];
let valueColumn = _.includes(['avg', 'min', 'max'], consolidateBy) ? consolidateBy : 'avg';
valueColumn = consolidateByTrendColumns[valueColumn];
let query = `
SELECT itemid AS metric, clock AS time_sec, ${aggFunction}(${valueColumn}) AS value
FROM ${table}
WHERE itemid IN (${itemids})
AND clock > ${timeFrom} AND clock < ${timeTill}
GROUP BY time_sec DIV ${intervalSec}, metric
`;
query = compactSQLQuery(query);
return this.invokeSQLQuery(query);
});
return Promise.all(promises).then(results => {
return _.flatten(results);
});
}
handleGrafanaTSResponse(history, items, addHostName = true) {
return convertGrafanaTSResponse(history, items, addHostName);
}
invokeSQLQuery(query) {
let queryDef = {
refId: 'A',
format: 'time_series',
datasourceId: this.sqlDataSourceId,
rawSql: query,
maxDataPoints: this.limit
};
return backendSrv.datasourceRequest({
url: '/api/tsdb/query',
method: 'POST',
data: {
queries: [queryDef],
}
})
.then(response => {
let results = response.data.results;
if (results['A']) {
return results['A'].series;
} else {
return null;
}
});
}
}
return ZabbixDBConnector;
}
angular
.module('grafana.services')
.factory('ZabbixDBConnector', ZabbixDBConnectorFactory);
///////////////////////////////////////////////////////////////////////////////
function convertGrafanaTSResponse(time_series, items, addHostName) {
var hosts = _.uniqBy(_.flatten(_.map(items, 'hosts')), 'hostid'); //uniqBy is needed to deduplicate
let grafanaSeries = _.map(time_series, series => {
let itemid = series.name;
let datapoints = series.points;
var item = _.find(items, {'itemid': itemid});
var alias = item.name;
if (_.keys(hosts).length > 1 && addHostName) { //only when actual multi hosts selected
var host = _.find(hosts, {'hostid': item.hostid});
alias = host.name + ": " + alias;
}
return {
target: alias,
datapoints: datapoints
};
});
return _.sortBy(grafanaSeries, 'target');
}
function compactSQLQuery(query) {
return query.replace(/\s+/g, ' ');
}