Merge branch 'master' into metric-functions
This commit is contained in:
28
src/datasource-zabbix/config.controller.js
Normal file
28
src/datasource-zabbix/config.controller.js
Normal 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';
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,10 @@ describe('ZabbixDatasource', () => {
|
||||
password: 'zabbix',
|
||||
trends: true,
|
||||
trendsFrom: '14d',
|
||||
trendsRange: '7d'
|
||||
trendsRange: '7d',
|
||||
dbConnection: {
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
};
|
||||
ctx.templateSrv = {};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
192
src/datasource-zabbix/zabbixDBConnector.js
Normal file
192
src/datasource-zabbix/zabbixDBConnector.js
Normal 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, ' ');
|
||||
}
|
||||
Reference in New Issue
Block a user