Merge branch 'develop'

Conflicts:
	.gitignore
	README.md
This commit is contained in:
Alexander Zobnin
2015-05-16 20:31:07 +03:00
7 changed files with 868 additions and 4 deletions

View File

@@ -1,17 +1,18 @@
# grafana-zabbix # grafana-zabbix
#### Zabbix API datasource for Grafana dashboard #### Zabbix API datasource for Grafana dashboard
Display your Zabbix data directly in Grafana dashboards! Display your Zabbix data directly in Grafana dashboards!
Useful metric editor with host group and application filtering: Useful metric editor with host group and application filtering:
![grafana - zabbix datasource](https://cloud.githubusercontent.com/assets/4932851/7441162/4f6af788-f0e4-11e4-887b-34d987d00c40.png) ![alt tag](https://cloud.githubusercontent.com/assets/4932851/7454206/34bf9f8c-f27a-11e4-8e96-a73829f188c4.png)
![alt tag](https://cloud.githubusercontent.com/assets/4932851/7441162/4f6af788-f0e4-11e4-887b-34d987d00c40.png)
## Installation ## Installation
### Grafana 1.9.x ### Grafana 1.9.x
Download [latest release](https://github.com/alexanderzobnin/grafana-zabbix/releases/latest) and unpack `zabbix` directory into `<your grafana installation>/plugins/datasource/`. Then edit Grafana config.js: Download latest release and unpack into `<your grafana installation>/plugins/datasource/`. Then edit Grafana config.js:
* Add dependencies * Add dependencies
``` ```
@@ -36,4 +37,33 @@ Download [latest release](https://github.com/alexanderzobnin/grafana-zabbix/rele
``` ```
### Grafana 2.0.x ### Grafana 2.0.x
Now in development. Download source code and put `zabbix` directory into `<your grafana-2 installation>/public/app/plugins/datasource/`.
* Edit plugin.json (located in `zabbix` directory) and set your `username` and `password`
```
{
"pluginType": "datasource",
"name": "Zabbix",
"type": "zabbix",
"serviceName": "ZabbixAPIDatasource",
"module": "plugins/datasource/zabbix/datasource",
"partials": {
"config": "app/plugins/datasource/zabbix/partials/config.html",
"query": "app/plugins/datasource/zabbix/partials/query.editor.html",
"annotations": "app/plugins/datasource/zabbix/partials/annotations.editor.html"
},
"username": "guest",
"password": "",
"metrics": true,
"annotations": true
}
```
* Restart grafana server.
* Add zabbix datasource in Grafana's "Data Sources" menu (see [Data Sources docs](http://docs.grafana.org/datasources/graphite/) for more info) and setup your Zabbix API url.
* **Important!** Change `Access` to `direct`!

401
zabbix/datasource.js Normal file
View File

@@ -0,0 +1,401 @@
define([
'angular',
'lodash',
'kbn',
'./queryCtrl',
],
function (angular, _, kbn) {
'use strict';
var module = angular.module('grafana.services');
module.factory('ZabbixAPIDatasource', function($q, backendSrv, templateSrv) {
function ZabbixAPIDatasource(datasource) {
this.name = datasource.name;
this.type = 'zabbix';
this.url = datasource.url;
// TODO: fix passing username and password from config.html
this.username = datasource.meta.username;
this.password = datasource.meta.password;
// No datapoints limit by default
this.limitMetrics = datasource.limitMetrics || 0;
this.supportMetrics = true;
this.supportAnnotations = true;
// For testing
this.ds = datasource;
}
ZabbixAPIDatasource.prototype.query = function(options) {
// get from & to in seconds
var from = kbn.parseDate(options.range.from).getTime();
var to = kbn.parseDate(options.range.to).getTime();
// Need for find target alias
var targets = options.targets;
// Remove undefined and hidden targets
var displayedTargets = _.filter(targets, function (target) {
return (!target.hide && target.item);
});
if (displayedTargets.length) {
// Extract zabbix api item objects from targets
var target_items = _.map(displayedTargets, 'item');
} else {
// No valid targets, return the empty dataset
var d = $q.defer();
d.resolve({ data: [] });
return d.promise;
}
from = Math.ceil(from/1000);
to = Math.ceil(to/1000);
return this.performTimeSeriesQuery(target_items, from, to).then(function (response) {
/**
* Response should be in the format:
* data: [
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* },
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* },
* ]
*/
// Index returned datapoints by item/metric id
var indexed_result = _.groupBy(response, 'itemid');
// TODO: realize correct timeseries reduce
/*
// Reduce timeseries to the same size for stacking and tooltip work properly
var min_length = _.min(_.map(indexed_result, function (history) {
return history.length;
}));
_.each(indexed_result, function (item) {
item.splice(0, item.length - min_length);
});*/
// Sort result as the same as targets for display
// stacked timeseries in proper order
var sorted_history = _.sortBy(indexed_result, function (value, key, list) {
return _.indexOf(_.map(target_items, 'itemid'), key);
});
var series = _.map(sorted_history,
// Foreach itemid index: iterate over the data points and
// normalize to Grafana response format.
function (history, index) {
return {
// Lookup itemid:alias map
//target: targets[itemid].alias,
target: targets[index].alias,
datapoints: _.map(history, function (p) {
// Value must be a number for properly work
var value = Number(p.value);
// TODO: Correct time for proper stacking
//var clock = Math.round(Number(p.clock) / 60) * 60;
return [value, p.clock * 1000];
})
};
})
return $q.when({data: series});
});
};
///////////////////////////////////////////////////////////////////////
/// Query methods
///////////////////////////////////////////////////////////////////////
// Request data from Zabbix API
ZabbixAPIDatasource.prototype.performZabbixAPIRequest = function(request_data) {
var options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
url: this.url,
data: request_data
};
var performedQuery;
// Check authorization first
if (!this.auth) {
var self = this;
performedQuery = this.performZabbixAPILogin().then(function (response) {
self.auth = response;
options.data.auth = response;
return backendSrv.datasourceRequest(options);
});
} else {
performedQuery = backendSrv.datasourceRequest(options);
}
// Handle response
return performedQuery.then(function (response) {
if (!response.data) {
return [];
}
return response.data.result;
});
};
/**
* Perform time series query to Zabbix API
*
* @param items: array of zabbix api item objects
*/
ZabbixAPIDatasource.prototype.performTimeSeriesQuery = function(items, start, end) {
// Group items by value type for separate requests
var items_by_value_type = _.groupBy(items, 'value_type');
var self = this;
var apiRequests = [];
// Prepare requests for each value type
_.each(items_by_value_type, function (value, key, list) {
var item_ids = _.map(value, 'itemid');
var history_type = key;
var data = {
jsonrpc: '2.0',
method: 'history.get',
params: {
output: 'extend',
history: history_type,
itemids: item_ids,
sortfield: 'clock',
sortorder: 'ASC',
limit: self.limitmetrics,
time_from: start,
},
auth: self.auth,
id: 1
};
// Relative queries (e.g. last hour) don't include an end time
if (end) {
data.params.time_till = end;
}
apiRequests.push(self.performZabbixAPIRequest(data));
});
return this.handleMultipleRequest(apiRequests);
};
// Handle multiple request
ZabbixAPIDatasource.prototype.handleMultipleRequest = function(apiRequests) {
var history = [];
var performedQuery = null;
// Build chain of api requests and put all history data into single array
_.each(apiRequests, function (apiRequest) {
if(!performedQuery) {
performedQuery = apiRequest.then(function (response) {
history = history.concat(response);
return history;
});
} else {
performedQuery = performedQuery.then(function () {
return apiRequest.then(function (response) {
history = history.concat(response);
return history;
});
});
}
});
return performedQuery;
};
// Get authentication token
ZabbixAPIDatasource.prototype.performZabbixAPILogin = function() {
var options = {
url : this.url,
method : 'POST',
data: {
jsonrpc: '2.0',
method: 'user.login',
params: {
user: this.username,
password: this.password
},
auth: null,
id: 1
},
};
return backendSrv.datasourceRequest(options).then(function (result) {
if (!result.data) {
return null;
}
return result.data.result;
});
};
// Get the list of host groups
ZabbixAPIDatasource.prototype.performHostGroupSuggestQuery = function() {
var data = {
jsonrpc: '2.0',
method: 'hostgroup.get',
params: {
output: ['name'],
real_hosts: true, //Return only host groups that contain hosts
sortfield: 'name'
},
auth: this.auth,
id: 1
};
return this.performZabbixAPIRequest(data);
};
// Get the list of hosts
ZabbixAPIDatasource.prototype.performHostSuggestQuery = function(groupid) {
var data = {
jsonrpc: '2.0',
method: 'host.get',
params: {
output: ['name'],
sortfield: 'name'
},
auth: this.auth,
id: 1
};
if (groupid) {
data.params.groupids = groupid;
}
return this.performZabbixAPIRequest(data);
};
// Get the list of applications
ZabbixAPIDatasource.prototype.performAppSuggestQuery = function(hostid) {
var data = {
jsonrpc: '2.0',
method: 'application.get',
params: {
output: ['name'],
sortfield: 'name',
hostids: hostid
},
auth: this.auth,
id: 1
};
return this.performZabbixAPIRequest(data);
};
// Get the list of host items
ZabbixAPIDatasource.prototype.performItemSuggestQuery = function(hostid, applicationid) {
var data = {
jsonrpc: '2.0',
method: 'item.get',
params: {
output: ['name', 'key_', 'value_type', 'delay'],
sortfield: 'name',
hostids: hostid,
webitems: true, //Include web items in the result
filter: {
value_type: [0,3]
}
},
auth: this.auth,
id: 1
};
// If application selected return only relative items
if (applicationid) {
data.params.applicationids = applicationid;
}
return this.performZabbixAPIRequest(data);
};
ZabbixAPIDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
var from = kbn.parseDate(rangeUnparsed.from).getTime();
var to = kbn.parseDate(rangeUnparsed.to).getTime();
var self = this;
from = Math.ceil(from/1000);
to = Math.ceil(to/1000);
var tid_options = {
method: 'POST',
url: self.url + '',
data: {
jsonrpc: '2.0',
method: 'trigger.get',
params: {
output: ['triggerid', 'description'],
itemids: annotation.aids.split(','), // TODO: validate / pull automatically from dashboard.
limit: self.limitmetrics,
},
auth: self.auth,
id: 1
},
};
return backendSrv.datasourceRequest(tid_options).then(function(result) {
var obs = {};
obs = _.indexBy(result.data.result, 'triggerid');
var options = {
method: 'POST',
url: self.url + '',
data: {
jsonrpc: '2.0',
method: 'event.get',
params: {
output: 'extend',
sortorder: 'DESC',
time_from: from,
time_till: to,
objectids: _.keys(obs),
limit: self.limitmetrics,
},
auth: self.auth,
id: 1
},
};
return backendSrv.datasourceRequest(options).then(function(result2) {
var list = [];
_.each(result2.data.result, function(e) {
list.push({
annotation: annotation,
time: e.clock * 1000,
title: obs[e.objectid].description,
text: e.eventid,
});
});
return list;
});
});
};
return ZabbixAPIDatasource;
});
});

View File

@@ -0,0 +1,8 @@
<div class="editor-row">
<div class="section">
<h5>Item ids <tip>Example: 123, 45, 678</tip></h5>
<div class="editor-option">
<input type="text" class="span10" ng-model='currentAnnotation.aids' placeholder="###, ###, ##"></input>
</div>
</div>
</div>

View File

@@ -0,0 +1,25 @@
<div ng-include="httpConfigPartialSrc"></div>
<br>
<h5>Zabbix API details</h5>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
User
</li>
<li>
<input type="text" class="tight-form-input input-large" ng-model='current.zabbixUser' placeholder=""></input>
</li>
<li class="tight-form-item">
Password
</li>
<li>
<input type="password" class="tight-form-input input-large" ng-model='current.zabbixPassword' placeholder=""></input>
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@@ -0,0 +1,124 @@
<div class="editor-row">
<div ng-repeat="target in panel.targets"
class="tight-form-container"
ng-class="{'tight-form-disabled': target.hide}"
ng-controller="ZabbixAPIQueryCtrl"
ng-init="init()">
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle"
data-toggle="dropdown"
tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem"><a tabindex="1" ng-click="duplicate()">Duplicate</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index-1)">Move up</a></li>
<li role="menuitem"><a tabindex="1" ng-click="moveMetricQuery($index, $index+1)">Move down</a></li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="fa fa-remove"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{targetLetters[$index]}}
</li>
<li>
<a class="tight-form-item"
ng-click="target.hide = !target.hide; get_data();"
role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list" role="menu">
<!-- Alias -->
<li>
<input type="text"
class="tight-form-input input-large"
ng-model="target.alias"
spellcheck='false'
placeholder="alias"
ng-blur="targetBlur()">
</li>
<!-- Select Host Group -->
<li>
<select style="width: 10em"
class="tight-form-input input-small"
ng-change="selectHostGroup()"
ng-model="target.hostGroup"
bs-tooltip="target.hostGroup.name.length > 25 ? target.hostGroup.name : ''"
ng-options="hostgroup.name for hostgroup in metric.hostGroupList" >
<option value="">All</option>
</select>
<a bs-tooltip="target.errors.metric"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.metric">
<i class="fa fa-warning"></i>
</a>
</li>
<!-- Select Host -->
<li>
<select style="width: 15em"
class="tight-form-input input-medium"
ng-change="selectHost()"
ng-model="target.host"
bs-tooltip="target.host.name.length > 25 ? target.host.name : ''"
ng-options="host.name for host in metric.hostList" >
<option value="">-- select host --</option>
</select>
<a bs-tooltip="target.errors.metric"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.metric">
<i class="icon-warning-sign"></i>
</a>
</li>
<!-- Select Application -->
<li>
<select style="width: 12em"
class="tight-form-input input-medium"
ng-change="selectApplication()"
ng-model="target.application"
bs-tooltip="target.application.name.length > 15 ? target.application.name : ''"
ng-options="app.name for app in metric.applicationList" >
<option value="">All</option>
</select>
<a bs-tooltip="target.errors.metric"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.metric">
<i class="icon-warning-sign"></i>
</a>
</li>
<!-- Select Item -->
<li>
<select style="width: 25em"
class="tight-form-input input-medium"
ng-change="selectItem()"
ng-model="target.item"
bs-tooltip="target.expandedName.length > 30 ? target.expandedName : ''"
ng-options="item.expandedName for item in metric.itemList" >
<option value="">--select item--</option>
</select>
<a bs-tooltip="target.errors.metric"
style="color: rgb(229, 189, 28)"
ng-show="target.errors.metric">
<i class="icon-warning-sign"></i>
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

21
zabbix/plugin.json Normal file
View File

@@ -0,0 +1,21 @@
{
"pluginType": "datasource",
"name": "Zabbix",
"type": "zabbix",
"serviceName": "ZabbixAPIDatasource",
"module": "plugins/datasource/zabbix/datasource",
"partials": {
"config": "app/plugins/datasource/zabbix/partials/config.html",
"query": "app/plugins/datasource/zabbix/partials/query.editor.html",
"annotations": "app/plugins/datasource/zabbix/partials/annotations.editor.html"
},
"username": "guest",
"password": "",
"metrics": true,
"annotations": true
}

255
zabbix/queryCtrl.js Normal file
View File

@@ -0,0 +1,255 @@
define([
'angular',
'lodash'
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
var targetLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
module.controller('ZabbixAPIQueryCtrl', function($scope) {
$scope.init = function() {
$scope.targetLetters = targetLetters;
$scope.metric = {
hostGroupList: ["Loading..."],
hostList: ["Loading..."],
applicationList: ["Loading..."],
itemList: ["Loading..."]
};
// Update host group, host, application and item lists
$scope.updateHostGroupList();
if ($scope.target.hostGroup) {
$scope.updateHostList($scope.target.hostGroup.groupid);
} else {
$scope.updateHostList();
}
if ($scope.target.host) {
$scope.updateAppList($scope.target.host.hostid);
if ($scope.target.application) {
$scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid);
} else {
$scope.updateItemList($scope.target.host.hostid, null);
}
}
setItemAlias();
$scope.target.errors = validateTarget($scope.target);
};
// Take alias from item name by default
function setItemAlias() {
if (!$scope.target.alias && $scope.target.item) {
$scope.target.alias = $scope.target.item.expandedName;
}
};
$scope.targetBlur = function() {
setItemAlias();
$scope.target.errors = validateTarget($scope.target);
if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
$scope.oldTarget = angular.copy($scope.target);
$scope.get_data();
}
};
// Call when host group selected
$scope.selectHostGroup = function() {
// Update host list
if ($scope.target.hostGroup) {
$scope.updateHostList($scope.target.hostGroup.groupid);
} else {
$scope.updateHostList('');
}
$scope.target.errors = validateTarget($scope.target);
if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
$scope.oldTarget = angular.copy($scope.target);
$scope.get_data();
}
};
// Call when host selected
$scope.selectHost = function() {
if ($scope.target.host) {
// Update item list
if ($scope.target.application) {
$scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid);
} else {
$scope.updateItemList($scope.target.host.hostid, null);
}
// Update application list
$scope.updateAppList($scope.target.host.hostid);
}
$scope.target.errors = validateTarget($scope.target);
if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
$scope.oldTarget = angular.copy($scope.target);
$scope.get_data();
}
};
// Call when application selected
$scope.selectApplication = function() {
// Update item list
if ($scope.target.application) {
$scope.updateItemList($scope.target.host.hostid, $scope.target.application.applicationid);
} else {
$scope.updateItemList($scope.target.host.hostid, null);
}
$scope.target.errors = validateTarget($scope.target);
if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
$scope.oldTarget = angular.copy($scope.target);
$scope.get_data();
}
};
// Call when item selected
$scope.selectItem = function() {
setItemAlias();
$scope.target.errors = validateTarget($scope.target);
if (!_.isEqual($scope.oldTarget, $scope.target) && _.isEmpty($scope.target.errors)) {
$scope.oldTarget = angular.copy($scope.target);
$scope.get_data();
}
};
$scope.duplicate = function() {
var clone = angular.copy($scope.target);
$scope.panel.targets.push(clone);
};
$scope.moveMetricQuery = function(fromIndex, toIndex) {
_.move($scope.panel.targets, fromIndex, toIndex);
};
//////////////////////////////
// SUGGESTION QUERIES
//////////////////////////////
/**
* Update list of host groups
*/
$scope.updateHostGroupList = function() {
$scope.datasource.performHostGroupSuggestQuery().then(function (series) {
$scope.metric.hostGroupList = series;
if ($scope.target.hostGroup) {
$scope.target.hostGroup = $scope.metric.hostGroupList.filter(function (item, index, array) {
// Find selected host in metric.hostList
return (item.groupid == $scope.target.hostGroup.groupid);
}).pop();
}
});
};
/**
* Update list of hosts
*/
$scope.updateHostList = function(groupid) {
$scope.datasource.performHostSuggestQuery(groupid).then(function (series) {
$scope.metric.hostList = series;
if ($scope.target.host) {
$scope.target.host = $scope.metric.hostList.filter(function (item, index, array) {
// Find selected host in metric.hostList
return (item.hostid == $scope.target.host.hostid);
}).pop();
}
});
};
/**
* Update list of host applications
*/
$scope.updateAppList = function(hostid) {
$scope.datasource.performAppSuggestQuery(hostid).then(function (series) {
$scope.metric.applicationList = series;
if ($scope.target.application) {
$scope.target.application = $scope.metric.applicationList.filter(function (item, index, array) {
// Find selected application in metric.hostList
return (item.applicationid == $scope.target.application.applicationid);
}).pop();
}
});
};
/**
* Update list of items
*/
$scope.updateItemList = function(hostid, applicationid) {
// Update only if host selected
if (hostid) {
$scope.datasource.performItemSuggestQuery(hostid, applicationid).then(function (series) {
$scope.metric.itemList = series;
// Expand item parameters
$scope.metric.itemList.forEach(function (item, index, array) {
if (item && item.key_ && item.name) {
item.expandedName = expandItemName(item);
}
});
if ($scope.target.item) {
$scope.target.item = $scope.metric.itemList.filter(function (item, index, array) {
// Find selected item in metric.hostList
return (item.itemid == $scope.target.item.itemid);
}).pop();
}
});
} else {
$scope.metric.itemList = [];
}
};
/**
* Expand item parameters, for example:
* CPU $2 time ($3) --> CPU system time (avg1)
*
* @param item: zabbix api item object
* @return: expanded item name (string)
*/
function expandItemName(item) {
var name = item.name;
var key = item.key_;
// extract params from key:
// "system.cpu.util[,system,avg1]" --> ["", "system", "avg1"]
var key_params = key.substring(key.indexOf('[') + 1, key.lastIndexOf(']')).split(',');
// replace item parameters
for (var i = key_params.length; i >= 1; i--) {
name = name.replace('$' + i, key_params[i - 1]);
};
return name;
};
//////////////////////////////
// VALIDATION
//////////////////////////////
function validateTarget(target) {
var errs = {};
return errs;
}
});
});