Merge branch 'develop'

This commit is contained in:
Alexander Zobnin
2016-04-01 00:05:48 +03:00
63 changed files with 5074 additions and 1888 deletions

24
.gitignore vendored
View File

@@ -6,6 +6,24 @@
*.bat
# Grafana linter config
.jshintrc
.jscs.json
.jsfmtrc
# .jshintrc
# .jscs.json
# .jsfmtrc
# Builded docs
docs/site/
node_modules
npm-debug.log
coverage/
.aws-config.json
awsconfig
/emails/dist
/public_gen
/tmp
vendor/phantomjs/phantomjs
dist/
# locally required config files
public/css/*.min.css

13
.jscs.json Normal file
View File

@@ -0,0 +1,13 @@
{
"disallowImplicitTypeConversion": ["string"],
"disallowKeywords": ["with"],
"disallowMultipleLineBreaks": true,
"disallowMixedSpacesAndTabs": true,
"disallowTrailingWhitespace": true,
"requireSpacesInFunctionExpression": {
"beforeOpeningCurlyBrace": true
},
"disallowSpacesInsideArrayBrackets": true,
"disallowSpacesInsideParentheses": true,
"validateIndentation": 2
}

39
.jshintrc Normal file
View File

@@ -0,0 +1,39 @@
{
"browser": true,
"bitwise":false,
"curly": true,
"eqnull": true,
"strict": true,
"module": true,
"devel": true,
"eqeqeq": true,
"forin": false,
"immed": true,
"supernew": true,
"expr": true,
"indent": 2,
"latedef": false,
"newcap": true,
"noarg": true,
"noempty": true,
"undef": true,
"boss": true,
"trailing": true,
"laxbreak": true,
"laxcomma": true,
"sub": true,
"unused": true,
"maxdepth": 6,
"maxlen": 140,
"esnext": true,
"globals": {
"System": true,
"Promise": true,
"define": true,
"require": true,
"Chromath": false,
"setImmediate": true
}
}

1
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1 @@
# Contributing to Grafana-Zabbix

82
Gruntfile.js Normal file
View File

@@ -0,0 +1,82 @@
module.exports = function(grunt) {
require('load-grunt-tasks')(grunt);
grunt.loadNpmTasks('grunt-execute');
grunt.loadNpmTasks('grunt-contrib-clean');
grunt.initConfig({
clean: ["dist"],
copy: {
src_to_dist: {
cwd: 'src',
expand: true,
src: [
'**/*',
'!datasource-zabbix/*.js',
'!panel-triggers/*.js',
'!components/*.js',
'!module.js',
'!**/*.scss'
],
dest: 'dist/'
},
pluginDef: {
expand: true,
src: ['plugin.json', 'README.md'],
dest: 'dist/',
}
},
watch: {
rebuild_all: {
files: ['src/**/*', 'plugin.json'],
tasks: ['default'],
options: {spawn: false}
},
},
babel: {
options: {
sourceMap: true,
presets: ["es2015"],
plugins: ['transform-es2015-modules-systemjs', "transform-es2015-for-of"],
},
dist: {
files: [{
cwd: 'src',
expand: true,
src: [
'datasource-zabbix/*.js',
'panel-triggers/*.js',
'components/*.js',
'module.js',
],
dest: 'dist/'
}]
},
},
sass: {
options: {
sourceMap: true
},
dist: {
files: {
'dist/panel-triggers/css/panel_triggers.css' : 'src/panel-triggers/sass/panel_triggers.scss',
}
}
}
});
grunt.registerTask('default', [
'clean',
'copy:src_to_dist',
'copy:pluginDef',
'babel',
'sass'
]);
};

View File

@@ -1,46 +1,36 @@
# Grafana-Zabbix
# Zabbix plugin for Grafana
#### Zabbix datasource for Grafana dashboard
Zabbix datasource, Triggers panel and more.
[![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/alexanderzobnin/grafana-zabbix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
##### [Donate](https://www.paypal.me/alexanderzobnin)
##### See features overview and dashboards examples at Grafana-Zabbix [Live demo](http://play.grafana-zabbix.org) site.
##### Download [latest release](https://github.com/alexanderzobnin/grafana-zabbix/releases/latest)
Display your Zabbix data directly in [Grafana](http://grafana.org) dashboards!
### Meet grafana-zabbix 3.0
Download [grafana-zabbix 3.0 beta](https://github.com/alexanderzobnin/grafana-zabbix/releases/latest)
[Documentation](http://docs.grafana-zabbix.org)
Read [installation instruction](http://docs.grafana-zabbix.org/installation/) for version 3.0.
Display your Zabbix data with powerful [Grafana](http://grafana.org) dashboards!
![Dashboard](https://cloud.githubusercontent.com/assets/4932851/8269101/9e6ee67e-17a3-11e5-85de-fe9dcc2dd375.png)
#### [Documentation](https://github.com/alexanderzobnin/grafana-zabbix/wiki)
1. [**Overview**](https://github.com/alexanderzobnin/grafana-zabbix/wiki/Overview)
2. [**Installation**](https://github.com/alexanderzobnin/grafana-zabbix/wiki/Installation#grafana-21x-and-25x)
3. [**Users Guide**](https://github.com/alexanderzobnin/grafana-zabbix/wiki/Usage)
4. [**Troubleshooting**](https://github.com/alexanderzobnin/grafana-zabbix/wiki/Troubleshooting)
## Features
#### Flexible metric editor
* hosts and items filtering:
[![regex_filter](https://cloud.githubusercontent.com/assets/4932851/8312766/5eb34480-19e7-11e5-925f-452a99ec0ab6.gif)](https://cloud.githubusercontent.com/assets/4932851/8312766/5eb34480-19e7-11e5-925f-452a99ec0ab6.gif)
* Custom scale for each target:
![Scale](https://cloud.githubusercontent.com/assets/4932851/8269207/212549be-17a9-11e5-9e33-90deb90ddc13.png)
* Custom scale for each target
#### Templated dashboards support
Group, host, application or item names can be replaced with a template variable. This allows you to create generic dashboards that can quickly be changed to show stats for a specific cluster, server or application.
[![templated_dashboard](https://cloud.githubusercontent.com/assets/4932851/8312492/7f286c38-19e5-11e5-8c19-1b9e97292b06.gif)](https://cloud.githubusercontent.com/assets/4932851/8312492/7f286c38-19e5-11e5-8c19-1b9e97292b06.gif)
#### Annotations support
* Display zabbix events on graphs:
![Annotations](https://cloud.githubusercontent.com/assets/4932851/8269358/622ec3be-17ad-11e5-8023-eba137369cfe.png)
* Show acknowledges for problems:
![Acknowledges](https://cloud.githubusercontent.com/assets/4932851/8269375/e6d8706a-17ad-11e5-8e2d-2d707d8ee67f.png)
* Display zabbix events on graphs
* Show acknowledges for problems
### Dockerized Grafana with Zabbix datasource

1
docs/README.md Normal file
View File

@@ -0,0 +1 @@
# Grafana-Zabbix Documentation

25
docs/mkdocs.yml Normal file
View File

@@ -0,0 +1,25 @@
site_name: Grafana-Zabbix Documentation
#site_url: http://docs.grafana-zabbix.org/
#site_url: /
site_description: Documentation for Grafana-Zabbix, Zabbix monitoring system plugin bundle for Grafana
repo_url: https://github.com/alexanderzobnin/grafana-zabbix/
copyright: Copyright © 2014-2015, Alexander Zobnin
docs_dir: sources
theme: readthedocs
pages:
- Project:
- 'About Grafana-Zabbix': 'index.md'
- 'Feature Highlights': 'features.md'
- Installation:
- 'Installation': 'installation/index.md'
- 'Configuration': 'installation/configuration.md'
- 'Troubleshooting': 'installation/troubleshooting.md'
- User Guides:
- 'Getting Started': 'guides/gettingstarted.md'
- Reference:
- 'Zabbix Datasource': 'reference/datasource-zabbix.md'
- 'Triggers Panel': 'reference/panel-triggers.md'
- Tutorials:
- 'Building Host Dashboard': 'tutorials/host_dashboard.md'

11
docs/sources/features.md Normal file
View File

@@ -0,0 +1,11 @@
page_title: Feature Highlights
page_description: Grafana-Zabbix Feature Highlights.
# Feature Highlights
Grafana in couple with Grafana-Zabbix plugin allows to create great dashboards. There is some
features:
- Rich graphing with Grafana
- Template variables allow to create reusable dashboards

View File

@@ -0,0 +1,3 @@
# Getting Started with Grafana-Zabbix
After you [installed and configured](../installation/index.md) Grafana-Zabbix data source let's
create a simple dashboard.

3
docs/sources/img/.gitattributes vendored Normal file
View File

@@ -0,0 +1,3 @@
*.png filter=lfs diff=lfs merge=lfs -text
*.jpg filter=lfs diff=lfs merge=lfs -text
*.psd filter=lfs diff=lfs merge=lfs -text

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:62deb035e0d9241c2ae9825b101c86d329766cf421a5e2555449f71420cb9891
size 58575

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5275ed470657abc2c9c0d648e1a24c0f1b3a029206f8aa9594009b837f3ec2f1
size 50282

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:767ffe4d4385871847acb9c1b77b42f63144299810c5c97af2eee9057e19c6d7
size 12828

View File

@@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ee8a5b214d169eb70f8c591cb0e64910b280ce1a8abe1fbf8ee4de26af81a809
size 14287

38
docs/sources/index.md Normal file
View File

@@ -0,0 +1,38 @@
page_title: About Grafana-Zabbix
page_description: Introduction to Grafana-Zabbix plugin.
# About Grafana-Zabbix plugin
Grafana-Zabbix is a plugin for Grafana allowing to visualize monitoring data from Zabbix
and create dashboards for analyzing metrics and realtime monitoring. Main goals of this project
are extend Zabbix capabilities for monitoring data visualization and provide quick and powerful way
to create dashboards. It is possible due both Grafana and Grafana-Zabbix plugin features.
## Community Resources, Feedback, and Support
This project is being started as a simple plugin for Grafana. But many powerful features and
improvements come from community. So don't hesitate to give any feedback and together we will make
this tool better.
If you have any troubles with Grafana or you just want clarification on a feature, there are
a number of ways to get help:
- [Troubleshooting guide](/installation/troubleshooting/)
- Search closed and open [issues on GitHub](https://github.com/grafana/grafana/issues)
- [Gitter room](https://gitter.im/alexanderzobnin/grafana-zabbix)
- [Twitter](https://twitter.com/AlexanderZobnin)
Or you can just send me [email](mailto:alexanderzobnin@gmail.com).
## Support Project
I develop this project in my free time, but if you really find it helpful and promising, you can
support me. There are some ways to do this. You can [donate](https://www.paypal.me/alexanderzobnin)
any reasonable amount, or you can request a feature development, interesting for you (for example,
Triggers panel was sponsored by [Core IT Project](http://coreit.fr/)).
## License
By utilizing this software, you agree to the terms of the included license. Grafana-Zabbix plugin is
licensed under the Apache 2.0 agreement. See
[LICENSE](https://github.com/alexanderzobnin/grafana-zabbix/blob/master/LICENSE.md) for the full
license terms.

View File

@@ -0,0 +1,75 @@
# Configuration
To add new Zabbix data source open _Data Sources_ in side panel, click _Add new_ and select Zabbix
from dropdown list.
![Add zabbix data source](../img/installation-add_datasource.png)
Then configure a data source
![Configure zabbix data source](../img/installation-datasource_config.png)
### Http settings
- **Url**: set Zabbix API url (full path with `api_jsonrpc.php`).
- **Access**: can be either _proxy_ or _direct_.
- **Http Auth**: configure if you use proxy authentication.
- **Basic Auth**:
- **With Credentials**:
### Zabbiz API details
- **User** and **Password**: setup login for access to Zabbix API. Also check user's permissions
in Zabbix if you can't get any groups and hosts in Grafana.
- **Trends**: enable if you use patch for trends
support in Zabbix 2.x ([ZBXNEXT-1193](https://support.zabbix.com/browse/ZBXNEXT-1193)). This is
strictly recommended for displaying long time periods (more than few days, depending of your item's
updating interval in Zabbix) because few days of item history contains tons of points. Using trends
can increase Grafana performance.
- **Use trends from**: time after which trends will be used. Default is **7d** (7 days).
You can set the time in Grafana format. Valid time specificators are:
- **h** - hours
- **d** - days
- **M** - months
- **Metrics limit**: maximum items number which can be returned by one request. Helps to prevent
Grafana slowdown due wrong request.
Then click _Add_ - datasource will be added and you can check connection using _Test Connection_ button.
![Test Connection](../img/installation-test_connection.png)
This feature can help to find some mistakes like invalid user name or password, wrong api url.
![Test Connection - Error](../img/installation-test_connection_error.png)
## Note about Zabbix 2.2 or less
Zabbix API (api_jsonrpc.php) before zabbix 2.4 don't allow cross-domain requests (CORS). And you
can get HTTP error 412 (Precondition Failed).
To fix it add this code to api_jsonrpc.php immediately after the copyright:
```php
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Headers: Content-Type');
header('Access-Control-Allow-Methods: POST');
header('Access-Control-Max-Age: 1000');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
return;
}
```
before
```php
require_once dirname(__FILE__).'/include/func.inc.php';
require_once dirname(__FILE__).'/include/classes/core/CHttpRequest.php';
```
[Full fix listing](https://gist.github.com/alexanderzobnin/f2348f318d7a93466a0c).
For more details see zabbix issues [ZBXNEXT-1377](https://support.zabbix.com/browse/ZBXNEXT-1377)
and [ZBX-8459](https://support.zabbix.com/browse/ZBX-8459).
## Note about Browser Cache
After updating plugin, clear browser cache and reload application page. See details
for [Chrome](https://support.google.com/chrome/answer/95582),
[Firefox](https://support.mozilla.org/en-US/kb/how-clear-firefox-cache). You need to clear cache
only, not cookies, history and other data.

View File

@@ -0,0 +1,60 @@
page_title: Grafana-Zabbix Installation
page_description: Installation instructions for Grafana-Zabbix.
# Installation
## From release package
Download [latest release](https://github.com/alexanderzobnin/grafana-zabbix/releases/latest)
for relative Grafana version. Unpack archive and copy *grafana-zabbix* into your grafana
plugins directory (default `/var/lib/grafana/plugins` if your installing grafana with package).
Restart grafana-server and the plugin should be automatically detected and used.
## Building from sources
You need NodeJS, npm and Grunt for building plugin from sources. Read more about required versions
in [Grafana docs](http://docs.grafana.org/project/building_from_source/).
```sh
git clone https://github.com/alexanderzobnin/grafana-zabbix.git
cd grafana-zabbix
npm install
npm install -g grunt-cli
grunt
```
Plugin will built into *dist/* directory. Then you can copy it into your grafana plugins directory
or set path to compiled plugin in grafana config:
```ini
[plugin.zabbix]
path = /home/your/clone/dir/grafana-zabbix/dist
```
If you need to upgrade plugin use
```sh
git pull
grunt
```
Restart Grafana server
```sh
sudo service grafana-server restart
systemctl restart grafana-server
```
## Using grafana-cli tool
Get list of available plugins
```sh
grafana-cli plugins list-remote
```
Install zabbix plugin
```sh
grafana-cli plugins install zabbix-app
```
Read more in [Grafana docs](http://docs.grafana.org/plugins/installation/)

View File

@@ -0,0 +1,5 @@
# Troubleshooting
See [Grafana troubleshooting](http://docs.grafana.org/installation/troubleshooting/) for general
connection issues. If you have a problem with Zabbix datasource, you should open
a [support issue](https://github.com/alexanderzobnin/grafana-zabbix/issues). Before you do that
please search the existing closed or open issues.

View File

View File

38
package.json Normal file
View File

@@ -0,0 +1,38 @@
{
"name": "grafana-zabbix",
"private": false,
"version": "3.0.0",
"description": "",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "git+https://github.com/alexanderzobnin/grafana-zabbix.git"
},
"author": "Alexander Zobnin",
"license": "Apache-2.0",
"bugs": {
"url": "https://github.com/alexanderzobnin/grafana-zabbix/issues"
},
"devDependencies": {
"grunt": "~0.4.5",
"babel": "~6.5.1",
"grunt-babel": "~6.0.0",
"grunt-sass": "^1.1.0",
"grunt-contrib-copy": "~0.8.2",
"grunt-contrib-watch": "^0.6.1",
"grunt-contrib-uglify": "~0.11.0",
"grunt-systemjs-builder": "^0.2.5",
"load-grunt-tasks": "~3.2.0",
"grunt-execute": "~0.2.2",
"grunt-contrib-clean": "~0.6.0"
},
"dependencies": {
"babel-plugin-transform-es2015-modules-systemjs": "^6.5.0",
"babel-plugin-transform-es2015-for-of": "^6.5.0",
"babel-preset-es2015": "^6.5.0",
"lodash": "~4.0.0"
},
"homepage": "http://grafana-zabbix.org"
}

View File

@@ -0,0 +1,13 @@
<h3 class="page-heading">Zabbix App Config</h3>
<div class="gf-form-group">
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">json Data property</span>
<input type="text" class="gf-form-input" ng-model="ctrl.appModel.jsonData.customText" >
</div>
<div class="gf-form">
<editor-checkbox text="Custom value" model="ctrl.appModel.jsonData.customCheckbox"></editor-checkbox>
</div>
</div>
</div>

4
src/components/config.js Normal file
View File

@@ -0,0 +1,4 @@
export class ZabbixAppConfigCtrl {
constructor() { }
}
ZabbixAppConfigCtrl.templateUrl = 'components/config.html';

View File

@@ -0,0 +1,416 @@
{
"id": null,
"title": "Zabbix Server Dashboard",
"originalTitle": "Zabbix Server Dashboard",
"tags": [
"zabbix",
"example"
],
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"rows": [
{
"collapse": false,
"editable": true,
"height": "100px",
"panels": [
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"datasource": null,
"editable": true,
"error": false,
"format": "none",
"id": 3,
"interval": null,
"isNew": true,
"links": [],
"maxDataPoints": 100,
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"span": 4,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [
{
"application": {
"filter": "General"
},
"functions": [],
"group": {
"filter": "Linux servers"
},
"host": {
"filter": "Zabbix server"
},
"item": {
"filter": "Host name"
},
"mode": 2,
"refId": "A"
}
],
"thresholds": "",
"title": "Host name",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "avg"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"datasource": null,
"decimals": 0,
"editable": true,
"error": false,
"format": "s",
"id": 4,
"interval": null,
"isNew": true,
"links": [],
"maxDataPoints": "",
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"span": 4,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [
{
"application": {
"filter": "General"
},
"functions": [],
"group": {
"filter": "Zabbix servers"
},
"host": {
"filter": "Zabbix server"
},
"item": {
"filter": "System uptime"
},
"mode": 0,
"refId": "A"
}
],
"thresholds": "",
"title": "Uptime",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
},
{
"cacheTimeout": null,
"colorBackground": false,
"colorValue": false,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"datasource": null,
"editable": true,
"error": false,
"format": "none",
"id": 5,
"interval": null,
"isNew": true,
"links": [],
"maxDataPoints": "",
"nullPointMode": "connected",
"nullText": null,
"postfix": "",
"postfixFontSize": "50%",
"prefix": "",
"prefixFontSize": "50%",
"span": 4,
"sparkline": {
"fillColor": "rgba(31, 118, 189, 0.18)",
"full": false,
"lineColor": "rgb(31, 120, 193)",
"show": false
},
"targets": [
{
"application": {
"filter": "Zabbix server"
},
"functions": [],
"group": {
"filter": "Zabbix servers"
},
"host": {
"filter": "Zabbix server"
},
"item": {
"filter": "Required performance of Zabbix server, new values per second"
},
"mode": 0,
"refId": "A"
}
],
"thresholds": "",
"title": "Required performance, NVPS",
"type": "singlestat",
"valueFontSize": "80%",
"valueMaps": [
{
"op": "=",
"text": "N/A",
"value": "null"
}
],
"valueName": "current"
}
],
"title": "General"
},
{
"collapse": false,
"editable": true,
"height": "300px",
"panels": [
{
"aliasColors": {},
"bars": false,
"datasource": null,
"editable": true,
"error": false,
"fill": 1,
"grid": {
"threshold1": null,
"threshold1Color": "rgba(216, 200, 27, 0.27)",
"threshold2": null,
"threshold2Color": "rgba(234, 112, 112, 0.22)"
},
"id": 1,
"isNew": true,
"legend": {
"alignAsTable": true,
"avg": false,
"current": false,
"max": false,
"min": false,
"rightSide": true,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "connected",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 7,
"stack": true,
"steppedLine": false,
"targets": [
{
"application": {
"filter": "CPU"
},
"functions": [],
"group": {
"filter": "Zabbix servers"
},
"host": {
"filter": "Zabbix server"
},
"item": {
"filter": "/CPU (?!idle)/"
},
"mode": 0,
"refId": "A"
}
],
"timeFrom": null,
"timeShift": null,
"title": "CPU",
"tooltip": {
"msResolution": false,
"shared": true,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"show": true
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"columns": [
{
"text": "Current",
"value": "current"
},
{
"text": "Avg",
"value": "avg"
}
],
"editable": true,
"error": false,
"fontSize": "100%",
"id": 2,
"isNew": true,
"links": [],
"pageSize": null,
"scroll": true,
"showHeader": true,
"sort": {
"col": 0,
"desc": true
},
"span": 5,
"styles": [
{
"dateFormat": "YYYY-MM-DD HH:mm:ss",
"pattern": "Time",
"type": "date"
},
{
"colorMode": null,
"colors": [
"rgba(245, 54, 54, 0.9)",
"rgba(237, 129, 40, 0.89)",
"rgba(50, 172, 45, 0.97)"
],
"decimals": 2,
"pattern": "/.*/",
"thresholds": [],
"type": "number",
"unit": "short"
}
],
"targets": [
{
"application": {
"filter": "Zabbix server"
},
"functions": [],
"group": {
"filter": "Zabbix servers"
},
"host": {
"filter": "Zabbix server"
},
"item": {
"filter": "/Zabbix busy/"
},
"mode": 0,
"refId": "A"
}
],
"title": "Zabbix processes",
"transform": "timeseries_aggregations",
"type": "table"
}
],
"title": "Row"
}
],
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"schemaVersion": 12,
"version": 5,
"links": []
}

View File

@@ -0,0 +1,234 @@
import _ from 'lodash';
import * as utils from './utils';
export default class DataProcessor {
/**
* Downsample datapoints series
*/
static downsampleSeries(datapoints, time_to, ms_interval, func) {
var downsampledSeries = [];
var timeWindow = {
from: time_to * 1000 - ms_interval,
to: time_to * 1000
};
var points_sum = 0;
var points_num = 0;
var value_avg = 0;
var frame = [];
for (var i = datapoints.length - 1; i >= 0; i -= 1) {
if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) {
points_sum += datapoints[i][0];
points_num++;
frame.push(datapoints[i][0]);
}
else {
value_avg = points_num ? points_sum / points_num : 0;
if (func === "max") {
downsampledSeries.push([_.max(frame), timeWindow.to]);
}
else if (func === "min") {
downsampledSeries.push([_.min(frame), timeWindow.to]);
}
// avg by default
else {
downsampledSeries.push([value_avg, timeWindow.to]);
}
// Shift time window
timeWindow.to = timeWindow.from;
timeWindow.from -= ms_interval;
points_sum = 0;
points_num = 0;
frame = [];
// Process point again
i++;
}
}
return downsampledSeries.reverse();
}
/**
* Group points by given time interval
* datapoints: [[<value>, <unixtime>], ...]
*/
static groupBy(interval, groupByCallback, datapoints) {
var ms_interval = utils.parseInterval(interval);
// Calculate frame timestamps
var frames = _.groupBy(datapoints, function(point) {
// Calculate time for group of points
return Math.floor(point[1] / ms_interval) * ms_interval;
});
// frame: { '<unixtime>': [[<value>, <unixtime>], ...] }
// return [{ '<unixtime>': <value> }, { '<unixtime>': <value> }, ...]
var grouped = _.mapValues(frames, function(frame) {
var points = _.map(frame, function(point) {
return point[0];
});
return groupByCallback(points);
});
// Convert points to Grafana format
return sortByTime(_.map(grouped, function(value, timestamp) {
return [Number(value), Number(timestamp)];
}));
}
static sumSeries(timeseries) {
// Calculate new points for interpolation
var new_timestamps = _.uniq(_.map(_.flatten(timeseries, true), function(point) {
return point[1];
}));
new_timestamps = _.sortBy(new_timestamps);
var interpolated_timeseries = _.map(timeseries, function(series) {
var timestamps = _.map(series, function(point) {
return point[1];
});
var new_points = _.map(_.difference(new_timestamps, timestamps), function(timestamp) {
return [null, timestamp];
});
var new_series = series.concat(new_points);
return sortByTime(new_series);
});
_.each(interpolated_timeseries, interpolateSeries);
var new_timeseries = [];
var sum;
for (var i = new_timestamps.length - 1; i >= 0; i--) {
sum = 0;
for (var j = interpolated_timeseries.length - 1; j >= 0; j--) {
sum += interpolated_timeseries[j][i][0];
}
new_timeseries.push([sum, new_timestamps[i]]);
}
return sortByTime(new_timeseries);
}
static AVERAGE(values) {
var sum = 0;
_.each(values, function(value) {
sum += value;
});
return sum / values.length;
}
static MIN(values) {
return _.min(values);
}
static MAX(values) {
return _.max(values);
}
static MEDIAN(values) {
var sorted = _.sortBy(values);
return sorted[Math.floor(sorted.length / 2)];
}
static setAlias(alias, timeseries) {
timeseries.target = alias;
return timeseries;
}
static groupByWrapper(interval, groupFunc, datapoints) {
var groupByCallback = DataProcessor.aggregationFunctions[groupFunc];
return DataProcessor.groupBy(interval, groupByCallback, datapoints);
}
static aggregateWrapper(groupByCallback, interval, datapoints) {
var flattenedPoints = _.flatten(datapoints, true);
return DataProcessor.groupBy(interval, groupByCallback, flattenedPoints);
}
static get aggregationFunctions() {
return {
avg: this.AVERAGE,
min: this.MIN,
max: this.MAX,
median: this.MEDIAN
};
}
static get metricFunctions() {
return {
groupBy: this.groupByWrapper,
average: _.partial(this.aggregateWrapper, this.AVERAGE),
min: _.partial(this.aggregateWrapper, this.MIN),
max: _.partial(this.aggregateWrapper, this.MAX),
median: _.partial(this.aggregateWrapper, this.MEDIAN),
sumSeries: this.sumSeries,
setAlias: this.setAlias,
};
}
}
function sortByTime(series) {
return _.sortBy(series, function(point) {
return point[1];
});
}
/**
* Interpolate series with gaps
*/
function interpolateSeries(series) {
var left, right;
// Interpolate series
for (var i = series.length - 1; i >= 0; i--) {
if (!series[i][0]) {
left = findNearestLeft(series, series[i]);
right = findNearestRight(series, series[i]);
if (!left) {
left = right;
}
if (!right) {
right = left;
}
series[i][0] = linearInterpolation(series[i][1], left, right);
}
}
return series;
}
function linearInterpolation(timestamp, left, right) {
if (left[1] === right[1]) {
return (left[0] + right[0]) / 2;
} else {
return (left[0] + (right[0] - left[0]) / (right[1] - left[1]) * (timestamp - left[1]));
}
}
function findNearestRight(series, point) {
var point_index = _.indexOf(series, point);
var nearestRight;
for (var i = point_index; i < series.length; i++) {
if (series[i][0]) {
return series[i];
}
}
return nearestRight;
}
function findNearestLeft(series, point) {
var point_index = _.indexOf(series, point);
var nearestLeft;
for (var i = point_index; i > 0; i--) {
if (series[i][0]) {
return series[i];
}
}
return nearestLeft;
}

View File

@@ -0,0 +1,104 @@
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
import * as metricFunctions from './metricFunctions';
/** @ngInject */
angular
.module('grafana.directives')
.directive('addMetricFunction', function($compile) {
var inputTemplate = '<input type="text"'+
' class="tight-form-input input-medium tight-form-input"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="tight-form-item tight-form-func dropdown-toggle"' +
' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
'<i class="fa fa-plus"></i></a>';
return {
link: function($scope, elem) {
var categories = metricFunctions.getCategories();
var allFunctions = getAllFunctionNames(categories);
$scope.functionMenu = createFunctionDropDownMenu(categories);
var $input = $(inputTemplate);
var $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: allFunctions,
minLength: 1,
items: 10,
updater: function (value) {
var funcDef = metricFunctions.getFuncDef(value);
if (!funcDef) {
// try find close match
value = value.toLowerCase();
funcDef = _.find(allFunctions, function(funcName) {
return funcName.toLowerCase().indexOf(value) === 0;
});
if (!funcDef) { return; }
}
$scope.$apply(function() {
$scope.addFunction(funcDef);
});
$input.trigger('blur');
return '';
}
});
$button.click(function() {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(function() {
elem.toggleClass('open', $input.val() === '');
});
$input.blur(function() {
// clicking the function dropdown menu wont
// work if you remove class at once
setTimeout(function() {
$input.val('');
$input.hide();
$button.show();
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
}
};
});
function getAllFunctionNames(categories) {
return _.reduce(categories, function(list, category) {
_.each(category, function(func) {
list.push(func.name);
});
return list;
}, []);
}
function createFunctionDropDownMenu(categories) {
return _.map(categories, function(list, category) {
return {
text: category,
submenu: _.map(list, function(value) {
return {
text: value.name,
click: "ctrl.addFunction('" + value.name + "')",
};
})
};
});
}

View File

@@ -0,0 +1,7 @@
.zbx-regex {
color: #CCA300;
}
.zbx-variable {
color: #33B5E5;
}

View File

@@ -0,0 +1,430 @@
//import angular from 'angular';
import _ from 'lodash';
import * as dateMath from 'app/core/utils/datemath';
import * as utils from './utils';
import * as migrations from './migrations';
import * as metricFunctions from './metricFunctions';
import DataProcessor from './DataProcessor';
import './zabbixAPI.service.js';
import './zabbixCache.service.js';
import './queryProcessor.service.js';
export class ZabbixAPIDatasource {
/** @ngInject */
constructor(instanceSettings, $q, templateSrv, alertSrv, zabbixAPIService, ZabbixCachingProxy, QueryProcessor) {
// General data source settings
this.name = instanceSettings.name;
this.url = instanceSettings.url;
this.basicAuth = instanceSettings.basicAuth;
this.withCredentials = instanceSettings.withCredentials;
// Zabbix API credentials
this.username = instanceSettings.jsonData.username;
this.password = instanceSettings.jsonData.password;
// Use trends instead history since specified time
this.trends = instanceSettings.jsonData.trends;
this.trendsFrom = instanceSettings.jsonData.trendsFrom || '7d';
// Set cache update interval
var ttl = instanceSettings.jsonData.cacheTTL || '1h';
this.cacheTTL = utils.parseInterval(ttl);
// Initialize Zabbix API
var ZabbixAPI = zabbixAPIService;
this.zabbixAPI = new ZabbixAPI(this.url, this.username, this.password, this.basicAuth, this.withCredentials);
// Initialize cache service
this.zabbixCache = new ZabbixCachingProxy(this.zabbixAPI, this.cacheTTL);
// Initialize query builder
this.queryProcessor = new QueryProcessor(this.zabbixCache);
// Dependencies
this.q = $q;
this.templateSrv = templateSrv;
this.alertSrv = alertSrv;
console.log(this.zabbixCache);
}
////////////////////////
// Datasource methods //
////////////////////////
/**
* Test connection to Zabbix API
* @return {object} Connection status and Zabbix API version
*/
testDatasource() {
var self = this;
return this.zabbixAPI.getVersion().then(function (version) {
return self.zabbixAPI.login().then(function (auth) {
if (auth) {
return {
status: "success",
title: "Success",
message: "Zabbix API version: " + version
};
} else {
return {
status: "error",
title: "Invalid user name or password",
message: "Zabbix API version: " + version
};
}
}, function(error) {
console.log(error);
return {
status: "error",
title: "Connection failed",
message: error
};
});
},
function(error) {
console.log(error);
return {
status: "error",
title: "Connection failed",
message: "Could not connect to given url"
};
});
}
/**
* Query panel data. Calls for each panel in dashboard.
* @param {Object} options Contains time range, targets and other info.
* @return {Object} Grafana metrics object with timeseries data for each target.
*/
query(options) {
var self = this;
// get from & to in seconds
var from = Math.ceil(dateMath.parse(options.range.from) / 1000);
var to = Math.ceil(dateMath.parse(options.range.to) / 1000);
var useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
// Create request for each target
var promises = _.map(options.targets, function(target) {
if (target.mode !== 1) {
// Migrate old targets
target = migrations.migrate(target);
// Don't request undefined and hidden targets
if (target.hide || !target.group ||
!target.host || !target.item) {
return [];
}
// Replace templated variables
var groupFilter = this.templateSrv.replace(target.group.filter, options.scopedVars);
var hostFilter = this.templateSrv.replace(target.host.filter, options.scopedVars);
var appFilter = this.templateSrv.replace(target.application.filter, options.scopedVars);
var itemFilter = this.templateSrv.replace(target.item.filter, options.scopedVars);
// Query numeric data
if (!target.mode || target.mode === 0) {
// Build query in asynchronous manner
return self.queryProcessor.build(groupFilter, hostFilter, appFilter, itemFilter)
.then(function(items) {
// Add hostname for items from multiple hosts
var addHostName = utils.isRegex(target.host.filter);
var getHistory;
// Use trends
if ((from < useTrendsFrom) && self.trends) {
// Find trendValue() function and get specified trend value
var trendFunctions = _.map(metricFunctions.getCategories()['Trends'], 'name');
var trendValueFunc = _.find(target.functions, function(func) {
return _.contains(trendFunctions, func.def.name);
});
var valueType = trendValueFunc ? trendValueFunc.params[0] : "avg";
getHistory = self.zabbixAPI.getTrend(items, from, to).then(function(history) {
return self.queryProcessor.handleTrends(history, addHostName, valueType);
});
} else {
// Use history
getHistory = self.zabbixCache.getHistory(items, from, to).then(function(history) {
return self.queryProcessor.handleHistory(history, addHostName);
});
}
return getHistory.then(function (timeseries_data) {
timeseries_data = _.map(timeseries_data, function (timeseries) {
// Filter only transform functions
var transformFunctions = bindFunctionDefs(target.functions, 'Transform', DataProcessor);
// Metric data processing
var dp = timeseries.datapoints;
for (var i = 0; i < transformFunctions.length; i++) {
dp = transformFunctions[i](dp);
}
timeseries.datapoints = dp;
return timeseries;
});
// Aggregations
var aggregationFunctions = bindFunctionDefs(target.functions, 'Aggregate', DataProcessor);
var dp = _.map(timeseries_data, 'datapoints');
if (aggregationFunctions.length) {
for (var i = 0; i < aggregationFunctions.length; i++) {
dp = aggregationFunctions[i](dp);
}
var lastAgg = _.findLast(target.functions, function(func) {
return _.contains(
_.map(metricFunctions.getCategories()['Aggregate'], 'name'), func.def.name);
});
timeseries_data = [{
target: lastAgg.text,
datapoints: dp
}];
}
// Apply alias functions
var aliasFunctions = bindFunctionDefs(target.functions, 'Alias', DataProcessor);
for (var j = 0; j < aliasFunctions.length; j++) {
_.each(timeseries_data, aliasFunctions[j]);
}
return timeseries_data;
});
});
}
// Query text data
else if (target.mode === 2) {
return self.queryProcessor.build(groupFilter, hostFilter, appFilter, itemFilter)
.then(function(items) {
var deferred = self.q.defer();
if (items.length) {
self.zabbixAPI.getLastValue(items[0].itemid).then(function(lastvalue) {
if (target.textFilter) {
var text_extract_pattern = new RegExp(self.templateSrv.replace(target.textFilter, options.scopedVars));
var result = text_extract_pattern.exec(lastvalue);
if (result) {
if (target.useCaptureGroups) {
result = result[1];
} else {
result = result[0];
}
}
deferred.resolve(result);
} else {
deferred.resolve(lastvalue);
}
});
} else {
deferred.resolve(null);
}
return deferred.promise.then(function(text) {
return {
target: target.item.name,
datapoints: [[text, to * 1000]]
};
});
});
}
}
// IT services mode
else if (target.mode === 1) {
// Don't show undefined and hidden targets
if (target.hide || !target.itservice || !target.slaProperty) {
return [];
} else {
return this.zabbixAPI.getSLA(target.itservice.serviceid, from, to)
.then(slaObject => {
return self.queryProcessor.handleSLAResponse(target.itservice, target.slaProperty, slaObject);
});
}
}
}, this);
// Data for panel (all targets)
return this.q.all(_.flatten(promises))
.then(_.flatten)
.then(function (timeseries_data) {
// Series downsampling
var data = _.map(timeseries_data, function(timeseries) {
if (timeseries.datapoints.length > options.maxDataPoints) {
timeseries.datapoints =
DataProcessor.groupBy(options.interval, DataProcessor.AVERAGE, timeseries.datapoints);
}
return timeseries;
});
return { data: data };
});
}
////////////////
// Templating //
////////////////
/**
* Find metrics from templated request.
*
* @param {string} query Query from Templating
* @return {string} Metric name - group, host, app or item or list
* of metrics in "{metric1,metcic2,...,metricN}" format.
*/
metricFindQuery(query) {
// Split query. Query structure:
// group.host.app.item
var self = this;
var parts = [];
_.each(query.split('.'), function (part) {
part = self.templateSrv.replace(part);
// Replace wildcard to regex
if (part === '*') {
part = '/.*/';
}
parts.push(part);
});
var template = _.object(['group', 'host', 'app', 'item'], parts);
// Get items
if (parts.length === 4) {
// Search for all items, even it's not belong to any application
if (template.app === '/.*/') {
template.app = '';
}
return this.queryProcessor.getItems(template.group, template.host, template.app)
.then(function(items) {
return _.map(items, formatMetric);
});
}
// Get applications
else if (parts.length === 3) {
return this.queryProcessor.getApps(template.group, template.host)
.then(function(apps) {
return _.map(apps, formatMetric);
});
}
// Get hosts
else if (parts.length === 2) {
return this.queryProcessor.getHosts(template.group)
.then(function(hosts) {
return _.map(hosts, formatMetric);
});
}
// Get groups
else if (parts.length === 1) {
return this.zabbixCache.getGroups(template.group).then(function(groups) {
return _.map(groups, formatMetric);
});
}
// Return empty object for invalid request
else {
return this.q.when([]);
}
}
/////////////////
// Annotations //
/////////////////
annotationQuery(options) {
var from = Math.ceil(dateMath.parse(options.rangeRaw.from) / 1000);
var to = Math.ceil(dateMath.parse(options.rangeRaw.to) / 1000);
var annotation = options.annotation;
var self = this;
var showOkEvents = annotation.showOkEvents ? [0, 1] : 1;
// Show all triggers
var showTriggers = [0, 1];
var buildQuery = self.queryProcessor.buildTriggerQuery(this.templateSrv.replace(annotation.group),
this.templateSrv.replace(annotation.host),
this.templateSrv.replace(annotation.application));
return buildQuery.then(function(query) {
return self.zabbixAPI.getTriggers(query.groupids,
query.hostids,
query.applicationids,
showTriggers)
.then(function(triggers) {
// Filter triggers by description
if (utils.isRegex(annotation.trigger)) {
triggers = _.filter(triggers, function(trigger) {
return utils.buildRegex(annotation.trigger).test(trigger.description);
});
} else if (annotation.trigger) {
triggers = _.filter(triggers, function(trigger) {
return trigger.description === annotation.trigger;
});
}
// Remove events below the chose severity
triggers = _.filter(triggers, function(trigger) {
return Number(trigger.priority) >= Number(annotation.minseverity);
});
var objectids = _.map(triggers, 'triggerid');
return self.zabbixAPI.getEvents(objectids, from, to, showOkEvents)
.then(function (events) {
var indexedTriggers = _.indexBy(triggers, 'triggerid');
// Hide acknowledged events if option enabled
if (annotation.hideAcknowledged) {
events = _.filter(events, function(event) {
return !event.acknowledges.length;
});
}
return _.map(events, function(e) {
var title ='';
if (annotation.showHostname) {
title += e.hosts[0].name + ': ';
}
// Show event type (OK or Problem)
title += Number(e.value) ? 'Problem' : 'OK';
var formatted_acknowledges = utils.formatAcknowledges(e.acknowledges);
return {
annotation: annotation,
time: e.clock * 1000,
title: title,
text: indexedTriggers[e.objectid].description + formatted_acknowledges
};
});
});
});
});
}
}
function bindFunctionDefs(functionDefs, category, DataProcessor) {
'use strict';
var aggregationFunctions = _.map(metricFunctions.getCategories()[category], 'name');
var aggFuncDefs = _.filter(functionDefs, function(func) {
return _.contains(aggregationFunctions, func.def.name);
});
return _.map(aggFuncDefs, function(func) {
var funcInstance = metricFunctions.createFuncInstance(func.def, func.params);
return funcInstance.bindFunction(DataProcessor.metricFunctions);
});
}
function formatMetric(metricObj) {
'use strict';
return {
text: metricObj.name,
expandable: false
};
}

View File

@@ -0,0 +1,242 @@
import angular from 'angular';
import _ from 'lodash';
import $ from 'jquery';
/** @ngInject */
angular
.module('grafana.directives')
.directive('metricFunctionEditor', function($compile, templateSrv) {
var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
var paramTemplate = '<input type="text" style="display:none"' +
' class="input-mini tight-form-func-param"></input>';
var funcControlsTemplate =
'<div class="tight-form-func-controls">' +
'<span class="pointer fa fa-arrow-left"></span>' +
'<span class="pointer fa fa-question-circle"></span>' +
'<span class="pointer fa fa-remove" ></span>' +
'<span class="pointer fa fa-arrow-right"></span>' +
'</div>';
return {
restrict: 'A',
link: function postLink($scope, elem) {
var $funcLink = $(funcSpanTemplate);
var $funcControls = $(funcControlsTemplate);
var ctrl = $scope.ctrl;
var func = $scope.func;
var funcDef = func.def;
var scheduledRelink = false;
var paramCountAtLink = 0;
function clickFuncParam(paramIndex) {
/*jshint validthis:true */
var $link = $(this);
var $input = $link.next();
$input.val(func.params[paramIndex]);
$input.css('width', ($link.width() + 16) + 'px');
$link.hide();
$input.show();
$input.focus();
$input.select();
var typeahead = $input.data('typeahead');
if (typeahead) {
$input.val('');
typeahead.lookup();
}
}
function scheduledRelinkIfNeeded() {
if (paramCountAtLink === func.params.length) {
return;
}
if (!scheduledRelink) {
scheduledRelink = true;
setTimeout(function() {
relink();
scheduledRelink = false;
}, 200);
}
}
function inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
var newValue = $input.val();
if (newValue !== '' || func.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
func.updateParam($input.val(), paramIndex);
scheduledRelinkIfNeeded();
$scope.$apply(function() {
ctrl.targetChanged();
});
$input.hide();
$link.show();
}
}
function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */
if(e.which === 13) {
inputBlur.call(this, paramIndex);
}
}
function inputKeyDown() {
/*jshint validthis:true */
this.style.width = (3 + this.value.length) * 8 + 'px';
}
function addTypeahead($input, paramIndex) {
$input.attr('data-provide', 'typeahead');
var options = funcDef.params[paramIndex].options;
if (funcDef.params[paramIndex].type === 'int') {
options = _.map(options, function(val) { return val.toString(); });
}
$input.typeahead({
source: options,
minLength: 0,
items: 20,
updater: function (value) {
setTimeout(function() {
inputBlur.call($input[0], paramIndex);
}, 0);
return value;
}
});
var typeahead = $input.data('typeahead');
typeahead.lookup = function () {
this.query = this.$element.val() || '';
return this.process(this.source);
};
}
function toggleFuncControls() {
var targetDiv = elem.closest('.tight-form');
if (elem.hasClass('show-function-controls')) {
elem.removeClass('show-function-controls');
targetDiv.removeClass('has-open-function');
$funcControls.hide();
return;
}
elem.addClass('show-function-controls');
targetDiv.addClass('has-open-function');
$funcControls.show();
}
function addElementsAndCompile() {
$funcControls.appendTo(elem);
$funcLink.appendTo(elem);
_.each(funcDef.params, function(param, index) {
if (param.optional && func.params.length <= index) {
return;
}
if (index > 0) {
$('<span>, </span>').appendTo(elem);
}
var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
var $input = $(paramTemplate);
paramCountAtLink++;
$paramLink.appendTo(elem);
$input.appendTo(elem);
$input.blur(_.partial(inputBlur, index));
$input.keyup(inputKeyDown);
$input.keypress(_.partial(inputKeyPress, index));
$paramLink.click(_.partial(clickFuncParam, index));
if (funcDef.params[index].options) {
addTypeahead($input, index);
}
});
$('<span>)</span>').appendTo(elem);
$compile(elem.contents())($scope);
}
function ifJustAddedFocusFistParam() {
if ($scope.func.added) {
$scope.func.added = false;
setTimeout(function() {
elem.find('.graphite-func-param-link').first().click();
}, 10);
}
}
function registerFuncControlsToggle() {
$funcLink.click(toggleFuncControls);
}
function registerFuncControlsActions() {
$funcControls.click(function(e) {
var $target = $(e.target);
if ($target.hasClass('fa-remove')) {
toggleFuncControls();
$scope.$apply(function() {
ctrl.removeFunction($scope.func);
});
return;
}
if ($target.hasClass('fa-arrow-left')) {
$scope.$apply(function() {
_.move($scope.target.functions, $scope.$index, $scope.$index - 1);
ctrl.targetChanged();
});
return;
}
if ($target.hasClass('fa-arrow-right')) {
$scope.$apply(function() {
_.move($scope.target.functions, $scope.$index, $scope.$index + 1);
ctrl.targetChanged();
});
return;
}
if ($target.hasClass('fa-question-circle')) {
window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank');
return;
}
});
}
function relink() {
elem.children().remove();
addElementsAndCompile();
ifJustAddedFocusFistParam();
registerFuncControlsToggle();
registerFuncControlsActions();
}
relink();
}
};
});

View File

@@ -0,0 +1,208 @@
import _ from 'lodash';
import $ from 'jquery';
var index = [];
var categories = {
Transform: [],
Aggregate: [],
Trends: [],
Alias: []
};
function addFuncDef(funcDef) {
funcDef.params = funcDef.params || [];
funcDef.defaultParams = funcDef.defaultParams || [];
if (funcDef.category) {
categories[funcDef.category].push(funcDef);
}
index[funcDef.name] = funcDef;
index[funcDef.shortName || funcDef.name] = funcDef;
}
addFuncDef({
name: 'groupBy',
category: 'Transform',
params: [
{ name: 'interval', type: 'string'},
{ name: 'function', type: 'string', options: ['avg', 'min', 'max', 'median'] }
],
defaultParams: ['1m', 'avg'],
});
addFuncDef({
name: 'sumSeries',
category: 'Aggregate',
params: [],
defaultParams: [],
});
addFuncDef({
name: 'median',
category: 'Aggregate',
params: [
{ name: 'interval', type: 'string'}
],
defaultParams: ['1m'],
});
addFuncDef({
name: 'average',
category: 'Aggregate',
params: [
{ name: 'interval', type: 'string' }
],
defaultParams: ['1m'],
});
addFuncDef({
name: 'min',
category: 'Aggregate',
params: [
{ name: 'interval', type: 'string' }
],
defaultParams: ['1m'],
});
addFuncDef({
name: 'max',
category: 'Aggregate',
params: [
{ name: 'interval', type: 'string' }
],
defaultParams: ['1m'],
});
addFuncDef({
name: 'trendValue',
category: 'Trends',
params: [
{ name: 'type', type: 'string', options: ['avg', 'min', 'max'] }
],
defaultParams: ['avg'],
});
addFuncDef({
name: 'setAlias',
category: 'Alias',
params: [
{ name: 'alias', type: 'string'}
],
defaultParams: [],
});
_.each(categories, function(funcList, catName) {
categories[catName] = _.sortBy(funcList, 'name');
});
class FuncInstance {
constructor(funcDef, params) {
this.def = funcDef;
if (params) {
this.params = params;
} else {
// Create with default params
this.params = [];
this.params = funcDef.defaultParams.slice(0);
}
this.updateText();
}
bindFunction(metricFunctions) {
var func = metricFunctions[this.def.name];
if (func) {
// Bind function arguments
var bindedFunc = func;
for (var i = 0; i < this.params.length; i++) {
bindedFunc = _.partial(bindedFunc, this.params[i]);
}
return bindedFunc;
} else {
throw { message: 'Method not found ' + this.def.name };
}
}
render(metricExp) {
var str = this.def.name + '(';
var parameters = _.map(this.params, function(value, index) {
var paramType = this.def.params[index].type;
if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') {
return value;
}
else if (paramType === 'int_or_interval' && $.isNumeric(value)) {
return value;
}
return "'" + value + "'";
}, this);
if (metricExp) {
parameters.unshift(metricExp);
}
return str + parameters.join(', ') + ')';
}
_hasMultipleParamsInString(strValue, index) {
if (strValue.indexOf(',') === -1) {
return false;
}
return this.def.params[index + 1] && this.def.params[index + 1].optional;
}
updateParam(strValue, index) {
// handle optional parameters
// if string contains ',' and next param is optional, split and update both
if (this._hasMultipleParamsInString(strValue, index)) {
_.each(strValue.split(','), function(partVal, idx) {
this.updateParam(partVal.trim(), idx);
}, this);
return;
}
if (strValue === '' && this.def.params[index].optional) {
this.params.splice(index, 1);
}
else {
this.params[index] = strValue;
}
this.updateText();
}
updateText() {
if (this.params.length === 0) {
this.text = this.def.name + '()';
return;
}
var text = this.def.name + '(';
text += this.params.join(', ');
text += ')';
this.text = text;
}
}
export function createFuncInstance(funcDef, params) {
if (_.isString(funcDef)) {
if (!index[funcDef]) {
throw { message: 'Method not found ' + name };
}
funcDef = index[funcDef];
}
return new FuncInstance(funcDef, params);
}
export function getFuncDef(name) {
return index[name];
}
export function getCategories() {
return categories;
}

View File

@@ -0,0 +1,38 @@
/**
* Query format migration.
* This module can detect query format version and make migration.
*/
export function isGrafana2target(target) {
if (!target.mode || target.mode === 0 || target.mode === 2) {
if ((target.hostFilter || target.itemFilter || target.downsampleFunction ||
(target.host && target.host.host)) &&
(target.item.filter === undefined && target.host.filter === undefined)) {
return true;
} else {
return false;
}
} else {
return false;
}
}
export function migrateFrom2To3version(target) {
target.group.filter = target.group.name === "*" ? "/.*/" : target.group.name;
target.host.filter = target.host.name === "*" ? convertToRegex(target.hostFilter) : target.host.name;
target.application.filter = target.application.name === "*" ? "" : target.application.name;
target.item.filter = target.item.name === "All" ? convertToRegex(target.itemFilter) : target.item.name;
return target;
}
export function migrate(target) {
if (isGrafana2target(target)) {
return migrateFrom2To3version(target);
} else {
return target;
}
}
function convertToRegex(str) {
return '/' + str + '/';
}

View File

@@ -0,0 +1,19 @@
import {ZabbixAPIDatasource} from './datasource';
import {ZabbixQueryController} from './query.controller';
class ZabbixConfigController {}
ZabbixConfigController.templateUrl = 'datasource-zabbix/partials/config.html';
class ZabbixQueryOptionsController {}
ZabbixQueryOptionsController.templateUrl = 'datasource-zabbix/partials/query.options.html';
class ZabbixAnnotationsQueryController {}
ZabbixAnnotationsQueryController.templateUrl = 'datasource-zabbix/partials/annotations.editor.html';
export {
ZabbixAPIDatasource as Datasource,
ZabbixConfigController as ConfigCtrl,
ZabbixQueryController as QueryCtrl,
ZabbixQueryOptionsController as QueryOptionsCtrl,
ZabbixAnnotationsQueryController as AnnotationsQueryCtrl
};

View File

@@ -0,0 +1,65 @@
<div class="gf-form-group">
<h6>Filter Triggers</h6>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10">Group</span>
<input type="text"
class="gf-form-input max-width-16"
ng-model="ctrl.annotation.group">
</input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Host</span>
<input type="text"
class="gf-form-input max-width-16"
ng-model="ctrl.annotation.host">
</input>
</div>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10">Application</span>
<input type="text"
class="gf-form-input max-width-16"
ng-model="ctrl.annotation.application">
</input>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Trigger</span>
<input type="text"
class="gf-form-input max-width-16"
ng-model="ctrl.annotation.trigger">
</input>
</div>
</div>
</div>
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-10">Minimum severity</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input gf-size-auto"
ng-init='ctrl.annotation.minseverity = ctrl.annotation.minseverity || 0'
ng-model='ctrl.annotation.minseverity'
ng-options="v as k for (k, v) in {
'Not classified': 0,
'Information': 1,
'Warning': 2,
'Average': 3,
'High': 4,
'Disaster': 5
}"
ng-change="render()">
</select>
</div>
</div>
</div>
<div class="gf-form-group">
<h6>Options</h6>
<div class="gf-form">
<editor-checkbox text="Show OK events" model="ctrl.annotation.showOkEvents"></editor-checkbox>
<editor-checkbox text="Hide acknowledged events" model="ctrl.annotation.hideAcknowledged"></editor-checkbox>
<editor-checkbox text="Show hostname" model="ctrl.annotation.showHostname"></editor-checkbox>
</div>
</div>

View File

@@ -0,0 +1,61 @@
<datasource-http-settings current="ctrl.current">
</datasource-http-settings>
<div class="gf-form-group">
<h3 class="page-heading">Zabbix API details</h3>
<div class="gf-form">
<span class="gf-form-label width-7">
Username
</span>
<input class="gf-form-input max-width-21"
type="text"
ng-model='ctrl.current.jsonData.username'
placeholder="user"
required>
</input>
</div>
<div class="gf-form">
<span class="gf-form-label width-7">
Password
</span>
<input class="gf-form-input max-width-21"
type="password"
ng-model='ctrl.current.jsonData.password'
placeholder="password"
required>
</input>
</div>
<div class="gf-form-inline">
<div class="gf-form">
<label class="gf-form-label width-7">Trends</label>
</div>
<gf-form-switch class="gf-form"
label="Enable"
checked="ctrl.current.jsonData.trends" switch-class="max-width-6">
</gf-form-switch>
<div class="gf-form" ng-if="ctrl.current.jsonData.trends">
<span class="gf-form-label width-7">
Use from
</span>
<input class="gf-form-input max-width-5"
type="text"
ng-model='ctrl.current.jsonData.trendsFrom'
placeholder="7d">
</input>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-12">
Cache update interval
</span>
<input class="gf-form-input max-width-4"
type="text"
ng-model='ctrl.current.jsonData.cacheTTL'
placeholder="1h">
</input>
</div>
</div>

View File

@@ -0,0 +1,233 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li class="tight-form-item small" ng-show="ctrl.target.datasource">
<em>{{ctrl.target.datasource}}</em>
</li>
<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">
<!-- Switch editor mode -->
<li role="menuitem" ng-show="ctrl.target.mode">
<a class="pointer" tabindex="1"
ng-click="ctrl.switchEditorMode(0)">Numeric metrics</a>
</li>
<li role="menuitem" ng-show="ctrl.target.mode != 1">
<a class="pointer" tabindex="1"
ng-click="ctrl.switchEditorMode(1)">IT services</a>
</li>
<li role="menuitem" ng-show="ctrl.target.mode != 2">
<a class="pointer" tabindex="1"
ng-click="ctrl.switchEditorMode(2)">Text metrics</a>
</li>
<li class="divider" role="menuitem"></li>
<!-- From app/features/panel/partials/query_editor_row.html -->
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a>
</li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(target)">
<i class="fa fa-trash"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{ctrl.target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="ctrl.toggleHideQuery()" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<!-- IT Service editor -->
<ul class="tight-form-list" role="menu" ng-show="ctrl.target.mode == 1">
<li class="tight-form-item input-small">IT Service</li>
<li>
<select class="tight-form-input input-large"
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>
</li>
<li class="tight-form-item input-medium">IT service property</li>
<li>
<select class="tight-form-input input-medium"
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>
</select>
</li>
</ul>
<ul class="tight-form-list" role="menu" ng-hide="ctrl.target.mode == 1">
<!-- Select Host Group -->
<li class="tight-form-item input-small" style="width: 5em">Group</li>
<li>
<input type="text"
ng-model="ctrl.target.group.filter"
bs-typeahead="ctrl.getGroupNames"
ng-change="ctrl.onTargetPartChange(ctrl.target.group)"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="input-medium tight-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.group.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.group.filter)
}">
</li>
<!-- Select Host -->
<li class="tight-form-item input-small" style="width: 3em">Host</li>
<li>
<input type="text"
ng-model="ctrl.target.host.filter"
bs-typeahead="ctrl.getHostNames"
ng-change="ctrl.onTargetPartChange(ctrl.target.host)"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="input-large tight-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.host.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.host.filter)
}">
</li>
<li class="tight-form-item" ng-hide="ctrl.target.mode == 2">
Show disabled items&nbsp;
<editor-checkbox text=""
model="ctrl.target.showDisabledItems"
change="ctrl.onTargetBlur()">
</editor-checkbox>
</li>
<!-- Downsampling function -->
<!-- <li class="tight-form-item input-medium" ng-hide="ctrl.target.mode == 2">
Downsampling
</li>
<li ng-hide="ctrl.target.mode == 2">
<select class="tight-form-input input-small"
ng-change="ctrl.targetBlur()"
ng-model="ctrl.target.downsampleFunction"
bs-tooltip="'Downsampling function'"
ng-options="func.name for func in downsampleFunctionList track by func.name">
</select>
<a bs-tooltip="ctrl.target.errors.metric"
style="color: rgb(229, 189, 28)"
ng-show="ctrl.target.errors.metric">
<i class="icon-warning-sign"></i>
</a>
</li> -->
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-hide="ctrl.target.mode == 1">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item" style="width: 44px">&nbsp</li>
<!-- Select Application -->
<li class="tight-form-item" style="width: 5em">Application</li>
<li>
<input type="text"
ng-model="ctrl.target.application.filter"
bs-typeahead="ctrl.getApplicationNames"
ng-change="ctrl.onTargetPartChange(ctrl.target.application)"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="input-medium tight-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.application.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.application.filter)
}">
</li>
<!-- Select Item -->
<li class="tight-form-item input-small" style="width: 3em">Item</li>
<li>
<input type="text"
ng-model="ctrl.target.item.filter"
bs-typeahead="ctrl.getItemNames"
ng-change="ctrl.onTargetPartChange(ctrl.target.item)"
ng-blur="ctrl.onTargetBlur()"
data-min-length=0
data-items=100
class="input-large tight-form-input"
ng-class="{
'zbx-variable': ctrl.isVariable(ctrl.target.item.filter),
'zbx-regex': ctrl.isRegex(ctrl.target.item.filter)
}">
</li>
<li class="tight-form-item query-keyword">Options</li>
<li ng-repeat="func in ctrl.target.functions">
<span metric-function-editor class="tight-form-item tight-form-func">
</span>
</li>
<li class="dropdown" add-metric-function>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-hide="ctrl.target.mode === 1" ng-if="ctrl.target.options">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item" style="width: 44px">&nbsp</li>
<li class="tight-form-item query-keyword" style="width: 5em">Options</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form" ng-show="ctrl.target.mode == 2">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item" style="width: 44px">&nbsp</li>
<!-- Text metric regex -->
<li class="tight-form-item" style="width: 5em" ng-show="ctrl.target.mode == 2">
Text filter
</li>
<li ng-show="ctrl.target.mode == 2">
<input type="text"
class="tight-form-input" style="width: 417px"
ng-model="ctrl.target.textFilter"
spellcheck='false'
placeholder="Text filter (regex)"
ng-blur="ctrl.targetBlur()">
</li>
<li class="tight-form-item" ng-show="ctrl.target.mode == 2">
Use capture groups&nbsp;
<input class="cr1" id="ctrl.target.useCaptureGroups" type="checkbox"
ng-model="ctrl.target.useCaptureGroups"
ng-checked="ctrl.target.useCaptureGroups"
ng-change="ctrl.targetBlur()">
<label for="ctrl.target.useCaptureGroups" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@@ -11,10 +11,10 @@
<li>
<input type="text"
class="input-mini tight-form-input"
ng-model="panel.maxDataPoints"
ng-model="ctrl.panelCtrl.panel.maxDataPoints"
bs-tooltip="'Override max data points, automatically set to graph width in pixels.'"
data-placement="right"
ng-model-onblur ng-change="get_data()"
ng-model-onblur ng-change="ctrl.panelCtrl.refresh()"
spellcheck='false'
placeholder="auto">
</li>
@@ -27,20 +27,25 @@
<i class="fa fa-info-circle"></i>
</li>
<li class="tight-form-item">
<a ng-click="toggleEditorHelp(1)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(1)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
Max data points
</a>
</li>
<li class="tight-form-item">
<a ng-click="toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
IT services
</a>
</li>
<li class="tight-form-item">
<a ng-click="toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
IT service property
</a>
</li>
<li class="tight-form-item">
<a ng-click="ctrl.panelCtrl.toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
Text filter
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
@@ -49,7 +54,7 @@
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span8" ng-if="editorHelpIndex === 1">
<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 1">
<h5>Max data points</h5>
<ul>
<li>Grafana-Zabbix plugin uses maxDataPoints parameter to consolidate the real number of values down to this
@@ -62,14 +67,14 @@
</ul>
</div>
<div class="grafana-info-box span8" ng-if="editorHelpIndex === 2">
<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 2">
<h5>IT services</h5>
<ul>
<li>Select "IT services" in targets menu to activate IT services mode.</li>
</ul>
</div>
<div class="grafana-info-box span8" ng-if="editorHelpIndex === 3">
<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 3">
<h5>IT service property</h5>
<ul>
<li>Zabbix returns the following availability information about IT service</li>
@@ -80,5 +85,12 @@
<li>Down time - time the service was in scheduled downtime, in seconds</li>
</ul>
</div>
<div class="grafana-info-box span8" ng-if="ctrl.panelCtrl.editorHelpIndex === 4">
<h5>Text filter</h5>
<ul>
<li>Use regex to extract a part of the returned value.</li>
</ul>
</div>
</div>
</div>

View File

@@ -0,0 +1,15 @@
{
"type": "datasource",
"name": "Zabbix",
"id": "zabbix-datasource",
"metrics": true,
"annotations": true,
"info": {
"author": {
"name": "Alexander Zobnin",
"url": "http://grafana-zabbix.org"
}
}
}

View File

@@ -0,0 +1,296 @@
import {QueryCtrl} from 'app/plugins/sdk';
import _ from 'lodash';
import * as utils from './utils';
import * as metricFunctions from './metricFunctions';
import * as migrations from './migrations';
import './add-metric-function.directive';
import './metric-function-editor.directive';
import './css/query-editor.css!';
export class ZabbixQueryController extends QueryCtrl {
// ZabbixQueryCtrl constructor
constructor($scope, $injector, $sce, $q, templateSrv) {
// Call superclass constructor
super($scope, $injector);
this.zabbix = this.datasource.zabbixAPI;
this.cache = this.datasource.zabbixCache;
this.$q = $q;
this.editorModes = {
0: 'num',
1: 'itservice',
2: 'text'
};
// Map functions for bs-typeahead
this.getGroupNames = _.partial(getMetricNames, this, 'groupList');
this.getHostNames = _.partial(getMetricNames, this, 'hostList');
this.getApplicationNames = _.partial(getMetricNames, this, 'appList');
this.getItemNames = _.partial(getMetricNames, this, 'itemList');
this.init = function() {
this.templateSrv = templateSrv;
var target = this.target;
// Migrate old targets
target = migrations.migrate(target);
var scopeDefaults = {
metric: {},
oldTarget: _.cloneDeep(this.target)
};
_.defaults(this, scopeDefaults);
// Load default values
var targetDefaults = {
mode: 0,
group: { filter: "" },
host: { filter: "" },
application: { filter: "" },
item: { filter: "" },
functions: [],
refId: "A"
};
_.defaults(target, targetDefaults);
// Create function instances from saved JSON
target.functions = _.map(target.functions, function(func) {
return metricFunctions.createFuncInstance(func.def, func.params);
});
if (target.mode === 0 ||
target.mode === 2) {
this.downsampleFunctionList = [
{name: "avg", value: "avg"},
{name: "min", value: "min"},
{name: "max", value: "max"}
];
this.initFilters();
}
else if (target.mode === 1) {
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();
}
};
this.init();
}
initFilters() {
var self = this;
return this.$q.when(this.suggestGroups())
.then(() => {return self.suggestHosts();})
.then(() => {return self.suggestApps();})
.then(() => {return self.suggestItems();});
}
suggestGroups() {
var self = this;
return this.cache.getGroups().then(groups => {
self.metric.groupList = groups;
return groups;
});
}
suggestHosts() {
var self = this;
var groupFilter = this.templateSrv.replace(this.target.group.filter);
return this.datasource.queryProcessor
.filterGroups(self.metric.groupList, groupFilter)
.then(groups => {
var groupids = _.map(groups, 'groupid');
return self.zabbix
.getHosts(groupids)
.then(hosts => {
self.metric.hostList = hosts;
return hosts;
});
});
}
suggestApps() {
var self = this;
var hostFilter = this.templateSrv.replace(this.target.host.filter);
return this.datasource.queryProcessor
.filterHosts(self.metric.hostList, hostFilter)
.then(hosts => {
var hostids = _.map(hosts, 'hostid');
return self.zabbix
.getApps(hostids)
.then(apps => {
return self.metric.appList = apps;
});
});
}
suggestItems() {
var self = this;
var appFilter = this.templateSrv.replace(this.target.application.filter);
if (appFilter) {
// Filter by applications
return this.datasource.queryProcessor
.filterApps(self.metric.appList, appFilter)
.then(apps => {
var appids = _.map(apps, 'applicationid');
return self.zabbix
.getItems(undefined, appids)
.then(items => {
if (!self.target.showDisabledItems) {
items = _.filter(items, {'status': '0'});
}
self.metric.itemList = items;
return items;
});
});
} else {
// Return all items belonged to selected hosts
var hostids = _.map(self.metric.hostList, 'hostid');
return self.zabbix
.getItems(hostids)
.then(items => {
if (!self.target.showDisabledItems) {
items = _.filter(items, {'status': '0'});
}
self.metric.itemList = items;
return items;
});
}
}
onTargetPartChange(targetPart) {
/*var regexStyle = {'color': '#CCA300'};
targetPart.isRegex = utils.isRegex(targetPart.filter);
targetPart.style = targetPart.isRegex ? regexStyle : {};*/
}
isRegex(str) {
return utils.isRegex(str);
}
isVariable(str) {
var variablePattern = /^\$\w+/;
if (variablePattern.test(str)) {
var variables = _.map(this.templateSrv.variables, variable => {
return '$' + variable.name;
});
return _.contains(variables, str);
} else {
return false;
}
}
onTargetBlur() {
var newTarget = _.cloneDeep(this.target);
if (!_.isEqual(this.oldTarget, this.target)) {
this.oldTarget = newTarget;
this.initFilters();
this.parseTarget();
this.panelCtrl.refresh();
}
}
parseTarget() {
// Parse target
}
// Validate target and set validation info
validateTarget() {
// validate
}
targetChanged() {
this.panelCtrl.refresh();
}
addFunction(funcDef) {
var newFunc = metricFunctions.createFuncInstance(funcDef);
newFunc.added = true;
this.target.functions.push(newFunc);
this.moveAliasFuncLast();
if (newFunc.params.length && newFunc.added ||
newFunc.def.params.length === 0) {
this.targetChanged();
}
}
removeFunction(func) {
this.target.functions = _.without(this.target.functions, func);
this.targetChanged();
}
moveAliasFuncLast() {
var aliasFunc = _.find(this.target.functions, function(func) {
return func.def.name === 'alias' ||
func.def.name === 'aliasByNode' ||
func.def.name === 'aliasByMetric';
});
if (aliasFunc) {
this.target.functions = _.without(this.target.functions, aliasFunc);
this.target.functions.push(aliasFunc);
}
}
/**
* Switch query editor to specified mode.
* Modes:
* 0 - items
* 1 - IT services
* 2 - Text metrics
*/
switchEditorMode(mode) {
this.target.mode = mode;
this.init();
}
/////////////////
// IT Services //
/////////////////
/**
* Update list of IT services
*/
updateITServiceList() {
var self = this;
this.datasource.zabbixAPI.getITService().then(function (iteservices) {
self.itserviceList = [];
self.itserviceList = self.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();
}
}
}
// Set templateUrl as static property
ZabbixQueryController.templateUrl = 'datasource-zabbix/partials/query.editor.html';
// Get list of metric names for bs-typeahead directive
function getMetricNames(scope, metricList) {
return _.uniq(_.map(scope.metric[metricList], 'name'));
}

View File

@@ -0,0 +1,372 @@
import angular from 'angular';
import _ from 'lodash';
import * as utils from './utils';
/** @ngInject */
angular.module('grafana.services').factory('QueryProcessor', function($q) {
class QueryProcessor {
constructor(zabbixCacheInstance) {
this.cache = zabbixCacheInstance;
this.$q = $q;
}
/**
* Build query in asynchronous manner
*/
build(groupFilter, hostFilter, appFilter, itemFilter) {
var self = this;
if (this.cache._initialized) {
return this.$q.when(self.buildFromCache(groupFilter, hostFilter, appFilter, itemFilter));
} else {
return this.cache.refresh().then(function() {
return self.buildFromCache(groupFilter, hostFilter, appFilter, itemFilter);
});
}
}
/**
* Build trigger query in asynchronous manner
*/
buildTriggerQuery(groupFilter, hostFilter, appFilter) {
var self = this;
if (this.cache._initialized) {
return this.$q.when(self.buildTriggerQueryFromCache(groupFilter, hostFilter, appFilter));
} else {
return this.cache.refresh().then(function() {
return self.buildTriggerQueryFromCache(groupFilter, hostFilter, appFilter);
});
}
}
filterGroups(groups, groupFilter) {
return this.$q.when(
findByFilter(groups, groupFilter)
);
}
/**
* Get list of host belonging to given groups.
* @return list of hosts
*/
filterHosts(hosts, hostFilter) {
return this.$q.when(
findByFilter(hosts, hostFilter)
);
}
filterApps(apps, appFilter) {
return this.$q.when(
findByFilter(apps, appFilter)
);
}
/**
* Build query - convert target filters to array of Zabbix items
*/
buildFromCache(groupFilter, hostFilter, appFilter, itemFilter, showDisabledItems) {
return this.getItems(groupFilter, hostFilter, appFilter, showDisabledItems)
.then(items => {
return getByFilter(items, itemFilter);
});
}
getGroups() {
return this.cache.getGroups();
}
/**
* Get list of host belonging to given groups.
* @return list of hosts
*/
getHosts(groupFilter) {
var self = this;
return this.cache
.getGroups()
.then(groups => {
return findByFilter(groups, groupFilter);
})
.then(groups => {
var groupids = _.map(groups, 'groupid');
return self.cache.getHosts(groupids);
});
}
/**
* Get list of applications belonging to given groups and hosts.
* @return list of applications belonging to given hosts
*/
getApps(groupFilter, hostFilter) {
var self = this;
return this.getHosts(groupFilter)
.then(hosts => {
return findByFilter(hosts, hostFilter);
})
.then(hosts => {
var hostids = _.map(hosts, 'hostid');
return self.cache.getApps(hostids);
});
}
getItems(groupFilter, hostFilter, appFilter, showDisabledItems) {
var self = this;
return this.getHosts(groupFilter)
.then(hosts => {
return findByFilter(hosts, hostFilter);
})
.then(hosts => {
var hostids = _.map(hosts, 'hostid');
if (appFilter) {
return self.cache
.getApps(hostids)
.then(apps => {
// Use getByFilter for proper item filtering
return getByFilter(apps, appFilter);
});
} else {
return {
appFilterEmpty: true,
hostids: hostids
};
}
})
.then(apps => {
if (apps.appFilterEmpty) {
return self.cache
.getItems(apps.hostids, undefined)
.then(items => {
if (showDisabledItems) {
items = _.filter(items, {'status': '0'});
}
return items;
});
} else {
var appids = _.map(apps, 'applicationid');
return self.cache
.getItems(undefined, appids)
.then(items => {
if (showDisabledItems) {
items = _.filter(items, {'status': '0'});
}
return items;
});
}
});
}
/**
* Build query - convert target filters to array of Zabbix items
*/
buildTriggerQueryFromCache(groupFilter, hostFilter, appFilter) {
var promises = [
this.cache.getGroups().then(function(groups) {
return _.filter(groups, function(group) {
if (utils.isRegex(groupFilter)) {
return utils.buildRegex(groupFilter).test(group.name);
} else {
return group.name === groupFilter;
}
});
}),
this.getHosts(groupFilter).then(function(hosts) {
return _.filter(hosts, function(host) {
if (utils.isRegex(hostFilter)) {
return utils.buildRegex(hostFilter).test(host.name);
} else {
return host.name === hostFilter;
}
});
}),
this.getApps(groupFilter, hostFilter).then(function(apps) {
return _.filter(apps, function(app) {
if (utils.isRegex(appFilter)) {
return utils.buildRegex(appFilter).test(app.name);
} else {
return app.name === appFilter;
}
});
})
];
return this.$q.all(promises).then(function(results) {
var filteredGroups = results[0];
var filteredHosts = results[1];
var filteredApps = results[2];
var query = {};
if (appFilter) {
query.applicationids = _.flatten(_.map(filteredApps, 'applicationid'));
}
if (hostFilter) {
query.hostids = _.map(filteredHosts, 'hostid');
}
if (groupFilter) {
query.groupids = _.map(filteredGroups, 'groupid');
}
return query;
});
}
/**
* Convert Zabbix API history.get response to Grafana format
*
* @return {Array} Array of timeseries in Grafana format
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* }
*/
convertHistory(history, addHostName, convertPointCallback) {
/**
* Response should be in the format:
* data: [
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* }, ...
* ]
*/
var self = this;
// Group history by itemid
var grouped_history = _.groupBy(history, 'itemid');
return _.map(grouped_history, function(hist, itemid) {
var item = self.cache.getItem(itemid);
var alias = item.name;
if (addHostName) {
var host = self.cache.getHost(item.hostid);
alias = host.name + ": " + alias;
}
return {
target: alias,
datapoints: _.map(hist, convertPointCallback)
};
});
}
handleHistory(history, addHostName) {
return this.convertHistory(history, addHostName, convertHistoryPoint);
}
handleTrends(history, addHostName, valueType) {
var convertPointCallback = _.partial(convertTrendPoint, valueType);
return this.convertHistory(history, addHostName, convertPointCallback);
}
handleSLAResponse(itservice, slaProperty, slaObject) {
var targetSLA = slaObject[itservice.serviceid].sla[0];
if (slaProperty.property === 'status') {
var targetStatus = parseInt(slaObject[itservice.serviceid].status);
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetStatus, targetSLA.to * 1000]
]
};
} else {
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetSLA[slaProperty.property], targetSLA.from * 1000],
[targetSLA[slaProperty.property], targetSLA.to * 1000]
]
};
}
}
}
return QueryProcessor;
});
/**
* Find group, host, app or item by given name.
* @param list list of groups, apps or other
* @param name visible name
* @return array with finded element or undefined
*/
function findByName(list, name) {
var finded = _.find(list, {'name': name});
if (finded) {
return [finded];
} else {
return undefined;
}
}
/**
* Different hosts can contains applications and items with same name.
* For this reason use _.filter, which return all elements instead _.find,
* which return only first finded.
* @param {[type]} list list of elements
* @param {[type]} name app name
* @return {[type]} array with finded element or undefined
*/
function filterByName(list, name) {
var finded = _.filter(list, {'name': name});
if (finded) {
return finded;
} else {
return undefined;
}
}
function findByRegex(list, regex) {
var filterPattern = utils.buildRegex(regex);
return _.filter(list, function (zbx_obj) {
return filterPattern.test(zbx_obj.name);
});
}
function findByFilter(list, filter) {
if (utils.isRegex(filter)) {
return findByRegex(list, filter);
} else {
return findByName(list, filter);
}
}
function getByFilter(list, filter) {
if (utils.isRegex(filter)) {
return findByRegex(list, filter);
} else {
return filterByName(list, filter);
}
}
function getFromIndex(index, objids) {
return _.map(objids, function(id) {
return index[id];
});
}
function convertHistoryPoint(point) {
// Value must be a number for properly work
return [
Number(point.value),
point.clock * 1000
];
}
function convertTrendPoint(valueType, point) {
var value;
switch (valueType) {
case "min":
value = point.value_min;
break;
case "max":
value = point.value_max;
break;
case "avg":
value = point.value_avg;
break;
default:
value = point.value_avg;
}
return [
Number(value),
point.clock * 1000
];
}

View File

@@ -0,0 +1,77 @@
import _ from 'lodash';
import moment from 'moment';
/**
* Expand Zabbix item name
*
* @param {string} name item name, ie "CPU $2 time"
* @param {string} key item key, ie system.cpu.util[,system,avg1]
* @return {string} expanded name, ie "CPU system time"
*/
export function expandItemName(name, 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;
}
// Pattern for testing regex
var regexPattern = /^\/(.*)\/([gmi]*)$/m;
export function isRegex(str) {
return regexPattern.test(str);
}
export function buildRegex(str) {
var matches = str.match(regexPattern);
var pattern = matches[1];
var flags = matches[2] !== "" ? matches[2] : undefined;
return new RegExp(pattern, flags);
}
export function parseInterval(interval) {
var intervalPattern = /(^[\d]+)(y|M|w|d|h|m|s)/g;
var momentInterval = intervalPattern.exec(interval);
return moment.duration(Number(momentInterval[1]), momentInterval[2]).valueOf();
}
/**
* Format acknowledges.
*
* @param {array} acknowledges array of Zabbix acknowledge objects
* @return {string} HTML-formatted table
*/
export function formatAcknowledges(acknowledges) {
if (acknowledges.length) {
var formatted_acknowledges = '<br><br>Acknowledges:<br><table><tr><td><b>Time</b></td>'
+ '<td><b>User</b></td><td><b>Comments</b></td></tr>';
_.each(_.map(acknowledges, function (ack) {
var timestamp = moment.unix(ack.clock);
return '<tr><td><i>' + timestamp.format("DD MMM YYYY HH:mm:ss") + '</i></td><td>' + ack.alias
+ ' (' + ack.name + ' ' + ack.surname + ')' + '</td><td>' + ack.message + '</td></tr>';
}), function (ack) {
formatted_acknowledges = formatted_acknowledges.concat(ack);
});
formatted_acknowledges = formatted_acknowledges.concat('</table>');
return formatted_acknowledges;
} else {
return '';
}
}
export function convertToZabbixAPIUrl(url) {
var zabbixAPIUrlPattern = /.*api_jsonrpc.php$/;
var trimSlashPattern = /(.*?)[\/]*$/;
if (url.match(zabbixAPIUrlPattern)) {
return url;
} else {
return url.replace(trimSlashPattern, "$1");
}
}

View File

@@ -0,0 +1,375 @@
import angular from 'angular';
import _ from 'lodash';
import * as utils from './utils';
import './zabbixAPICore.service';
/** @ngInject */
function ZabbixAPIService($q, alertSrv, zabbixAPICoreService) {
/**
* Zabbix API Wrapper.
* Creates Zabbix API instance with given parameters (url, credentials and other).
* Wraps API calls and provides high-level methods.
*/
class ZabbixAPI {
constructor(api_url, username, password, basicAuth, withCredentials) {
this.url = api_url;
this.username = username;
this.password = password;
this.auth = "";
this.requestOptions = {
basicAuth: basicAuth,
withCredentials: withCredentials
};
this.loginPromise = null;
this.$q = $q;
this.alertSrv = alertSrv;
this.zabbixAPICore = zabbixAPICoreService;
this.getTrend = this.getTrend_ZBXNEXT1193;
//getTrend = getTrend_30;
}
//////////////////////////
// Core method wrappers //
//////////////////////////
request(method, params) {
var self = this;
return this.zabbixAPICore.request(this.url, method, params, this.requestOptions, this.auth)
.then(function(result) {
return result;
},
// Handle API errors
function(error) {
if (isNotAuthorized(error.data)) {
return self.loginOnce().then(
function() {
return self.request(method, params);
},
// Handle user.login method errors
function(error) {
self.alertAPIError(error.data);
});
}
});
}
alertAPIError(message) {
this.alertSrv.set(
"Zabbix API Error",
message,
'error'
);
}
/**
* When API unauthenticated or auth token expired each request produce login()
* call. But auth token is common to all requests. This function wraps login() method
* and call it once. If login() already called just wait for it (return its promise).
* @return login promise
*/
loginOnce() {
var self = this;
var deferred = this.$q.defer();
if (!self.loginPromise) {
self.loginPromise = deferred.promise;
self.login().then(
function(auth) {
self.loginPromise = null;
self.auth = auth;
deferred.resolve(auth);
},
function(error) {
self.loginPromise = null;
deferred.reject(error);
}
);
} else {
return self.loginPromise;
}
return deferred.promise;
}
/**
* Get authentication token.
*/
login() {
return this.zabbixAPICore.login(this.url, this.username, this.password, this.requestOptions);
}
/**
* Get Zabbix API version
*/
getVersion() {
return this.zabbixAPICore.getVersion(this.url, this.requestOptions);
}
////////////////////////////////
// Zabbix API method wrappers //
////////////////////////////////
getGroups() {
var params = {
output: ['name'],
sortfield: 'name',
real_hosts: true
};
return this.request('hostgroup.get', params);
}
getHosts(groupids) {
var params = {
output: ['name', 'host'],
sortfield: 'name'
};
if (groupids) {
params.groupids = groupids;
}
return this.request('host.get', params);
}
getApps(hostids) {
var params = {
output: ['applicationid', 'name'],
hostids: hostids
};
return this.request('application.get', params);
}
getItems(hostids, appids) {
var params = {
output: [
'name', 'key_',
'value_type',
'hostid',
'status',
'state'
],
sortfield: 'name',
};
if (hostids) {
params.hostids = hostids;
}
if (appids) {
params.applicationids = appids;
}
return this.request('item.get', params)
.then(items => {
return _.forEach(items, item => {
item.item = item.name;
item.name = utils.expandItemName(item.item, item.key_);
return item;
});
});
}
getLastValue(itemid) {
var params = {
output: ['lastvalue'],
itemids: itemid
};
return this.request('item.get', params).then(function(items) {
if (items.length) {
return items[0].lastvalue;
} else {
return null;
}
});
}
/**
* Perform history query from Zabbix API
*
* @param {Array} items Array of Zabbix item objects
* @param {Number} time_from Time in seconds
* @param {Number} time_till Time in seconds
* @return {Array} Array of Zabbix history objects
*/
getHistory(items, time_from, time_till) {
var self = this;
// Group items by value type
var grouped_items = _.groupBy(items, 'value_type');
// Perform request for each value type
return this.$q.all(_.map(grouped_items, function (items, value_type) {
var itemids = _.map(items, 'itemid');
var params = {
output: 'extend',
history: value_type,
itemids: itemids,
sortfield: 'clock',
sortorder: 'ASC',
time_from: time_from
};
// Relative queries (e.g. last hour) don't include an end time
if (time_till) {
params.time_till = time_till;
}
return self.request('history.get', params);
})).then(_.flatten);
}
/**
* Perform trends query from Zabbix API
* Use trends api extension from ZBXNEXT-1193 patch.
*
* @param {Array} items Array of Zabbix item objects
* @param {Number} time_from Time in seconds
* @param {Number} time_till Time in seconds
* @return {Array} Array of Zabbix trend objects
*/
getTrend_ZBXNEXT1193(items, time_from, time_till) {
var self = this;
// Group items by value type
var grouped_items = _.groupBy(items, 'value_type');
// Perform request for each value type
return this.$q.all(_.map(grouped_items, function (items, value_type) {
var itemids = _.map(items, 'itemid');
var params = {
output: 'extend',
trend: value_type,
itemids: itemids,
sortfield: 'clock',
sortorder: 'ASC',
time_from: time_from
};
// Relative queries (e.g. last hour) don't include an end time
if (time_till) {
params.time_till = time_till;
}
return self.request('trend.get', params);
})).then(_.flatten);
}
getTrend_30(items, time_from, time_till, value_type) {
var self = this;
var itemids = _.map(items, 'itemid');
var params = {
output: ["itemid",
"clock",
value_type
],
itemids: itemids,
time_from: time_from
};
// Relative queries (e.g. last hour) don't include an end time
if (time_till) {
params.time_till = time_till;
}
return self.request('trend.get', params);
}
getITService(/* optional */ serviceids) {
var params = {
output: 'extend',
serviceids: serviceids
};
return this.request('service.get', params);
}
getSLA(serviceids, from, to) {
var params = {
serviceids: serviceids,
intervals: [{
from: from,
to: to
}]
};
return this.request('service.getsla', params);
}
getTriggers(groupids, hostids, applicationids, showTriggers) {
var params = {
output: 'extend',
groupids: groupids,
hostids: hostids,
applicationids: applicationids,
expandDescription: true,
expandData: true,
monitored: true,
skipDependent: true,
//only_true: true,
filter: {
value: 1
},
selectGroups: ['name'],
selectHosts: ['name', 'host'],
selectItems: ['name', 'key_', 'lastvalue'],
selectLastEvent: 'extend'
};
if (showTriggers) {
params.filter.value = showTriggers;
}
return this.request('trigger.get', params);
}
getEvents(objectids, from, to, showEvents) {
var params = {
output: 'extend',
time_from: from,
time_till: to,
objectids: objectids,
select_acknowledges: 'extend',
selectHosts: 'extend',
value: showEvents
};
return this.request('event.get', params);
}
getAcknowledges(eventids) {
var params = {
output: 'extend',
eventids: eventids,
preservekeys: true,
select_acknowledges: 'extend',
sortfield: 'clock',
sortorder: 'DESC'
};
return this.request('event.get', params)
.then(function (events) {
return _.filter(events, function(event) {
return event.acknowledges.length;
});
});
}
}
return ZabbixAPI;
}
function isNotAuthorized(message) {
return (
message === "Session terminated, re-login, please." ||
message === "Not authorised." ||
message === "Not authorized."
);
}
angular
.module('grafana.services')
.factory('zabbixAPIService', ZabbixAPIService);

View File

@@ -0,0 +1,104 @@
/**
* General Zabbix API methods
*/
import angular from 'angular';
class ZabbixAPICoreService {
/** @ngInject */
constructor($q, backendSrv) {
this.$q = $q;
this.backendSrv = backendSrv;
}
/**
* Request data from Zabbix API
* @return {object} response.result
*/
request(api_url, method, params, options, auth) {
var deferred = this.$q.defer();
var requestData = {
jsonrpc: '2.0',
method: method,
params: params,
id: 1
};
if (auth === "") {
// Reject immediately if not authenticated
deferred.reject({data: "Not authorised."});
return deferred.promise;
} else if (auth) {
// Set auth parameter only if it needed
requestData.auth = auth;
}
var requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
url: api_url,
data: requestData
};
// Set request options for basic auth
if (options.basicAuth || options.withCredentials) {
requestOptions.withCredentials = true;
}
if (options.basicAuth) {
requestOptions.headers.Authorization = options.basicAuth;
}
this.backendSrv.datasourceRequest(requestOptions).then(function (response) {
// General connection issues
if (!response.data) {
deferred.reject(response);
}
// Handle Zabbix API errors
else if (response.data.error) {
deferred.reject(response.data.error);
}
deferred.resolve(response.data.result);
});
return deferred.promise;
}
/**
* Get authentication token.
* @return {string} auth token
*/
login(api_url, username, password, options) {
var params = {
user: username,
password: password
};
return this.request(api_url, 'user.login', params, options, null);
}
/**
* Get Zabbix API version
* Matches the version of Zabbix starting from Zabbix 2.0.4
*/
getVersion(api_url, options) {
return this.request(api_url, 'apiinfo.version', [], options);
}
}
// Define zabbix API exception type
function ZabbixException(error) {
this.code = error.code;
this.errorType = error.message;
this.message = error.data;
}
ZabbixException.prototype.toString = function() {
return this.errorType + ": " + this.message;
};
angular
.module('grafana.services')
.service('zabbixAPICoreService', ZabbixAPICoreService);

View File

@@ -0,0 +1,236 @@
import angular from 'angular';
import _ from 'lodash';
import * as utils from './utils';
// Use factory() instead service() for multiple datasources support.
// Each datasource instance must initialize its own cache.
/** @ngInject */
angular.module('grafana.services').factory('ZabbixCachingProxy', function($q, $interval) {
class ZabbixCachingProxy {
constructor(zabbixAPI, ttl) {
this.zabbixAPI = zabbixAPI;
this.ttl = ttl;
this.$q = $q;
// Internal objects for data storing
this._groups = undefined;
this._hosts = undefined;
this._applications = undefined;
this._items = undefined;
this.storage = {
history: {},
trends: {}
};
// Check is a service initialized or not
this._initialized = undefined;
this.refreshPromise = false;
this.historyPromises = {};
// Wrap _refresh() method to call it once.
this.refresh = callOnce(this._refresh, this.refreshPromise);
// Update cache periodically
$interval(_.bind(this.refresh, this), this.ttl);
// Don't run duplicated history requests
this.getHistory = callHistoryOnce(_.bind(this.zabbixAPI.getHistory, this.zabbixAPI),
this.historyPromises);
// Don't run duplicated requests
this.groupPromises = {};
this.getGroupsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getGroups, this.zabbixAPI),
this.groupPromises);
this.hostPromises = {};
this.getHostsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getHosts, this.zabbixAPI),
this.hostPromises);
this.appPromises = {};
this.getAppsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getApps, this.zabbixAPI),
this.appPromises);
this.itemPromises = {};
this.getItemsOnce = callAPIRequestOnce(_.bind(this.zabbixAPI.getItems, this.zabbixAPI),
this.itemPromises);
}
_refresh() {
var self = this;
var promises = [
this.zabbixAPI.getGroups()
];
return this.$q.all(promises).then(function(results) {
if (results.length) {
self._groups = results[0];
}
self._initialized = true;
});
}
getGroups() {
var self = this;
if (this._groups) {
return this.$q.when(self._groups);
} else {
return this.getGroupsOnce()
.then(groups => {
self._groups = groups;
return self._groups;
});
}
}
getHosts(groupids) {
var self = this;
return this.getHostsOnce(groupids)
.then(hosts => {
self._hosts = _.union(self._hosts, hosts);
return hosts;
});
}
getApps(hostids) {
return this.getAppsOnce(hostids)
.then(apps => {
return apps;
});
}
getItems(hostids, appids) {
var self = this;
return this.getItemsOnce(hostids, appids)
.then(items => {
self._items = _.union(self._items, items);
return items;
});
}
getHistoryFromCache(items, time_from, time_till) {
var deferred = this.$q.defer();
var historyStorage = this.storage.history;
var full_history;
var expired = _.filter(_.indexBy(items, 'itemid'), function(item, itemid) {
return !historyStorage[itemid];
});
if (expired.length) {
this.zabbixAPI.getHistory(expired, time_from, time_till).then(function(history) {
var grouped_history = _.groupBy(history, 'itemid');
_.forEach(expired, function(item) {
var itemid = item.itemid;
historyStorage[itemid] = item;
historyStorage[itemid].time_from = time_from;
historyStorage[itemid].time_till = time_till;
historyStorage[itemid].history = grouped_history[itemid];
});
full_history = _.map(items, function(item) {
return historyStorage[item.itemid].history;
});
deferred.resolve(_.flatten(full_history, true));
});
} else {
full_history = _.map(items, function(item) {
return historyStorage[item.itemid].history;
});
deferred.resolve(_.flatten(full_history, true));
}
return deferred.promise;
}
getHistoryFromAPI(items, time_from, time_till) {
return this.zabbixAPI.getHistory(items, time_from, time_till);
}
getHost(hostid) {
return _.find(this._hosts, {'hostid': hostid});
}
getItem(itemid) {
return _.find(this._items, {'itemid': itemid});
}
}
function callAPIRequestOnce(func, promiseKeeper) {
return function() {
var hash = getAPIRequestHash(arguments);
var deferred = $q.defer();
if (!promiseKeeper[hash]) {
promiseKeeper[hash] = deferred.promise;
func.apply(this, arguments).then(function(result) {
deferred.resolve(result);
promiseKeeper[hash] = null;
});
} else {
return promiseKeeper[hash];
}
return deferred.promise;
};
}
function callHistoryOnce(func, promiseKeeper) {
return function() {
var itemids = _.map(arguments[0], 'itemid');
var stamp = itemids.join() + arguments[1] + arguments[2];
var hash = stamp.getHash();
var deferred = $q.defer();
if (!promiseKeeper[hash]) {
promiseKeeper[hash] = deferred.promise;
func.apply(this, arguments).then(function(result) {
deferred.resolve(result);
promiseKeeper[hash] = null;
});
} else {
return promiseKeeper[hash];
}
return deferred.promise;
};
}
function callOnce(func, promiseKeeper) {
return function() {
var deferred = $q.defer();
if (!promiseKeeper) {
promiseKeeper = deferred.promise;
func.apply(this, arguments).then(function(result) {
deferred.resolve(result);
promiseKeeper = null;
});
} else {
return promiseKeeper;
}
return deferred.promise;
};
}
return ZabbixCachingProxy;
});
function getAPIRequestHash(args) {
var requestStamp = _.map(args, arg => {
if (arg === undefined) {
return 'undefined';
} else {
return arg.toString();
}
}).join();
return requestStamp.getHash();
}
String.prototype.getHash = function() {
var hash = 0, i, chr, len;
if (this.length === 0) {
return hash;
}
for (i = 0, len = this.length; i < len; i++) {
chr = this.charCodeAt(i);
hash = ((hash << 5) - hash) + chr;
hash |= 0; // Convert to 32bit integer
}
return hash;
};

5
src/module.js Normal file
View File

@@ -0,0 +1,5 @@
import {ZabbixAppConfigCtrl} from './components/config';
export {
ZabbixAppConfigCtrl as ConfigCtrl
};

View File

@@ -0,0 +1,288 @@
<div class="editor-row">
<div class="section tight-form-container" style="margin-bottom: 20px">
<h5>Select triggers</h5>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
Group
</li>
<li>
<input type="text"
ng-model="editor.panel.triggers.group.filter"
bs-typeahead="editor.getGroupNames"
ng-change="editor.onTargetPartChange(editor.panel.triggers.group)"
ng-blur="editor.parseTarget()"
data-min-length=0
data-items=100
class="input-large tight-form-input"
ng-style="editor.panel.triggers.group.style">
</li>
<li class="tight-form-item" style="width: 50px">
Host
</li>
<li>
<input type="text"
ng-model="editor.panel.triggers.host.filter"
bs-typeahead="editor.getHostNames"
ng-change="editor.onTargetPartChange(editor.panel.triggers.host)"
ng-blur="editor.parseTarget()"
data-min-length=0
data-items=100
class="input-large tight-form-input last"
ng-style="editor.panel.triggers.host.style">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
Application
</li>
<li>
<input type="text"
ng-model="editor.panel.triggers.application.filter"
bs-typeahead="editor.getApplicationNames"
ng-change="editor.onTargetPartChange(editor.panel.triggers.application)"
ng-blur="editor.parseTarget()"
data-min-length=0
data-items=100
class="input-large tight-form-input"
ng-style="editor.panel.triggers.application.style">
</li>
<li class="tight-form-item" style="width: 50px">
Trigger
</li>
<li>
<input type="text"
ng-model="editor.panel.triggers.trigger.filter"
ng-change="editor.onTargetPartChange(editor.panel.triggers.trigger)"
ng-blur="editor.parseTarget()"
placeholder="trigger name"
class="input-large tight-form-input last"
ng-style="editor.panel.triggers.trigger.style"
empty-to-null>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<div class="section">
<h5>Data source</h5>
<div class="section tight-form-container" style="margin-bottom: 20px">
<div class="tight-form">
<ul class="tight-form-list">
<li>
<select class="tight-form-input input-large last"
ng-model="editor.panel.datasource"
ng-options="ds for ds in editor.datasources"
ng-change="editor.datasourceChanged()">
</select>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>Options</h5>
<div class="tight-form-container" style="margin-bottom: 20px">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
<strong>Acknowledged</strong>
</li>
<li>
<select class="input-medium tight-form-input"
ng-model="editor.panel.showTriggers"
ng-options="f for f in editor.ackFilters"
ng-change="editor.panelCtrl.refresh()">
</select>
</li>
<li class="tight-form-item" style="width: 13em">
<strong>Limit triggers number to</strong>
</li>
<li>
<input class="input-small tight-form-input"
type="number"
ng-model="editor.panel.limit"
ng-model-onblur
ng-change="editor.panelCtrl.refresh()">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
<strong>Sort by</strong>
</li>
<li>
<select class="input-medium tight-form-input"
ng-model="editor.panel.sortTriggersBy"
ng-options="f.text for f in editor.sortByFields track by f.value"
ng-change="editor.panelCtrl.refresh()">
</select>
</li>
<li class="tight-form-item" style="width: 13em">
<strong>Show events</strong>
</li>
<li>
<select class="tight-form-input input-medium"
ng-model="editor.panel.showEvents"
ng-options="f.text for f in editor.showEventsFields track by f.value"
ng-change="editor.panelCtrl.refresh()">
</select>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 100px">
<strong>Show fields</strong>
</li>
<li class="tight-form-item">
<label class="checkbox-label" for="hostField">Host</label>
<input class="cr1"
id="hostField"
type="checkbox"
ng-model="editor.panel.hostField"
ng-checked="editor.panel.hostField">
<label for="hostField" class="cr1"></label>
</li>
<li class="tight-form-item">
<label class="checkbox-label" for="statusField">Status</label>
<input class="cr1"
id="statusField"
type="checkbox"
ng-model="editor.panel.statusField"
ng-checked="editor.panel.statusField">
<label for="statusField" class="cr1"></label>
</li>
<li class="tight-form-item">
<label class="checkbox-label" for="severityField">Severity</label>
<input class="cr1"
id="severityField"
type="checkbox"
ng-model="editor.panel.severityField"
ng-checked="editor.panel.severityField">
<label for="severityField" class="cr1"></label>
</li>
<li class="tight-form-item">
<label class="checkbox-label" for="lastChangeField">Last change</label>
<input class="cr1"
id="lastChangeField"
type="checkbox"
ng-model="editor.panel.lastChangeField"
ng-checked="editor.panel.lastChangeField">
<label for="lastChangeField" class="cr1"></label>
</li>
<li class="tight-form-item">
<label class="checkbox-label" for="ageField">Age</label>
<input class="cr1"
id="ageField"
type="checkbox"
ng-model="editor.panel.ageField"
ng-checked="editor.panel.ageField">
<label for="ageField" class="cr1"></label>
</li>
<li class="tight-form-item last">
<label class="checkbox-label" for="infoField">Info</label>
<input class="cr1"
id="infoField"
type="checkbox"
ng-model="editor.panel.infoField"
ng-checked="editor.panel.infoField">
<label for="infoField" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item">
<strong>Custom Last change format</strong>
<label class="checkbox-label" for="customLastChangeFormat">&nbsp;</label>
<input class="cr1"
id="customLastChangeFormat"
type="checkbox"
ng-change="editor.panelCtrl.refresh()"
ng-model="editor.panel.customLastChangeFormat"
ng-checked="editor.panel.customLastChangeFormat">
<label for="customLastChangeFormat" class="cr1"></label>
</li>
<li ng-if="editor.panel.customLastChangeFormat">
<input type="text"
ng-model="editor.panel.lastChangeFormat"
ng-blur="editor.panelCtrl.refresh()"
placeholder="dddd, MMMM Do YYYY, h:mm:ss a"
class="tight-form-input"
style="width: 300px"
empty-to-null>
</li>
<li class="tight-form-item last" ng-if="editor.panel.customLastChangeFormat">
<a href="http://momentjs.com/docs/#/displaying/format/" target="_blank">
<i class="fa fa-question-circle"
bs-tooltip="'See moment.js dosc for time format.'">
</i>
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="section">
<h5>Customize triggers severity and colors</h5>
<div class="tight-form" ng-repeat="trigger in editor.panel.triggerSeverity">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 10px">
{{ trigger.priority }}
</li>
<li>
<input class="tight-form-input input-medium"
type="text"
empty-to-null
ng-model="trigger.severity"
style="color: white"
ng-style="{background: trigger.color}"
ng-model-onblur
ng-change="editor.panelCtrl.refresh()">
</li>
<li class="tight-form-item">
<spectrum-picker ng-model="trigger.color" ng-change="editor.panelCtrl.refresh()"></spectrum-picker>
</li>
<li class="tight-form-item last" style="width: 28px">
<label class="checkbox-label" for="{{ 'trigger-show-' + $index }}"></label>
<input class="cr1"
ng-attr-id="{{ 'trigger-show-' + $index }}"
type="checkbox"
ng-model="trigger.show"
ng-checked="trigger.show"
ng-change="editor.panelCtrl.refresh()">
<label for="{{ 'trigger-show-' + $index }}" class="cr1"></label>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item"
ng-style="{background:editor.panel.okEventColor}"
style="width: 160px; color: white">
<span style="padding-left: 25px"> OK event color </span>
</li>
<li class="tight-form-item">
<spectrum-picker
ng-model="editor.panel.okEventColor"
ng-change="editor.panelCtrl.refresh()">
</spectrum-picker>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@@ -0,0 +1,198 @@
/**
* Grafana-Zabbix
* Zabbix plugin for Grafana.
* http://github.com/alexanderzobnin/grafana-zabbix
*
* Trigger panel.
* This feature sponsored by CORE IT
* http://www.coreit.fr
*
* Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com
* Licensed under the Apache License, Version 2.0
*/
import _ from 'lodash';
import $ from 'jquery';
class TriggerPanelEditorCtrl{
/** @ngInject */
constructor($scope, $q, uiSegmentSrv, datasourceSrv, templateSrv, popoverSrv) {
$scope.editor = this;
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
this.datasourceSrv = datasourceSrv;
this.templateSrv = templateSrv;
this.popoverSrv = popoverSrv;
// Map functions for bs-typeahead
this.getGroupNames = _.partial(getMetricNames, this, 'groupList');
this.getHostNames = _.partial(getMetricNames, this, 'filteredHosts');
this.getApplicationNames = _.partial(getMetricNames, this, 'filteredApplications');
this.getItemNames = _.partial(getMetricNames, this, 'filteredItems');
this.ackFilters = [
'all triggers',
'unacknowledged',
'acknowledged'
];
this.sortByFields = [
{ text: 'last change', value: 'lastchange' },
{ text: 'severity', value: 'priority' }
];
this.showEventsFields = [
{ text: 'All', value: [0,1] },
{ text: 'OK', value: [0] },
{ text: 'Problems', value: 1 }
];
// Load scope defaults
var scopeDefaults = {
metric: {},
inputStyles: {},
oldTarget: _.cloneDeep(this.panel.triggers)
};
_.defaults(this, scopeDefaults);
var self = this;
// Get zabbix data sources
var datasources = _.filter(this.datasourceSrv.getMetricSources(), datasource => {
return datasource.meta.id === 'zabbix-datasource';
});
this.datasources = _.map(datasources, 'name');
// Set default datasource
if (!this.panel.datasource) {
this.panel.datasource = this.datasources[0];
}
// Load datasource
this.datasourceSrv.get(this.panel.datasource).then(function (datasource) {
self.datasource = datasource;
self.initFilters();
self.panelCtrl.refresh();
});
}
initFilters() {
this.filterGroups();
this.filterHosts();
this.filterApplications();
}
filterGroups() {
var self = this;
this.datasource.queryProcessor.getGroups().then(function(groups) {
self.metric.groupList = groups;
});
}
filterHosts() {
var self = this;
var groupFilter = this.templateSrv.replace(this.panel.triggers.group.filter);
this.datasource.queryProcessor.getHosts(groupFilter).then(function(hosts) {
self.metric.filteredHosts = hosts;
});
}
filterApplications() {
var self = this;
var groupFilter = this.templateSrv.replace(this.panel.triggers.group.filter);
var hostFilter = this.templateSrv.replace(this.panel.triggers.host.filter);
this.datasource.queryProcessor.getApps(groupFilter, hostFilter)
.then(function(apps) {
self.metric.filteredApplications = apps;
});
}
onTargetPartChange(targetPart) {
var regexStyle = {'color': '#CCA300'};
targetPart.isRegex = isRegex(targetPart.filter);
targetPart.style = targetPart.isRegex ? regexStyle : {};
}
parseTarget() {
this.initFilters();
var newTarget = _.cloneDeep(this.panel.triggers);
if (!_.isEqual(this.oldTarget, this.panel.triggers)) {
this.oldTarget = newTarget;
this.panelCtrl.refresh();
}
}
refreshTriggerSeverity() {
_.each(this.triggerList, function(trigger) {
trigger.color = this.panel.triggerSeverity[trigger.priority].color;
trigger.severity = this.panel.triggerSeverity[trigger.priority].severity;
});
this.panelCtrl.refresh();
}
datasourceChanged() {
this.panelCtrl.refresh();
}
changeTriggerSeverityColor(trigger, color) {
this.panel.triggerSeverity[trigger.priority].color = color;
this.refreshTriggerSeverity();
}
openTriggerColorSelector(event) {
var el = $(event.currentTarget);
var index = getTriggerIndexForElement(el);
var popoverScope = this.$new();
popoverScope.trigger = this.panel.triggerSeverity[index];
popoverScope.changeTriggerSeverityColor = this.changeTriggerSeverityColor;
this.popoverSrv.show({
element: el,
placement: 'top',
templateUrl: 'public/plugins/zabbix-app/panel-triggers/trigger.colorpicker.html',
scope: popoverScope
});
}
openOkEventColorSelector(event) {
var el = $(event.currentTarget);
var popoverScope = this.$new();
popoverScope.trigger = {color: this.panel.okEventColor};
popoverScope.changeTriggerSeverityColor = function(trigger, color) {
this.panel.okEventColor = color;
this.refreshTriggerSeverity();
};
this.popoverSrv.show({
element: el,
placement: 'top',
templateUrl: 'public/plugins/zabbix-app/panel-triggers/trigger.colorpicker.html',
scope: popoverScope
});
}
}
// Get list of metric names for bs-typeahead directive
function getMetricNames(scope, metricList) {
return _.uniq(_.map(scope.metric[metricList], 'name'));
}
function getTriggerIndexForElement(el) {
return el.parents('[data-trigger-index]').data('trigger-index');
}
function isRegex(str) {
// Pattern for testing regex
var regexPattern = /^\/(.*)\/([gmi]*)$/m;
return regexPattern.test(str);
}
export function triggerPanelEditor() {
return {
restrict: 'E',
scope: true,
templateUrl: 'public/plugins/zabbix-app/panel-triggers/editor.html',
controller: TriggerPanelEditorCtrl,
};
}

View File

@@ -0,0 +1,136 @@
<div class="triggers-panel-container">
<div class="triggers-panel-header-bg"></div>
<div class="triggers-panel-scroll">
<table class="triggers-panel-table">
<thead>
<tr>
<th ng-if="ctrl.panel.hostField" style="width: 15%">
<div class="triggers-panel-table-header-inner pointer">
Host
</div>
</th>
<th ng-if="ctrl.panel.statusField" style="width: 85px">
<div class="triggers-panel-table-header-inner pointer">Status</div>
</th>
<th ng-if="ctrl.panel.severityField" style="width: 120px">
<div class="triggers-panel-table-header-inner pointer">Severity</div>
</th>
<th>
<div class="triggers-panel-table-header-inner pointer">Issue</div>
</th>
<th ng-if="ctrl.panel.lastChangeField" style="width: 220px">
<div class="triggers-panel-table-header-inner pointer">Last change</div>
</th>
<th ng-if="ctrl.panel.ageField" style="width: 180px">
<div class="triggers-panel-table-header-inner pointer">Age</div>
</th>
<th ng-if="ctrl.panel.infoField" style="width: 100px">
<div class="triggers-panel-table-header-inner pointer">Info</div>
</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="trigger in ctrl.triggerList">
<td ng-if="ctrl.panel.hostField">
<div>
<span><strong>{{trigger.host}}</strong></span>
</div>
</td>
<td ng-if="ctrl.panel.statusField" style="background-color: {{trigger.color}}; color: white">
<div>
{{ctrl.triggerStatusMap[trigger.value]}}
</div>
</td>
<td ng-if="ctrl.panel.severityField" style="background-color: {{trigger.color}}; color: white">
<div>
{{trigger.severity}}
</div>
</td>
<td style="background-color: {{trigger.color}}; color: white">
<div>
{{trigger.description}}
<a ng-if="trigger.comments"
role="button"
ng-click="ctrl.switchComment(trigger)"
class="pointer"
style="float: right; padding-right: 8px"
bs-tooltip="'Show additional trigger description'"
data-placement="top">
<i class="fa fa-file-text-o"></i>
</a>
</div>
<!-- Trigger comments -->
<div class="collapse"
id="comments-{{trigger.triggerid}}"
ng-if="trigger.showComment">
<div>
<small>{{trigger.comments}}</small>
</div>
</div>
<!-- Trigger acknowledges -->
<div class="collapse"
id="acknowledges-{{trigger.triggerid}}"
ng-if="trigger.showAcknowledges">
<div style="padding-top: 12px;">
<table class="table table-condensed">
<thead>
<tr>
<th><small>Time</small></th>
<th><small>User</small></th>
<th><small>Comments</small></th>
</tr>
</thead>
<tbody>
<tr ng-repeat="ack in trigger.acknowledges">
<td>
<small>{{ack.time}}</small>
</td>
<td>
<small>{{ack.user}}</small>
</td>
<td>
<small>{{ack.message}}</small>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</td>
<td ng-if="ctrl.panel.lastChangeField">
{{trigger.lastchange}}
</td>
<td ng-if="ctrl.panel.ageField">
{{trigger.age}}
</td>
<td ng-if="ctrl.panel.infoField">
<!-- Trigger Url -->
<a ng-if="trigger.url"
href="{{trigger.url}}"
target="_blank">
<i class="fa fa-external-link"></i>
</a>
<!-- Trigger state -->
<span ng-if="trigger.state === '1'"
bs-tooltip="'{{trigger.error}}'">
<i class="fa fa-question-circle"></i>
</span>
<!-- Trigger acknowledges -->
<a ng-if="trigger.acknowledges"
role="button"
ng-click="ctrl.switchAcknowledges(trigger)"
bs-tooltip="'Acknowledges ({{trigger.acknowledges.length}})'">
<i class="fa fa-comments"></i>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="triggers-panel-footer"></div>

View File

@@ -0,0 +1,259 @@
/**
* Grafana-Zabbix
* Zabbix plugin for Grafana.
* http://github.com/alexanderzobnin/grafana-zabbix
*
* Trigger panel.
* This feature sponsored by CORE IT
* http://www.coreit.fr
*
* Copyright 2015 Alexander Zobnin alexanderzobnin@gmail.com
* Licensed under the Apache License, Version 2.0
*/
import _ from 'lodash';
import moment from 'moment';
import {MetricsPanelCtrl} from 'app/plugins/sdk';
import {triggerPanelEditor} from './editor';
import './css/panel_triggers.css!';
var defaultSeverity = [
{ priority: 0, severity: 'Not classified', color: '#B7DBAB', show: true },
{ priority: 1, severity: 'Information', color: '#82B5D8', show: true },
{ priority: 2, severity: 'Warning', color: '#E5AC0E', show: true },
{ priority: 3, severity: 'Average', color: '#C15C17', show: true },
{ priority: 4, severity: 'High', color: '#BF1B00', show: true },
{ priority: 5, severity: 'Disaster', color: '#890F02', show: true }
];
var panelDefaults = {
datasource: null,
triggers: {
group: {filter: ""},
host: {filter: ""},
application: {filter: ""},
trigger: {filter: ""}
},
hostField: true,
statusField: false,
severityField: false,
lastChangeField: true,
ageField: true,
infoField: true,
limit: 10,
showTriggers: 'all triggers',
sortTriggersBy: { text: 'last change', value: 'lastchange' },
showEvents: { text: 'Problems', value: '1' },
triggerSeverity: defaultSeverity,
okEventColor: 'rgba(0, 245, 153, 0.45)',
};
var triggerStatusMap = {
'0': 'OK',
'1': 'Problem'
};
var defaultTimeFormat = "DD MMM YYYY HH:mm:ss";
class TriggerPanelCtrl extends MetricsPanelCtrl {
/** @ngInject */
constructor($scope, $injector, $q, $element, datasourceSrv, templateSrv) {
super($scope, $injector);
this.datasourceSrv = datasourceSrv;
this.templateSrv = templateSrv;
this.triggerStatusMap = triggerStatusMap;
this.defaultTimeFormat = defaultTimeFormat;
// Load panel defaults
_.defaults(this.panel, panelDefaults);
this.triggerList = [];
this.refreshData();
}
/**
* Override onInitMetricsPanelEditMode() method from MetricsPanelCtrl.
* We don't need metric editor from Metrics Panel.
*/
onInitMetricsPanelEditMode() {
this.addEditorTab('Options', triggerPanelEditor, 2);
}
refresh() {
this.onMetricsPanelRefresh();
}
onMetricsPanelRefresh() {
// ignore fetching data if another panel is in fullscreen
if (this.otherPanelInFullscreenMode()) { return; }
// clear loading/error state
delete this.error;
this.loading = true;
this.setTimeQueryStart();
this.refreshData();
}
refreshData() {
var self = this;
// Load datasource
return this.datasourceSrv.get(this.panel.datasource).then(datasource => {
var zabbix = datasource.zabbixAPI;
var queryProcessor = datasource.queryProcessor;
var showEvents = self.panel.showEvents.value;
var triggerFilter = self.panel.triggers;
// Replace template variables
var groupFilter = self.templateSrv.replace(triggerFilter.group.filter);
var hostFilter = self.templateSrv.replace(triggerFilter.host.filter);
var appFilter = self.templateSrv.replace(triggerFilter.application.filter);
var buildQuery = queryProcessor.buildTriggerQuery(groupFilter, hostFilter, appFilter);
return buildQuery.then(query => {
return zabbix.getTriggers(query.groupids,
query.hostids,
query.applicationids,
showEvents)
.then(triggers => {
return _.map(triggers, trigger => {
var triggerObj = trigger;
// Format last change and age
trigger.lastchangeUnix = Number(trigger.lastchange);
var timestamp = moment.unix(trigger.lastchangeUnix);
if (self.panel.customLastChangeFormat) {
// User defined format
triggerObj.lastchange = timestamp.format(self.panel.lastChangeFormat);
} else {
triggerObj.lastchange = timestamp.format(self.defaultTimeFormat);
}
triggerObj.age = timestamp.fromNow(true);
// Set host that the trigger belongs
if (trigger.hosts.length) {
triggerObj.host = trigger.hosts[0].name;
}
// Set color
if (trigger.value === '1') {
// Problem state
triggerObj.color = self.panel.triggerSeverity[trigger.priority].color;
} else {
// OK state
triggerObj.color = self.panel.okEventColor;
}
triggerObj.severity = self.panel.triggerSeverity[trigger.priority].severity;
return triggerObj;
});
})
.then(triggerList => {
// Request acknowledges for trigger
var eventids = _.map(triggerList, trigger => {
return trigger.lastEvent.eventid;
});
return zabbix.getAcknowledges(eventids)
.then(events => {
// Map events to triggers
_.each(triggerList, trigger => {
var event = _.find(events, event => {
return event.eventid === trigger.lastEvent.eventid;
});
if (event) {
trigger.acknowledges = _.map(event.acknowledges, ack => {
var time = new Date(+ack.clock * 1000);
ack.time = time.toLocaleString();
ack.user = ack.alias + ' (' + ack.name + ' ' + ack.surname + ')';
return ack;
});
}
});
// Filter triggers by description
var triggerFilter = self.panel.triggers.trigger.filter;
if (triggerFilter) {
triggerList = filterTriggers(triggerList, triggerFilter);
}
// Filter acknowledged triggers
if (self.panel.showTriggers === 'unacknowledged') {
triggerList = _.filter(triggerList, trigger => {
return !trigger.acknowledges;
});
} else if (self.panel.showTriggers === 'acknowledged') {
triggerList = _.filter(triggerList, 'acknowledges');
} else {
triggerList = triggerList;
}
// Filter triggers by severity
triggerList = _.filter(triggerList, trigger => {
return self.panel.triggerSeverity[trigger.priority].show;
});
// Sort triggers
if (self.panel.sortTriggersBy.value === 'priority') {
triggerList = _.sortBy(triggerList, 'priority').reverse();
} else {
triggerList = _.sortBy(triggerList, 'lastchangeUnix').reverse();
}
// Limit triggers number
self.triggerList = _.first(triggerList, self.panel.limit);
this.setTimeQueryEnd();
this.loading = false;
});
});
});
});
}
switchComment(trigger) {
trigger.showComment = !trigger.showComment;
}
switchAcknowledges(trigger) {
trigger.showAcknowledges = !trigger.showAcknowledges;
}
}
TriggerPanelCtrl.templateUrl = 'panel-triggers/module.html';
function filterTriggers(triggers, triggerFilter) {
if (isRegex(triggerFilter)) {
return _.filter(triggers, function(trigger) {
return buildRegex(triggerFilter).test(trigger.description);
});
} else {
return _.filter(triggers, function(trigger) {
return trigger.description === triggerFilter;
});
}
}
function isRegex(str) {
// Pattern for testing regex
var regexPattern = /^\/(.*)\/([gmi]*)$/m;
return regexPattern.test(str);
}
function buildRegex(str) {
var regexPattern = /^\/(.*)\/([gmi]*)$/m;
var matches = str.match(regexPattern);
var pattern = matches[1];
var flags = matches[2] !== "" ? matches[2] : undefined;
return new RegExp(pattern, flags);
}
export {
TriggerPanelCtrl,
TriggerPanelCtrl as PanelCtrl
};

View File

@@ -0,0 +1,12 @@
{
"type": "panel",
"name": "Zabbix Triggers",
"id": "zabbix-triggers-panel",
"info": {
"author": {
"name": "Alexander Zobnin",
"url": "http://grafana-zabbix.org"
}
}
}

View File

@@ -0,0 +1,108 @@
$tight-form-func-bg: #333;
$blue: #33B5E5;
$dark-2: #1f1d1d;
$body-bg: rgb(20,20,20);
$grafanaListAccent: lighten($dark-2, 2%);
.triggers-panel-wrapper {
.panel-content {
padding: 0;
}
.panel-title-container {
padding-bottom: 4px;
}
}
.triggers-panel-scroll {
overflow: auto;
}
.triggers-panel-container {
padding-top: 2.2em;
position: relative;
}
.triggers-panel-footer {
text-align: center;
font-size: 90%;
line-height: 2px;
ul {
position: relative;
display: inline-block;
margin-left: 0;
margin-bottom: 0;
}
ul > li {
display: inline; // Remove list-style and block-level defaults
}
ul > li > a {
float: left; // Collapse white-space
padding: 4px 12px;
text-decoration: none;
border-left-width: 0;
&:hover {
background-color: $tight-form-func-bg;
}
&.active {
font-weight: bold;
color: $blue;
}
}
}
.triggers-panel-table {
width: 100%;
border-collapse: collapse;
th {
padding: 0;
&:first-child {
.triggers-panel-table-header-inner {
padding-left: 15px;
}
}
}
td {
padding: 0.45em 0 0.45em 1.1em;
border-bottom: 2px solid $body-bg;
border-right: 2px solid $body-bg;
&:first-child {
padding-left: 15px;
}
&:last-child {
border-right: none;
}
}
}
.triggers-panel-header-bg {
background: $grafanaListAccent;
border-top: 2px solid $body-bg;
border-bottom: 2px solid $body-bg;
height: 2.0em;
position: absolute;
top: 0;
right: 0;
left: 0;
}
.triggers-panel-table-header-inner {
padding: 0.45em 0 0.45em 1.1em;
text-align: left;
color: $blue;
position: absolute;
top: 0;
}
.triggers-panel-width-hack {
visibility: hidden;
height: 0px;
line-height: 0px;
}

View File

@@ -0,0 +1,13 @@
<div class="graph-legend-popover">
<a class="close"
href=""
ng-click="dismiss();">×</a>
<div class="editor-row">
<i ng-repeat="color in colors" class="pointer"
ng-class="{'fa fa-circle-o': color === trigger.color,'fa fa-circle': color !== trigger.color}"
ng-style="{color:color}"
ng-click="changeTriggerSeverityColor(trigger, color);dismiss();">&nbsp;</i>
</div>
</div>

47
src/plugin.json Normal file
View File

@@ -0,0 +1,47 @@
{
"type": "app",
"name": "Zabbix App",
"id": "zabbix-app",
"css": {
"dark": "css/dark.css",
"light": "css/light.css"
},
"info": {
"description": "Zabbix plugin for Grafana",
"author": {
"name": "Alexander Zobnin",
"url": "http://grafana-zabbix.org"
},
"keywords": ["zabbix"],
"links": [
{"name": "Project site", "url": "https://github.com/alexanderzobnin/grafana-zabbix"},
{"name": "License & Terms", "url": "https://github.com/alexanderzobnin/grafana-zabbix/blob/master/LICENSE.md"}
],
"version": "1.0.0-beta1",
"updated": "2016-03-31"
},
"includes": [
{
"type": "datasource",
"name": "Zabbix Datasource"
},
{
"type": "panel",
"name": "Triggers Panel"
},
{
"type": "dashboard",
"name": "Zabbix Server Dashboard",
"path": "dashboards/zabbix_server_dashboard.json",
"addToNav": true
}
],
"dependencies": {
"grafanaVersion": "3.x.x",
"plugins": []
}
}

View File

@@ -1,398 +0,0 @@
define([
'angular',
'lodash',
'app/core/utils/datemath',
'./directives',
'./zabbixAPIWrapper',
'./helperFunctions',
'./queryCtrl'
],
function (angular, _, dateMath) {
'use strict';
var module = angular.module('grafana.services');
module.factory('ZabbixAPIDatasource', function($q, backendSrv, templateSrv, alertSrv, ZabbixAPI, zabbixHelperSrv) {
/**
* Datasource initialization. Calls when you refresh page, add
* or modify datasource.
*
* @param {Object} datasource Grafana datasource object.
*/
function ZabbixAPIDatasource(datasource) {
this.name = datasource.name;
this.url = datasource.url;
this.basicAuth = datasource.basicAuth;
this.withCredentials = datasource.withCredentials;
if (datasource.jsonData) {
this.username = datasource.jsonData.username;
this.password = datasource.jsonData.password;
// Use trends instead history since specified time
this.trends = datasource.jsonData.trends;
this.trendsFrom = datasource.jsonData.trendsFrom || '7d';
// Limit metrics per panel for templated request
this.limitmetrics = datasource.jsonData.limitMetrics || 100;
} else {
// DEPRECATED. Loads settings from plugin.json file.
// For backward compatibility only.
this.username = datasource.meta.username;
this.password = datasource.meta.password;
this.trends = datasource.meta.trends;
this.trendsFrom = datasource.meta.trendsFrom || '7d';
this.limitmetrics = datasource.meta.limitmetrics || 100;
}
// Initialize Zabbix API
this.zabbixAPI = new ZabbixAPI(this.url, this.username, this.password, this.basicAuth, this.withCredentials);
}
/**
* Test connection to Zabbix API
*
* @return {object} Connection status and Zabbix API version
*/
ZabbixAPIDatasource.prototype.testDatasource = function() {
var self = this;
return this.zabbixAPI.getZabbixAPIVersion().then(function (apiVersion) {
return self.zabbixAPI.performZabbixAPILogin().then(function (auth) {
if (auth) {
return {
status: "success",
title: "Success",
message: "Zabbix API version: " + apiVersion
};
} else {
return {
status: "error",
title: "Invalid user name or password",
message: "Zabbix API version: " + apiVersion
};
}
});
}, function(error) {
return {
status: "error",
title: "Connection failed",
message: "Could not connect to " + error.config.url
};
});
};
/**
* Calls for each panel in dashboard.
*
* @param {Object} options Query options. Contains time range, targets
* and other info.
*
* @return {Object} Grafana metrics object with timeseries data
* for each target.
*/
ZabbixAPIDatasource.prototype.query = function(options) {
// get from & to in seconds
var from = Math.ceil(dateMath.parse(options.range.from) / 1000);
var to = Math.ceil(dateMath.parse(options.range.to) / 1000);
var useTrendsFrom = Math.ceil(dateMath.parse('now-' + this.trendsFrom) / 1000);
// Create request for each target
var promises = _.map(options.targets, function(target) {
if (target.mode !== 1) {
// Don't show undefined and hidden targets
if (target.hide || !target.group || !target.host
|| !target.application || !target.item) {
return [];
}
// Replace templated variables
var groupname = templateSrv.replace(target.group.name, options.scopedVars);
var hostname = templateSrv.replace(target.host.name, options.scopedVars);
var appname = templateSrv.replace(target.application.name, options.scopedVars);
var itemname = templateSrv.replace(target.item.name, options.scopedVars);
// Extract zabbix groups, hosts and apps from string:
// "{host1,host2,...,hostN}" --> [host1, host2, ..., hostN]
var groups = zabbixHelperSrv.splitMetrics(groupname);
var hosts = zabbixHelperSrv.splitMetrics(hostname);
var apps = zabbixHelperSrv.splitMetrics(appname);
// Remove hostnames from item names and then
// extract item names
// "hostname: itemname" --> "itemname"
var delete_hostname_pattern = /(?:\[[\w\.]+]:\s)/g;
var itemnames = zabbixHelperSrv.splitMetrics(itemname.replace(delete_hostname_pattern, ''));
var self = this;
// Query numeric data
if (!target.mode) {
// Find items by item names and perform queries
return this.zabbixAPI.itemFindQuery(groups, hosts, apps)
.then(function (items) {
// Filter hosts by regex
if (target.host.visible_name === 'All') {
if (target.hostFilter && _.every(items, _.identity.hosts)) {
// Use templated variables in filter
var host_pattern = new RegExp(templateSrv.replace(target.hostFilter, options.scopedVars));
items = _.filter(items, function (item) {
return _.some(item.hosts, function (host) {
return host_pattern.test(host.name);
});
});
}
}
if (itemnames[0] === 'All') {
// Filter items by regex
if (target.itemFilter) {
// Use templated variables in filter
var item_pattern = new RegExp(templateSrv.replace(target.itemFilter, options.scopedVars));
return _.filter(items, function (item) {
return item_pattern.test(zabbixHelperSrv.expandItemName(item));
});
} else {
return items;
}
} else {
// Filtering items
return _.filter(items, function (item) {
return _.contains(itemnames, zabbixHelperSrv.expandItemName(item));
});
}
}).then(function (items) {
// Don't perform query for high number of items
// to prevent Grafana slowdown
if (items.length > self.limitmetrics) {
var message = "Try to increase limitmetrics parameter in datasource config.<br>"
+ "Current limitmetrics value is " + self.limitmetrics;
alertSrv.set("Metrics limit exceeded", message, "warning", 10000);
return [];
} else {
items = _.flatten(items);
// Use alias only for single metric, otherwise use item names
var alias = target.item.name === 'All' || itemnames.length > 1 ?
undefined : templateSrv.replace(target.alias, options.scopedVars);
var history;
if ((from < useTrendsFrom) && self.trends) {
var points = target.downsampleFunction ? target.downsampleFunction.value : "avg";
history = self.zabbixAPI.getTrends(items, from, to)
.then(_.bind(zabbixHelperSrv.handleTrendResponse, zabbixHelperSrv, items, alias, target.scale, points));
} else {
history = self.zabbixAPI.getHistory(items, from, to)
.then(_.bind(zabbixHelperSrv.handleHistoryResponse, zabbixHelperSrv, items, alias, target.scale));
}
return history.then(function (timeseries) {
var timeseries_data = _.flatten(timeseries);
return _.map(timeseries_data, function (timeseries) {
// Series downsampling
if (timeseries.datapoints.length > options.maxDataPoints) {
var ms_interval = Math.floor((to - from) / options.maxDataPoints) * 1000;
var downsampleFunc = target.downsampleFunction ? target.downsampleFunction.value : "avg";
timeseries.datapoints = zabbixHelperSrv.downsampleSeries(timeseries.datapoints, to, ms_interval, downsampleFunc);
}
return timeseries;
});
});
}
});
}
// Query text data
else if (target.mode === 2) {
// Find items by item names and perform queries
return this.zabbixAPI.itemFindQuery(groups, hosts, apps, "text")
.then(function (items) {
items = _.filter(items, function (item) {
return _.contains(itemnames, zabbixHelperSrv.expandItemName(item));
});
return self.zabbixAPI.getHistory(items, from, to).then(function(history) {
return {
target: target.item.name,
datapoints: _.map(history, function (p) {
return [p.value, p.clock * 1000];
})
};
});
});
}
}
// IT services mode
else if (target.mode === 1) {
// Don't show undefined and hidden targets
if (target.hide || !target.itservice || !target.slaProperty) {
return [];
} else {
return this.zabbixAPI.getSLA(target.itservice.serviceid, from, to)
.then(_.bind(zabbixHelperSrv.handleSLAResponse, zabbixHelperSrv, target.itservice, target.slaProperty));
}
}
}, this);
return $q.all(_.flatten(promises)).then(function (results) {
var timeseries_data = _.flatten(results);
return { data: timeseries_data };
});
};
////////////////
// Templating //
////////////////
/**
* Find metrics from templated request.
*
* @param {string} query Query from Templating
* @return {string} Metric name - group, host, app or item or list
* of metrics in "{metric1,metcic2,...,metricN}" format.
*/
ZabbixAPIDatasource.prototype.metricFindQuery = function (query) {
// Split query. Query structure:
// group.host.app.item
var parts = [];
_.each(query.split('.'), function (part) {
part = templateSrv.replace(part);
if (part[0] === '{') {
// Convert multiple mettrics to array
// "{metric1,metcic2,...,metricN}" --> [metric1, metcic2,..., metricN]
parts.push(zabbixHelperSrv.splitMetrics(part));
} else {
parts.push(part);
}
});
var template = _.object(['group', 'host', 'app', 'item'], parts);
// Get items
if (parts.length === 4) {
return this.zabbixAPI.itemFindQuery(template.group, template.host, template.app)
.then(function (result) {
return _.map(result, function (item) {
var itemname = zabbixHelperSrv.expandItemName(item);
return {
text: itemname,
expandable: false
};
});
});
}
// Get applications
else if (parts.length === 3) {
return this.zabbixAPI.appFindQuery(template.host, template.group).then(function (result) {
return _.map(result, function (app) {
return {
text: app.name,
expandable: false
};
});
});
}
// Get hosts
else if (parts.length === 2) {
return this.zabbixAPI.hostFindQuery(template.group).then(function (result) {
return _.map(result, function (host) {
return {
text: host.name,
expandable: false
};
});
});
}
// Get groups
else if (parts.length === 1) {
return this.zabbixAPI.getGroupByName(template.group).then(function (result) {
return _.map(result, function (hostgroup) {
return {
text: hostgroup.name,
expandable: false
};
});
});
}
// Return empty object for invalid request
else {
var d = $q.defer();
d.resolve([]);
return d.promise;
}
};
/////////////////
// Annotations //
/////////////////
ZabbixAPIDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) {
var from = Math.ceil(dateMath.parse(rangeUnparsed.from) / 1000);
var to = Math.ceil(dateMath.parse(rangeUnparsed.to) / 1000);
var self = this;
var params = {
output: ['triggerid', 'description'],
search: {
'description': annotation.trigger
},
searchWildcardsEnabled: true,
expandDescription: true
};
if (annotation.host) {
params.host = templateSrv.replace(annotation.host);
}
else if (annotation.group) {
params.group = templateSrv.replace(annotation.group);
}
return this.zabbixAPI.performZabbixAPIRequest('trigger.get', params)
.then(function (result) {
if(result) {
var objects = _.indexBy(result, 'triggerid');
var params = {
output: 'extend',
time_from: from,
time_till: to,
objectids: _.keys(objects),
select_acknowledges: 'extend'
};
// Show problem events only
if (!annotation.showOkEvents) {
params.value = 1;
}
return self.zabbixAPI.performZabbixAPIRequest('event.get', params)
.then(function (result) {
var events = [];
_.each(result, function(e) {
var formatted_acknowledges = zabbixHelperSrv.formatAcknowledges(e.acknowledges);
events.push({
annotation: annotation,
time: e.clock * 1000,
title: Number(e.value) ? 'Problem' : 'OK',
text: objects[e.objectid].description + formatted_acknowledges
});
});
return events;
});
} else {
return [];
}
});
};
return ZabbixAPIDatasource;
});
});

View File

@@ -1,21 +0,0 @@
define([
'angular'
],
function (angular) {
'use strict';
var module = angular.module('grafana.directives');
module.directive('metricQueryEditorZabbix', function() {
return {controller: 'ZabbixAPIQueryCtrl', templateUrl: 'app/plugins/datasource/zabbix/partials/query.editor.html'};
});
module.directive('metricQueryOptionsZabbix', function() {
return {templateUrl: 'app/plugins/datasource/zabbix/partials/query.options.html'};
});
module.directive('annotationsQueryEditorZabbix', function() {
return {templateUrl: 'app/plugins/datasource/zabbix/partials/annotations.editor.html'};
});
});

View File

@@ -1,283 +0,0 @@
define([
'angular',
'lodash'
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.services');
module.service('zabbixHelperSrv', function($q) {
var self = this;
/**
* Convert Zabbix API history.get response to Grafana format
*
* @param {Array} items Array of Zabbix Items
* @param alias
* @param scale
* @param {Array} history Array of Zabbix History
*
* @return {Array} Array of timeseries in Grafana format
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* }
*/
this.handleHistoryResponse = function(items, alias, scale, history) {
/**
* Response should be in the format:
* data: [
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* },
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* },
* ]
*/
// Group items and history by itemid
var indexed_items = _.indexBy(items, 'itemid');
var grouped_history = _.groupBy(history, 'itemid');
var self = this;
return $q.when(_.map(grouped_history, function (history, itemid) {
var item = indexed_items[itemid];
return {
target: (item.hosts ? item.hosts[0].name+': ' : '')
+ (alias ? alias : self.expandItemName(item)),
datapoints: _.map(history, function (p) {
// Value must be a number for properly work
var value = Number(p.value);
// Apply scale
if (scale) {
value *= scale;
}
return [value, p.clock * 1000];
})
};
})).then(function (result) {
return _.sortBy(result, 'target');
});
};
/**
* Convert Zabbix API trends.get response to Grafana format
*
* @param {Array} items Array of Zabbix Items
* @param alias
* @param scale
* @param {string} points Point value to return: min, max or avg
* @param {Array} trends Array of Zabbix Trends
*
* @return {Array} Array of timeseries in Grafana format
* {
* target: "Metric name",
* datapoints: [[<value>, <unixtime>], ...]
* }
*/
this.handleTrendResponse = function (items, alias, scale, points, trends) {
// Group items and trends by itemid
var indexed_items = _.indexBy(items, 'itemid');
var grouped_trends = _.groupBy(trends, 'itemid');
var self = this;
return $q.when(_.map(grouped_trends, function (trends, itemid) {
var item = indexed_items[itemid];
return {
target: (item.hosts ? item.hosts[0].name+': ' : '')
+ (alias ? alias : self.expandItemName(item)),
datapoints: _.map(trends, function (p) {
// Value must be a number for properly work
var value;
if (points === "min") {
value = Number(p.value_min);
}
else if (points === "max") {
value = Number(p.value_max);
}
else {
value = Number(p.value_avg);
}
// Apply scale
if (scale) {
value *= scale;
}
return [value, p.clock * 1000];
})
};
})).then(function (result) {
return _.sortBy(result, 'target');
});
};
/**
* Convert Zabbix API service.getsla response to Grafana format
*
* @param itservice
* @param slaProperty
* @param slaObject
* @returns {{target: *, datapoints: *[]}}
*/
this.handleSLAResponse = function (itservice, slaProperty, slaObject) {
var targetSLA = slaObject[itservice.serviceid].sla[0];
if (slaProperty.property === 'status') {
var targetStatus = slaObject[itservice.serviceid].status;
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetStatus, targetSLA.to * 1000]
]
};
} else {
return {
target: itservice.name + ' ' + slaProperty.name,
datapoints: [
[targetSLA[slaProperty.property], targetSLA.from * 1000],
[targetSLA[slaProperty.property], targetSLA.to * 1000]
]
};
}
};
/**
* Expand item parameters, for example:
* CPU $2 time ($3) --> CPU system time (avg1)
*
* @param item: zabbix api item object
* @return {string} expanded item name (string)
*/
this.expandItemName = function(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;
};
/**
* Convert multiple mettrics to array
* "{metric1,metcic2,...,metricN}" --> [metric1, metcic2,..., metricN]
*
* @param {string} metrics "{metric1,metcic2,...,metricN}"
* @return {Array} [metric1, metcic2,..., metricN]
*/
this.splitMetrics = function(metrics) {
var remove_brackets_pattern = /^{|}$/g;
var metric_split_pattern = /,(?!\s)/g;
return metrics.replace(remove_brackets_pattern, '').split(metric_split_pattern);
};
/**
* Convert Date object to local time in format
* YYYY-MM-DD HH:mm:ss
*
* @param {Date} date Date object
* @return {string} formatted local time YYYY-MM-DD HH:mm:ss
*/
this.getShortTime = function(date) {
var MM = date.getMonth() < 10 ? '0' + date.getMonth() : date.getMonth();
var DD = date.getDate() < 10 ? '0' + date.getDate() : date.getDate();
var HH = date.getHours() < 10 ? '0' + date.getHours() : date.getHours();
var mm = date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes();
var ss = date.getSeconds() < 10 ? '0' + date.getSeconds() : date.getSeconds();
return date.getFullYear() + '-' + MM + '-' + DD + ' ' + HH + ':' + mm + ':' + ss;
};
/**
* Format acknowledges.
*
* @param {array} acknowledges array of Zabbix acknowledge objects
* @return {string} HTML-formatted table
*/
this.formatAcknowledges = function(acknowledges) {
if (acknowledges.length) {
var formatted_acknowledges = '<br><br>Acknowledges:<br><table><tr><td><b>Time</b></td>'
+ '<td><b>User</b></td><td><b>Comments</b></td></tr>';
_.each(_.map(acknowledges, function (ack) {
var time = new Date(ack.clock * 1000);
return '<tr><td><i>' + self.getShortTime(time) + '</i></td><td>' + ack.alias
+ ' (' + ack.name + ' ' + ack.surname + ')' + '</td><td>' + ack.message + '</td></tr>';
}), function (ack) {
formatted_acknowledges = formatted_acknowledges.concat(ack);
});
formatted_acknowledges = formatted_acknowledges.concat('</table>');
return formatted_acknowledges;
} else {
return '';
}
};
/**
* Downsample datapoints series
*
* @param {Object[]} datapoints [[<value>, <unixtime>], ...]
* @param {integer} time_to Panel time to
* @param {integer} ms_interval Interval in milliseconds for grouping datapoints
* @param {string} func Value to return: min, max or avg
* @return {Object[]} [[<value>, <unixtime>], ...]
*/
this.downsampleSeries = function(datapoints, time_to, ms_interval, func) {
var downsampledSeries = [];
var timeWindow = {
from: time_to * 1000 - ms_interval,
to: time_to * 1000
};
var points_sum = 0;
var points_num = 0;
var value_avg = 0;
var frame = [];
for (var i = datapoints.length - 1; i >= 0; i -= 1) {
if (timeWindow.from < datapoints[i][1] && datapoints[i][1] <= timeWindow.to) {
points_sum += datapoints[i][0];
points_num++;
frame.push(datapoints[i][0]);
}
else {
value_avg = points_num ? points_sum / points_num : 0;
if (func === "max") {
downsampledSeries.push([_.max(frame), timeWindow.to]);
}
else if (func === "min") {
downsampledSeries.push([_.min(frame), timeWindow.to]);
}
// avg by default
else {
downsampledSeries.push([value_avg, timeWindow.to]);
}
// Shift time window
timeWindow.to = timeWindow.from;
timeWindow.from -= ms_interval;
points_sum = 0;
points_num = 0;
frame = [];
// Process point again
i++;
}
}
return downsampledSeries.reverse();
};
});
});

View File

@@ -1,36 +0,0 @@
<div class="editor-row">
<div class="section">
<h5>Zabbix trigger
</h5>
</div>
</div>
<div class="editor-row">
<div class="editor-option">
<label class="small">Group</label>
<input type="text" class="input-medium" ng-model='currentAnnotation.group'></input>
</div>
<div class="editor-option">
<label class="small">Host</label>
<input type="text" class="input-medium" ng-model='currentAnnotation.host'></input>
</div>
<div class="editor-option">
<label class="small">Trigger
<tip>Trigger name for search. Wildcards are supports. Examples: Lack of free swap space, Lack of*.
</tip>
</label>
<input type="text" style="width: 25em" ng-model='currentAnnotation.trigger' placeholder="Trigger name"></input>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>Options</h5>
<input type="checkbox" class="cr1" id="currentAnnotation.showOkEvents"
ng-model="currentAnnotation.showOkEvents"
ng-checked="currentAnnotation.showOkEvents">
<label for="currentAnnotation.showOkEvents" class="cr1">Show OK events
<tip>Show events, generated when trigger release to OK state</tip>
</label>
</div>
</div>

View File

@@ -1,63 +0,0 @@
<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.jsonData.username'
placeholder="">
</li>
<li class="tight-form-item">
Password
</li>
<li>
<input type="password" class="tight-form-input input-large"
ng-model='current.jsonData.password'
placeholder="">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
Trends
</li>
<li class="tight-form-item">
Enable&nbsp;
<input class="cr1" id="current.jsonData.trends" type="checkbox"
ng-model="current.jsonData.trends"
ng-checked="current.jsonData.trends">
<label for="current.jsonData.trends" class="cr1"></label>
</li>
<li class="tight-form-item" ng-if="current.jsonData.trends">
Use trends from
</li>
<li ng-if="current.jsonData.trends">
<input type="text" class="tight-form-input input-small"
ng-model='current.jsonData.trendsFrom'
placeholder="7d">
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 80px">
Metrics limit
</li>
<li>
<input type="text" class="tight-form-input input-small"
ng-model='current.jsonData.limitMetrics'
placeholder="100">
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

@@ -1,227 +0,0 @@
<div class="tight-form">
<ul class="tight-form-list pull-right">
<li class="tight-form-item small" ng-show="target.datasource">
<em>{{target.datasource}}</em>
</li>
<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">
<!-- Switch editor mode -->
<li role="menuitem" ng-show="target.mode">
<a class="pointer" tabindex="1"
ng-click="switchEditorMode(0)">Numeric metrics</a>
</li>
<li role="menuitem" ng-show="target.mode != 1">
<a class="pointer" tabindex="1"
ng-click="switchEditorMode(1)">IT services</a>
</li>
<li role="menuitem" ng-show="target.mode != 2">
<a class="pointer" tabindex="1"
ng-click="switchEditorMode(2)">Text metrics</a>
</li>
<li class="divider" role="menuitem"></li>
<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">
{{target.refId}}
</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>
<!-- IT Service editor -->
<ul class="tight-form-list" role="menu" ng-show="target.mode == 1">
<li class="tight-form-item input-small">IT Service</li>
<li>
<select class="tight-form-input input-large"
ng-change="selectITService()"
ng-model="target.itservice"
bs-tooltip="target.itservice.name.length > 25 ? target.itservice.name : ''"
ng-options="itservice.name for itservice in itserviceList track by itservice.name">
<option value="">-- Select IT service --</option>
</select>
</li>
<li class="tight-form-item input-medium">IT service property</li>
<li>
<select class="tight-form-input input-medium"
ng-change="selectITService()"
ng-model="target.slaProperty"
ng-options="slaProperty.name for slaProperty in slaPropertyList track by slaProperty.name">
<option value="">-- Property --</option>
</select>
</li>
</ul>
<ul class="tight-form-list" role="menu" ng-hide="target.mode == 1">
<!-- Alias -->
<li>
<input type="text"
class="tight-form-input input-medium"
ng-model="target.alias"
spellcheck='false'
placeholder="Alias"
ng-blur="targetBlur()">
</li>
<!-- Select Host Group -->
<li class="tight-form-item input-small" style="width: 5em">Group</li>
<li>
<select class="tight-form-input input-large"
ng-change="selectHostGroup()"
ng-model="target.group"
bs-tooltip="target.group.name.length > 25 ? target.group.name : ''"
ng-options="group.visible_name ? group.visible_name : group.name for group in metric.groupList track by group.name">
<option value="">-- Select host group --</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 class="tight-form-item input-small" style="width: 3em">Host</li>
<li>
<select class="tight-form-input input-large"
ng-change="selectHost()"
ng-model="target.host"
bs-tooltip="target.host.name.length > 25 ? target.host.name : ''"
ng-options="host.visible_name ? host.visible_name : host.name for host in metric.hostList track by host.name">
<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>
<!-- Host filter -->
<li class="tight-form-item" ng-hide="target.mode == 2">
Filter
<i class="fa fa-question-circle"
bs-tooltip="'Filtering hosts by regex. Select All in items and specify regex for host names.'"></i>
</li>
<li ng-hide="target.mode == 2">
<input type="text"
class="tight-form-input input-large"
ng-model="target.hostFilter"
spellcheck='false'
placeholder="Host filter (regex)"
ng-blur="targetBlur()">
</li>
<!-- Downsampling function -->
<li class="tight-form-item input-medium" ng-hide="target.mode == 2">
Downsampling
</li>
<li ng-hide="target.mode == 2">
<select class="tight-form-input input-small"
ng-change="targetBlur()"
ng-model="target.downsampleFunction"
bs-tooltip="'Downsampling function'"
ng-options="func.name for func in downsampleFunctionList track by func.name">
</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 class="tight-form" ng-hide="target.mode == 1">
<ul class="tight-form-list" role="menu">
<li class="tight-form-item" style="min-width: 15px; text-align: center">&nbsp</li>
<li class="tight-form-item">
<i class="fa fa-eye invisible"></i>
</li>
<li class="tight-form-item" style="width: 135px">&nbsp</li>
<!-- Select Application -->
<li class="tight-form-item input-small" style="width: 5em">Application</li>
<li>
<select class="tight-form-input input-large"
ng-change="selectApplication()"
ng-model="target.application"
bs-tooltip="target.application.name.length > 15 ? target.application.name : ''"
ng-options="app.visible_name ? app.visible_name : app.name for app in metric.applicationList track by app.name">
<option value="">-- Select application --</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 class="tight-form-item input-small" style="width: 3em">Item</li>
<li>
<select class="tight-form-input input-large"
ng-change="selectItem()"
ng-model="target.item"
bs-tooltip="target.item.name.length > 25 ? target.item.name : ''"
ng-options="item.name for item in metric.itemList track by item.name">
<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>
<!-- Item filter -->
<li class="tight-form-item" ng-hide="target.mode == 2">
Filter
<i class="fa fa-question-circle"
bs-tooltip="'Filtering items by regex. Select All in items and specify regex for item names.'"></i>
</li>
<li ng-hide="target.mode == 2">
<input type="text"
class="tight-form-input input-large"
ng-model="target.itemFilter"
spellcheck='false'
placeholder="Item filter (regex)"
ng-blur="targetBlur()">
</li>
<!-- Scale -->
<li class="tight-form-item" ng-hide="target.mode == 2">
Scale
<i class="fa fa-question-circle"
bs-tooltip="'Set a custom multiplier for series values, for example -1 to invert series'"></i>
</li>
<li ng-hide="target.mode == 2">
<input type="text"
class="tight-form-input input-small"
ng-model="target.scale"
spellcheck='false'
placeholder="1"
ng-blur="targetBlur()">
</li>
</ul>
<div class="clearfix"></div>
</div>

View File

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

View File

@@ -1,273 +0,0 @@
define([
'angular',
'lodash',
'./helperFunctions'
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.controllers');
var targetLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
module.controller('ZabbixAPIQueryCtrl', function ($scope, $sce, templateSrv, zabbixHelperSrv) {
$scope.init = function () {
$scope.targetLetters = targetLetters;
if (!$scope.target.mode || $scope.target.mode !== 1) {
$scope.downsampleFunctionList = [
{name: "avg", value: "avg"},
{name: "min", value: "min"},
{name: "max", value: "max"}
];
// Set avg by default
if (!$scope.target.downsampleFunction) {
$scope.target.downsampleFunction = $scope.downsampleFunctionList[0];
}
if (!$scope.metric) {
$scope.metric = {
hostGroupList: [],
hostList: [{name: '*', visible_name: 'All'}],
applicationList: [{name: '*', visible_name: 'All'}],
itemList: [{name: 'All'}]
};
}
// Update host group, host, application and item lists
$scope.updateGroupList();
$scope.updateHostList();
$scope.updateAppList();
$scope.updateItemList();
setItemAlias();
}
else if ($scope.target.mode === 1) {
$scope.slaPropertyList = [
{name: "Status", property: "status"},
{name: "SLA", property: "sla"},
{name: "OK time", property: "okTime"},
{name: "Problem time", property: "problemTime"},
{name: "Down time", property: "downtimeTime"}
];
$scope.itserviceList = [{name: "test"}];
$scope.updateITServiceList();
}
$scope.target.errors = validateTarget($scope.target);
};
/**
* Switch query editor to specified mode.
* Modes:
* 0 - items
* 1 - IT services
*/
$scope.switchEditorMode = function (mode) {
$scope.target.mode = mode;
$scope.init();
};
/**
* Take alias from item name by default
*/
function setItemAlias() {
if (!$scope.target.alias && $scope.target.item) {
$scope.target.alias = $scope.target.item.name;
}
}
$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 IT service is selected.
*/
$scope.selectITService = function () {
$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 () {
$scope.updateHostList();
$scope.updateAppList();
$scope.updateItemList();
$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 () {
$scope.updateAppList();
$scope.updateItemList();
$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 () {
$scope.updateItemList();
$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 IT services
*/
$scope.updateITServiceList = function () {
$scope.datasource.zabbixAPI.getITService().then(function (iteservices) {
$scope.itserviceList = [];
$scope.itserviceList = $scope.itserviceList.concat(iteservices);
});
};
/**
* Update list of host groups
*/
$scope.updateGroupList = function () {
$scope.datasource.zabbixAPI.performHostGroupSuggestQuery().then(function (groups) {
$scope.metric.groupList = [{name: '*', visible_name: 'All'}];
addTemplatedVariables($scope.metric.groupList);
$scope.metric.groupList = $scope.metric.groupList.concat(groups);
});
};
/**
* Update list of hosts
*/
$scope.updateHostList = function () {
var groups = $scope.target.group ? zabbixHelperSrv.splitMetrics(templateSrv.replace($scope.target.group.name)) : undefined;
if (groups) {
$scope.datasource.zabbixAPI.hostFindQuery(groups).then(function (hosts) {
$scope.metric.hostList = [{name: '*', visible_name: 'All'}];
addTemplatedVariables($scope.metric.hostList);
$scope.metric.hostList = $scope.metric.hostList.concat(hosts);
});
}
};
/**
* Update list of host applications
*/
$scope.updateAppList = function () {
var groups = $scope.target.group ? zabbixHelperSrv.splitMetrics(templateSrv.replace($scope.target.group.name)) : undefined;
var hosts = $scope.target.host ? zabbixHelperSrv.splitMetrics(templateSrv.replace($scope.target.host.name)) : undefined;
if (groups && hosts) {
$scope.datasource.zabbixAPI.appFindQuery(hosts, groups).then(function (apps) {
apps = _.map(_.uniq(_.map(apps, 'name')), function (appname) {
return {name: appname};
});
$scope.metric.applicationList = [{name: '*', visible_name: 'All'}];
addTemplatedVariables($scope.metric.applicationList);
$scope.metric.applicationList = $scope.metric.applicationList.concat(apps);
});
}
};
/**
* Update list of items
*/
$scope.updateItemList = function () {
var groups = $scope.target.group ? zabbixHelperSrv.splitMetrics(templateSrv.replace($scope.target.group.name)) : undefined;
var hosts = $scope.target.host ? zabbixHelperSrv.splitMetrics(templateSrv.replace($scope.target.host.name)) : undefined;
var apps = $scope.target.application ?
zabbixHelperSrv.splitMetrics(templateSrv.replace($scope.target.application.name)) : undefined;
var itemtype = $scope.target.mode === 2 ? "text" : "numeric";
if (groups && hosts && apps) {
$scope.datasource.zabbixAPI.itemFindQuery(groups, hosts, apps, itemtype).then(function (items) {
// Show only unique item names
var uniq_items = _.map(_.uniq(items, function (item) {
return zabbixHelperSrv.expandItemName(item);
}), function (item) {
return {name: zabbixHelperSrv.expandItemName(item)};
});
$scope.metric.itemList = [{name: 'All'}];
addTemplatedVariables($scope.metric.itemList);
$scope.metric.itemList = $scope.metric.itemList.concat(uniq_items);
});
}
};
/**
* Add templated variables to list of available metrics
*
* @param {Array} metricList List of metrics which variables add to
*/
function addTemplatedVariables(metricList) {
_.each(templateSrv.variables, function (variable) {
metricList.push({
name: '$' + variable.name,
templated: true
});
});
}
//////////////////////////////
// VALIDATION
//////////////////////////////
function validateTarget(target) {
var errs = {};
if (!target) {
errs = 'Not defined';
}
return errs;
}
$scope.init();
});
});

View File

@@ -1,538 +0,0 @@
define([
'angular',
'lodash'
],
function (angular, _) {
'use strict';
var module = angular.module('grafana.services');
module.factory('ZabbixAPI', function($q, backendSrv) {
function ZabbixAPI(api_url, username, password, basicAuth, withCredentials) {
// Initialize API parameters.
this.url = api_url;
this.username = username;
this.password = password;
this.basicAuth = basicAuth;
this.withCredentials = withCredentials;
}
var p = ZabbixAPI.prototype;
//////////////////
// Core methods //
//////////////////
/**
* Request data from Zabbix API
*
* @param {string} method Zabbix API method name
* @param {object} params method params
* @return {object} data.result field or []
*/
p.performZabbixAPIRequest = function(method, params) {
var options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
url: this.url,
data: {
jsonrpc: '2.0',
method: method,
params: params,
auth: this.auth,
id: 1
}
};
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
}
if (this.basicAuth) {
options.headers.Authorization = this.basicAuth;
}
var self = this;
return backendSrv.datasourceRequest(options).then(function (response) {
if (!response.data) {
return [];
}
// Handle Zabbix API errors
else if (response.data.error) {
// Handle auth errors
if (response.data.error.data === "Session terminated, re-login, please." ||
response.data.error.data === "Not authorised." ||
response.data.error.data === "Not authorized") {
return self.performZabbixAPILogin().then(function (response) {
self.auth = response;
return self.performZabbixAPIRequest(method, params);
});
}
}
return response.data.result;
});
};
/**
* Get authentication token.
*
* @return {string} auth token
*/
p.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
}
};
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
}
if (this.basicAuth) {
options.headers = options.headers || {};
options.headers.Authorization = this.basicAuth;
}
return backendSrv.datasourceRequest(options).then(function (result) {
if (!result.data) {
return null;
}
return result.data.result;
});
};
/////////////////////////
// API method wrappers //
/////////////////////////
/**
* Request version of the Zabbix API.
*
* @return {string} Zabbix API version
*/
p.getZabbixAPIVersion = function() {
var options = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
url: this.url,
data: {
jsonrpc: '2.0',
method: 'apiinfo.version',
params: [],
id: 1
}
};
if (this.basicAuth || this.withCredentials) {
options.withCredentials = true;
}
if (this.basicAuth) {
options.headers = options.headers || {};
options.headers.Authorization = this.basicAuth;
}
return backendSrv.datasourceRequest(options).then(function (result) {
if (!result.data) {
return null;
}
return result.data.result;
});
};
/**
* Perform history query from Zabbix API
*
* @param {Array} items Array of Zabbix item objects
* @param {Number} start Time in seconds
* @param {Number} end Time in seconds
* @return {Array} Array of Zabbix history objects
*/
p.getHistory = function(items, start, end) {
// Group items by value type
var grouped_items = _.groupBy(items, 'value_type');
// Perform request for each value type
return $q.all(_.map(grouped_items, function (items, value_type) {
var itemids = _.map(items, 'itemid');
var params = {
output: 'extend',
history: value_type,
itemids: itemids,
sortfield: 'clock',
sortorder: 'ASC',
time_from: start
};
// Relative queries (e.g. last hour) don't include an end time
if (end) {
params.time_till = end;
}
return this.performZabbixAPIRequest('history.get', params);
}, this)).then(function (results) {
return _.flatten(results);
});
};
/**
* Perform trends query from Zabbix API
* Use trends api extension from ZBXNEXT-1193 patch.
*
* @param {Array} items Array of Zabbix item objects
* @param {Number} start Time in seconds
* @param {Number} end Time in seconds
* @return {Array} Array of Zabbix trend objects
*/
p.getTrends = function(items, start, end) {
// Group items by value type
var grouped_items = _.groupBy(items, 'value_type');
// Perform request for each value type
return $q.all(_.map(grouped_items, function (items, value_type) {
var itemids = _.map(items, 'itemid');
var params = {
output: 'extend',
trend: value_type,
itemids: itemids,
sortfield: 'clock',
sortorder: 'ASC',
time_from: start
};
// Relative queries (e.g. last hour) don't include an end time
if (end) {
params.time_till = end;
}
return this.performZabbixAPIRequest('trend.get', params);
}, this)).then(function (results) {
return _.flatten(results);
});
};
/**
* Get the list of host groups
*
* @return {array} array of Zabbix hostgroup objects
*/
p.performHostGroupSuggestQuery = function() {
var params = {
output: ['name'],
sortfield: 'name',
// Return only host groups that contain hosts
real_hosts: true,
// Return only host groups that contain monitored hosts.
monitored_hosts: true
};
return this.performZabbixAPIRequest('hostgroup.get', params);
};
/**
* Get the list of hosts
*
* @param {string|string[]} groupids
* @return {Object} array of Zabbix host objects
*/
p.performHostSuggestQuery = function(groupids) {
var params = {
output: ['name', 'host'],
sortfield: 'name',
// Return only hosts that have items with numeric type of information.
with_simple_graph_items: true,
// Return only monitored hosts.
monitored_hosts: true
};
// Return only hosts in given group
if (groupids) {
params.groupids = groupids;
}
return this.performZabbixAPIRequest('host.get', params);
};
/**
* Get the list of applications
*
* @param {array} hostids
* @param {array} groupids
* @return {Object} array of Zabbix application objects
*/
p.performAppSuggestQuery = function(hostids, /* optional */ groupids) {
var params = {
output: ['name'],
sortfield: 'name'
};
if (hostids) {
params.hostids = hostids;
}
else if (groupids) {
params.groupids = groupids;
}
return this.performZabbixAPIRequest('application.get', params);
};
/**
* Items request
*
* @param {string|string[]} hostids ///////////////////////////
* @param {string|string[]} applicationids // Zabbix API parameters //
* @param {string|string[]} groupids ///////////////////////////
* @return {string|string[]} Array of Zabbix API item objects
*/
p.performItemSuggestQuery = function(hostids, applicationids, groupids, itemtype) {
var params = {
output: ['name', 'key_', 'value_type', 'delay'],
sortfield: 'name',
//Include web items in the result
webitems: true,
// Return only numeric items
filter: {
value_type: [0, 3]
},
// Return only enabled items
monitored: true,
searchByAny: true
};
if (itemtype === "text") {
params.filter.value_type = [1, 2, 4];
}
// Filter by hosts or by groups
if (hostids) {
params.hostids = hostids;
} else if (groupids) {
params.groupids = groupids;
}
// If application selected return only relative items
if (applicationids) {
params.applicationids = applicationids;
}
// Return host property for multiple hosts
if (!hostids || (_.isArray(hostids) && hostids.length > 1)) {
params.selectHosts = ['name'];
}
return this.performZabbixAPIRequest('item.get', params);
};
/**
* Get groups by names
*
* @param {string or array} group group names
* @return {array} array of Zabbix API hostgroup objects
*/
p.getGroupByName = function (group) {
var params = {
output: ['name']
};
if (group && group[0] !== '*') {
params.filter = {
name: group
};
}
return this.performZabbixAPIRequest('hostgroup.get', params);
};
/**
* Search group by name.
*
* @param {string} group group name
* @return {array} groups
*/
p.searchGroup = function (group) {
var params = {
output: ['name'],
search: {
name: group
},
searchWildcardsEnabled: true
};
return this.performZabbixAPIRequest('hostgroup.get', params);
};
/**
* Get hosts by names
*
* @param {string or array} hostnames hosts names
* @return {array} array of Zabbix API host objects
*/
p.getHostByName = function (hostnames) {
var params = {
output: ['host', 'name']
};
if (hostnames && hostnames[0] !== '*') {
params.filter = {
name: hostnames
};
}
return this.performZabbixAPIRequest('host.get', params);
};
/**
* Get applications by names
*
* @param {string or array} application applications names
* @return {array} array of Zabbix API application objects
*/
p.getAppByName = function (application) {
var params = {
output: ['name']
};
if (application && application[0] !== '*') {
params.filter = {
name: application
};
}
return this.performZabbixAPIRequest('application.get', params);
};
/**
* Get items belongs to passed groups, hosts and
* applications
*
* @param {string or array} groups
* @param {string or array} hosts
* @param {string or array} apps
* @return {array} array of Zabbix API item objects
*/
p.itemFindQuery = function(groups, hosts, apps, itemtype) {
var promises = [];
// Get hostids from names
if (hosts && hosts[0] !== '*') {
promises.push(this.getHostByName(hosts));
}
// Get groupids from names
else if (groups) {
promises.push(this.getGroupByName(groups));
}
// Get applicationids from names
if (apps && apps[0] !== '*') {
promises.push(this.getAppByName(apps));
}
var self = this;
return $q.all(promises).then(function (results) {
results = _.flatten(results);
var groupids;
var hostids;
var applicationids;
if (groups) {
groupids = _.map(_.filter(results, function (object) {
return object.groupid;
}), 'groupid');
}
if (hosts && hosts[0] !== '*') {
hostids = _.map(_.filter(results, function (object) {
return object.hostid;
}), 'hostid');
}
if (apps && apps[0] !== '*') {
applicationids = _.map(_.filter(results, function (object) {
return object.applicationid;
}), 'applicationid');
}
return self.performItemSuggestQuery(hostids, applicationids, groupids, itemtype);
});
};
/**
* Find applications belongs to passed groups and hosts
*
* @param {string or array} hosts
* @param {string or array} groups
* @return {array} array of Zabbix API application objects
*/
p.appFindQuery = function(hosts, groups) {
var promises = [];
// Get hostids from names
if (hosts && hosts[0] !== '*') {
promises.push(this.getHostByName(hosts));
}
// Get groupids from names
else if (groups) {
promises.push(this.getGroupByName(groups));
}
var self = this;
return $q.all(promises).then(function (results) {
results = _.flatten(results);
var groupids;
var hostids;
if (groups) {
groupids = _.map(_.filter(results, function (object) {
return object.groupid;
}), 'groupid');
}
if (hosts && hosts[0] !== '*') {
hostids = _.map(_.filter(results, function (object) {
return object.hostid;
}), 'hostid');
}
return self.performAppSuggestQuery(hostids, groupids);
});
};
/**
* Find hosts belongs to passed groups
*
* @param {string or array} groups
* @return {array} array of Zabbix API host objects
*/
p.hostFindQuery = function(groups) {
var self = this;
return this.getGroupByName(groups).then(function (results) {
results = _.flatten(results);
var groupids = _.map(_.filter(results, function (object) {
return object.groupid;
}), 'groupid');
return self.performHostSuggestQuery(groupids);
});
};
p.getITService = function(/* optional */ serviceids) {
var params = {
output: 'extend',
serviceids: serviceids
};
return this.performZabbixAPIRequest('service.get', params);
};
p.getSLA = function(serviceids, from, to) {
var params = {
serviceids: serviceids,
intervals: [{
from: from,
to: to
}]
};
return this.performZabbixAPIRequest('service.getsla', params);
};
return ZabbixAPI;
});
});