diff --git a/.gitignore b/.gitignore index 93706d652..5593838a0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,24 @@ *.bat # Grafana linter config -.jshintrc -.jscs.json -.jsfmtrc \ No newline at end of file +# .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 diff --git a/.jscs.json b/.jscs.json new file mode 100644 index 000000000..dcf694dcc --- /dev/null +++ b/.jscs.json @@ -0,0 +1,13 @@ +{ + "disallowImplicitTypeConversion": ["string"], + "disallowKeywords": ["with"], + "disallowMultipleLineBreaks": true, + "disallowMixedSpacesAndTabs": true, + "disallowTrailingWhitespace": true, + "requireSpacesInFunctionExpression": { + "beforeOpeningCurlyBrace": true + }, + "disallowSpacesInsideArrayBrackets": true, + "disallowSpacesInsideParentheses": true, + "validateIndentation": 2 +} \ No newline at end of file diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 000000000..fe8638292 --- /dev/null +++ b/.jshintrc @@ -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 + } +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..be343e60f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1 @@ +# Contributing to Grafana-Zabbix \ No newline at end of file diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 000000000..85b623df3 --- /dev/null +++ b/Gruntfile.js @@ -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' + ]); +}; diff --git a/README.md b/README.md index eb2287b5d..9b4b964c0 100644 --- a/README.md +++ b/README.md @@ -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) -![Dashboard](https://cloud.githubusercontent.com/assets/4932851/8269101/9e6ee67e-17a3-11e5-85de-fe9dcc2dd375.png) +[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! -#### [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. [**User’s Guide**](https://github.com/alexanderzobnin/grafana-zabbix/wiki/Usage) -4. [**Troubleshooting**](https://github.com/alexanderzobnin/grafana-zabbix/wiki/Troubleshooting) +![Dashboard](https://cloud.githubusercontent.com/assets/4932851/8269101/9e6ee67e-17a3-11e5-85de-fe9dcc2dd375.png) ## 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 diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..0be873702 --- /dev/null +++ b/docs/README.md @@ -0,0 +1 @@ +# Grafana-Zabbix Documentation \ No newline at end of file diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 000000000..a11d34689 --- /dev/null +++ b/docs/mkdocs.yml @@ -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' diff --git a/docs/sources/features.md b/docs/sources/features.md new file mode 100644 index 000000000..9f91a5ae4 --- /dev/null +++ b/docs/sources/features.md @@ -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 + diff --git a/docs/sources/guides/gettingstarted.md b/docs/sources/guides/gettingstarted.md new file mode 100644 index 000000000..67236f114 --- /dev/null +++ b/docs/sources/guides/gettingstarted.md @@ -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. \ No newline at end of file diff --git a/docs/sources/img/.gitattributes b/docs/sources/img/.gitattributes new file mode 100644 index 000000000..257c15f57 --- /dev/null +++ b/docs/sources/img/.gitattributes @@ -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 diff --git a/docs/sources/img/installation-add_datasource.png b/docs/sources/img/installation-add_datasource.png new file mode 100644 index 000000000..1163c20b8 --- /dev/null +++ b/docs/sources/img/installation-add_datasource.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62deb035e0d9241c2ae9825b101c86d329766cf421a5e2555449f71420cb9891 +size 58575 diff --git a/docs/sources/img/installation-datasource_config.png b/docs/sources/img/installation-datasource_config.png new file mode 100644 index 000000000..36a86c188 --- /dev/null +++ b/docs/sources/img/installation-datasource_config.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5275ed470657abc2c9c0d648e1a24c0f1b3a029206f8aa9594009b837f3ec2f1 +size 50282 diff --git a/docs/sources/img/installation-test_connection.png b/docs/sources/img/installation-test_connection.png new file mode 100644 index 000000000..226079385 --- /dev/null +++ b/docs/sources/img/installation-test_connection.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:767ffe4d4385871847acb9c1b77b42f63144299810c5c97af2eee9057e19c6d7 +size 12828 diff --git a/docs/sources/img/installation-test_connection_error.png b/docs/sources/img/installation-test_connection_error.png new file mode 100644 index 000000000..ee85b01a4 --- /dev/null +++ b/docs/sources/img/installation-test_connection_error.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee8a5b214d169eb70f8c591cb0e64910b280ce1a8abe1fbf8ee4de26af81a809 +size 14287 diff --git a/docs/sources/index.md b/docs/sources/index.md new file mode 100644 index 000000000..bd9ed6ba2 --- /dev/null +++ b/docs/sources/index.md @@ -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. diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md new file mode 100644 index 000000000..824668dd5 --- /dev/null +++ b/docs/sources/installation/configuration.md @@ -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. diff --git a/docs/sources/installation/index.md b/docs/sources/installation/index.md new file mode 100644 index 000000000..3f10253d0 --- /dev/null +++ b/docs/sources/installation/index.md @@ -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/) diff --git a/docs/sources/installation/troubleshooting.md b/docs/sources/installation/troubleshooting.md new file mode 100644 index 000000000..a84d15c00 --- /dev/null +++ b/docs/sources/installation/troubleshooting.md @@ -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. diff --git a/docs/sources/reference/datasource-zabbix.md b/docs/sources/reference/datasource-zabbix.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/sources/reference/panel-triggers.md b/docs/sources/reference/panel-triggers.md new file mode 100644 index 000000000..e69de29bb diff --git a/docs/sources/tutorials/host_dashboard.md b/docs/sources/tutorials/host_dashboard.md new file mode 100644 index 000000000..e69de29bb diff --git a/package.json b/package.json new file mode 100644 index 000000000..b4b96f1d2 --- /dev/null +++ b/package.json @@ -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" +} diff --git a/src/components/config.html b/src/components/config.html new file mode 100644 index 000000000..89eea1a69 --- /dev/null +++ b/src/components/config.html @@ -0,0 +1,13 @@ +

Zabbix App Config

+ +
+
+
+ json Data property + +
+
+ +
+
+
diff --git a/src/components/config.js b/src/components/config.js new file mode 100644 index 000000000..fe9c1ef3c --- /dev/null +++ b/src/components/config.js @@ -0,0 +1,4 @@ +export class ZabbixAppConfigCtrl { + constructor() { } +} +ZabbixAppConfigCtrl.templateUrl = 'components/config.html'; diff --git a/src/dashboards/zabbix_server_dashboard.json b/src/dashboards/zabbix_server_dashboard.json new file mode 100644 index 000000000..172fcc9ff --- /dev/null +++ b/src/dashboards/zabbix_server_dashboard.json @@ -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": [] +} diff --git a/src/datasource-zabbix/DataProcessor.js b/src/datasource-zabbix/DataProcessor.js new file mode 100644 index 000000000..c47965e8d --- /dev/null +++ b/src/datasource-zabbix/DataProcessor.js @@ -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: [[, ], ...] + */ + 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: { '': [[, ], ...] } + // return [{ '': }, { '': }, ...] + 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; +} diff --git a/src/datasource-zabbix/add-metric-function.directive.js b/src/datasource-zabbix/add-metric-function.directive.js new file mode 100644 index 000000000..a3676ee17 --- /dev/null +++ b/src/datasource-zabbix/add-metric-function.directive.js @@ -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 = ''; + + var buttonTemplate = '' + + ''; + + 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 + "')", + }; + }) + }; + }); +} + diff --git a/src/datasource-zabbix/css/query-editor.css b/src/datasource-zabbix/css/query-editor.css new file mode 100644 index 000000000..3edd27d6c --- /dev/null +++ b/src/datasource-zabbix/css/query-editor.css @@ -0,0 +1,7 @@ +.zbx-regex { + color: #CCA300; +} + +.zbx-variable { + color: #33B5E5; +} diff --git a/src/datasource-zabbix/datasource.js b/src/datasource-zabbix/datasource.js new file mode 100644 index 000000000..faeda51eb --- /dev/null +++ b/src/datasource-zabbix/datasource.js @@ -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 + }; +} diff --git a/src/datasource-zabbix/metric-function-editor.directive.js b/src/datasource-zabbix/metric-function-editor.directive.js new file mode 100644 index 000000000..e00119448 --- /dev/null +++ b/src/datasource-zabbix/metric-function-editor.directive.js @@ -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 = '{{func.def.name}}('; + var paramTemplate = ''; + + var funcControlsTemplate = + '
' + + '' + + '' + + '' + + '' + + '
'; + + 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) { + $(', ').appendTo(elem); + } + + var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); + var $paramLink = $('' + paramValue + ''); + 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); + } + + }); + + $(')').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(); + } + }; + + }); diff --git a/src/datasource-zabbix/metricFunctions.js b/src/datasource-zabbix/metricFunctions.js new file mode 100644 index 000000000..aa0351ce7 --- /dev/null +++ b/src/datasource-zabbix/metricFunctions.js @@ -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; +} diff --git a/src/datasource-zabbix/migrations.js b/src/datasource-zabbix/migrations.js new file mode 100644 index 000000000..0bbd737c6 --- /dev/null +++ b/src/datasource-zabbix/migrations.js @@ -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 + '/'; +} diff --git a/src/datasource-zabbix/module.js b/src/datasource-zabbix/module.js new file mode 100644 index 000000000..35bb3af72 --- /dev/null +++ b/src/datasource-zabbix/module.js @@ -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 +}; diff --git a/src/datasource-zabbix/partials/annotations.editor.html b/src/datasource-zabbix/partials/annotations.editor.html new file mode 100644 index 000000000..6e2e4954e --- /dev/null +++ b/src/datasource-zabbix/partials/annotations.editor.html @@ -0,0 +1,65 @@ +
+
Filter Triggers
+
+
+ Group + + +
+ +
+ Host + + +
+
+
+
+ Application + + +
+ +
+ Trigger + + +
+
+
+
+
+ Minimum severity +
+ +
+
+
+
+
Options
+
+ + + +
+
diff --git a/src/datasource-zabbix/partials/config.html b/src/datasource-zabbix/partials/config.html new file mode 100644 index 000000000..85a6fe250 --- /dev/null +++ b/src/datasource-zabbix/partials/config.html @@ -0,0 +1,61 @@ + + + +
+

Zabbix API details

+ +
+ + Username + + + +
+ +
+ + Password + + + +
+ +
+
+ +
+ + +
+ + Use from + + + +
+
+ +
+ + Cache update interval + + + +
+
diff --git a/src/datasource-zabbix/partials/query.editor.html b/src/datasource-zabbix/partials/query.editor.html new file mode 100644 index 000000000..72f6b7388 --- /dev/null +++ b/src/datasource-zabbix/partials/query.editor.html @@ -0,0 +1,233 @@ +
+ + +
    +
  • + {{ctrl.target.refId}} +
  • +
  • + + + +
  • +
+ + + + + + +
+
+ +
+ + +
+
+
+ +
+
+ +
+ + +
+
+ diff --git a/zabbix/partials/query.options.html b/src/datasource-zabbix/partials/query.options.html similarity index 67% rename from zabbix/partials/query.options.html rename to src/datasource-zabbix/partials/query.options.html index b63a48fe1..fa58b313d 100644 --- a/zabbix/partials/query.options.html +++ b/src/datasource-zabbix/partials/query.options.html @@ -11,10 +11,10 @@
  • @@ -27,20 +27,25 @@
  • - + Max data points
  • - + IT services
  • - + IT service property
  • +
  • + + Text filter + +
  • @@ -49,7 +54,7 @@
    -
    +
    Max data points
    • Grafana-Zabbix plugin uses maxDataPoints parameter to consolidate the real number of values down to this @@ -62,14 +67,14 @@
      Max data points
    -
    +
    IT services
    • Select "IT services" in targets menu to activate IT services mode.
    -
    +
    IT service property
    • Zabbix returns the following availability information about IT service
    • @@ -80,5 +85,12 @@
      IT service property
    • Down time - time the service was in scheduled downtime, in seconds
    + +
    +
    Text filter
    +
      +
    • Use regex to extract a part of the returned value.
    • +
    +
    diff --git a/src/datasource-zabbix/plugin.json b/src/datasource-zabbix/plugin.json new file mode 100644 index 000000000..940df269f --- /dev/null +++ b/src/datasource-zabbix/plugin.json @@ -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" + } + } +} diff --git a/src/datasource-zabbix/query.controller.js b/src/datasource-zabbix/query.controller.js new file mode 100644 index 000000000..d4a62f4df --- /dev/null +++ b/src/datasource-zabbix/query.controller.js @@ -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')); +} diff --git a/src/datasource-zabbix/queryProcessor.service.js b/src/datasource-zabbix/queryProcessor.service.js new file mode 100644 index 000000000..abeffdb17 --- /dev/null +++ b/src/datasource-zabbix/queryProcessor.service.js @@ -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: [[, ], ...] + * } + */ + convertHistory(history, addHostName, convertPointCallback) { + /** + * Response should be in the format: + * data: [ + * { + * target: "Metric name", + * datapoints: [[, ], ...] + * }, ... + * ] + */ + 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 + ]; +} diff --git a/src/datasource-zabbix/utils.js b/src/datasource-zabbix/utils.js new file mode 100644 index 000000000..cf89a7879 --- /dev/null +++ b/src/datasource-zabbix/utils.js @@ -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 = '

    Acknowledges:
    ' + + ''; + _.each(_.map(acknowledges, function (ack) { + var timestamp = moment.unix(ack.clock); + return ''; + }), function (ack) { + formatted_acknowledges = formatted_acknowledges.concat(ack); + }); + formatted_acknowledges = formatted_acknowledges.concat('
    TimeUserComments
    ' + timestamp.format("DD MMM YYYY HH:mm:ss") + '' + ack.alias + + ' (' + ack.name + ' ' + ack.surname + ')' + '' + ack.message + '
    '); + 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"); + } +} diff --git a/src/datasource-zabbix/zabbixAPI.service.js b/src/datasource-zabbix/zabbixAPI.service.js new file mode 100644 index 000000000..beb4eecb2 --- /dev/null +++ b/src/datasource-zabbix/zabbixAPI.service.js @@ -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); diff --git a/src/datasource-zabbix/zabbixAPICore.service.js b/src/datasource-zabbix/zabbixAPICore.service.js new file mode 100644 index 000000000..a1623e60f --- /dev/null +++ b/src/datasource-zabbix/zabbixAPICore.service.js @@ -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); diff --git a/src/datasource-zabbix/zabbixCache.service.js b/src/datasource-zabbix/zabbixCache.service.js new file mode 100644 index 000000000..fe5e59340 --- /dev/null +++ b/src/datasource-zabbix/zabbixCache.service.js @@ -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; +}; diff --git a/src/module.js b/src/module.js new file mode 100644 index 000000000..34ff163f9 --- /dev/null +++ b/src/module.js @@ -0,0 +1,5 @@ +import {ZabbixAppConfigCtrl} from './components/config'; + +export { + ZabbixAppConfigCtrl as ConfigCtrl +}; diff --git a/src/panel-triggers/editor.html b/src/panel-triggers/editor.html new file mode 100644 index 000000000..fc55d3e45 --- /dev/null +++ b/src/panel-triggers/editor.html @@ -0,0 +1,288 @@ +
    +
    +
    Select triggers
    +
    +
      +
    • + Group +
    • +
    • + +
    • +
    • + Host +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Application +
    • +
    • + +
    • +
    • + Trigger +
    • +
    • + +
    • +
    +
    +
    +
    +
    +
    Data source
    +
    +
    +
      +
    • + +
    • +
    +
    +
    +
    +
    + +
    +
    +
    Options
    +
    +
    +
      +
    • + Acknowledged +
    • +
    • + +
    • +
    • + Limit triggers number to +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Sort by +
    • +
    • + +
    • +
    • + Show events +
    • +
    • + +
    • +
    +
    +
    +
    +
      +
    • + Show fields +
    • +
    • + + + +
    • +
    • + + + +
    • +
    • + + + +
    • +
    • + + + +
    • +
    • + + + +
    • +
    • + + + +
    • +
    +
    +
    +
    +
      +
    • + Custom Last change format + + + +
    • +
    • + +
    • +
    • + + + + +
    • +
    +
    +
    +
    +
    +
    +
    Customize triggers severity and colors
    +
    +
      +
    • + {{ trigger.priority }} +
    • +
    • + +
    • +
    • + +
    • +
    • + + + +
    • +
    +
    +
    +
    +
      +
    • + OK event color +
    • +
    • + + +
    • +
    +
    +
    +
    +
    diff --git a/src/panel-triggers/editor.js b/src/panel-triggers/editor.js new file mode 100644 index 000000000..df53ff536 --- /dev/null +++ b/src/panel-triggers/editor.js @@ -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, + }; +} diff --git a/src/panel-triggers/module.html b/src/panel-triggers/module.html new file mode 100644 index 000000000..fddc00481 --- /dev/null +++ b/src/panel-triggers/module.html @@ -0,0 +1,136 @@ +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + Host +
    +
    +
    Status
    +
    +
    Severity
    +
    +
    Issue
    +
    +
    Last change
    +
    +
    Age
    +
    +
    Info
    +
    +
    + {{trigger.host}} +
    +
    +
    + {{ctrl.triggerStatusMap[trigger.value]}} +
    +
    +
    + {{trigger.severity}} +
    +
    +
    + {{trigger.description}} + + + +
    + + +
    +
    + {{trigger.comments}} +
    +
    + + +
    +
    + + + + + + + + + + + + + + + +
    TimeUserComments
    + {{ack.time}} + + {{ack.user}} + + {{ack.message}} +
    +
    +
    +
    + {{trigger.lastchange}} + + {{trigger.age}} + + + + + + + + + + + + + + + + +
    +
    +
    + diff --git a/src/panel-triggers/module.js b/src/panel-triggers/module.js new file mode 100644 index 000000000..b29c0b048 --- /dev/null +++ b/src/panel-triggers/module.js @@ -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 +}; diff --git a/src/panel-triggers/plugin.json b/src/panel-triggers/plugin.json new file mode 100644 index 000000000..0e8bdbd92 --- /dev/null +++ b/src/panel-triggers/plugin.json @@ -0,0 +1,12 @@ +{ + "type": "panel", + "name": "Zabbix Triggers", + "id": "zabbix-triggers-panel", + + "info": { + "author": { + "name": "Alexander Zobnin", + "url": "http://grafana-zabbix.org" + } + } +} diff --git a/src/panel-triggers/sass/panel_triggers.scss b/src/panel-triggers/sass/panel_triggers.scss new file mode 100644 index 000000000..478f7dd2a --- /dev/null +++ b/src/panel-triggers/sass/panel_triggers.scss @@ -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; +} diff --git a/src/panel-triggers/trigger.colorpicker.html b/src/panel-triggers/trigger.colorpicker.html new file mode 100644 index 000000000..4f878d0d9 --- /dev/null +++ b/src/panel-triggers/trigger.colorpicker.html @@ -0,0 +1,13 @@ +
    + × + +
    +   +
    +
    + diff --git a/src/plugin.json b/src/plugin.json new file mode 100644 index 000000000..1f030c984 --- /dev/null +++ b/src/plugin.json @@ -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": [] + } +} diff --git a/zabbix/datasource.js b/zabbix/datasource.js deleted file mode 100644 index 447d82719..000000000 --- a/zabbix/datasource.js +++ /dev/null @@ -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.
    " - + "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; - }); -}); diff --git a/zabbix/directives.js b/zabbix/directives.js deleted file mode 100644 index 071d4be3a..000000000 --- a/zabbix/directives.js +++ /dev/null @@ -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'}; - }); - - }); diff --git a/zabbix/helperFunctions.js b/zabbix/helperFunctions.js deleted file mode 100644 index f59a4708a..000000000 --- a/zabbix/helperFunctions.js +++ /dev/null @@ -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: [[, ], ...] - * } - */ - this.handleHistoryResponse = function(items, alias, scale, history) { - /** - * Response should be in the format: - * data: [ - * { - * target: "Metric name", - * datapoints: [[, ], ...] - * }, - * { - * target: "Metric name", - * datapoints: [[, ], ...] - * }, - * ] - */ - - // 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: [[, ], ...] - * } - */ - 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 = '

    Acknowledges:
    ' - + ''; - _.each(_.map(acknowledges, function (ack) { - var time = new Date(ack.clock * 1000); - return ''; - }), function (ack) { - formatted_acknowledges = formatted_acknowledges.concat(ack); - }); - formatted_acknowledges = formatted_acknowledges.concat('
    TimeUserComments
    ' + self.getShortTime(time) + '' + ack.alias - + ' (' + ack.name + ' ' + ack.surname + ')' + '' + ack.message + '
    '); - return formatted_acknowledges; - } else { - return ''; - } - }; - - /** - * Downsample datapoints series - * - * @param {Object[]} datapoints [[, ], ...] - * @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[]} [[, ], ...] - */ - 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(); - }; - }); -}); \ No newline at end of file diff --git a/zabbix/partials/annotations.editor.html b/zabbix/partials/annotations.editor.html deleted file mode 100644 index 1a95b3651..000000000 --- a/zabbix/partials/annotations.editor.html +++ /dev/null @@ -1,36 +0,0 @@ -
    -
    -
    Zabbix trigger -
    -
    -
    - -
    -
    - - -
    -
    - - -
    -
    - - -
    -
    - -
    -
    -
    Options
    - - -
    -
    diff --git a/zabbix/partials/config.html b/zabbix/partials/config.html deleted file mode 100644 index a0a7f5074..000000000 --- a/zabbix/partials/config.html +++ /dev/null @@ -1,63 +0,0 @@ -
    - -
    - -
    Zabbix API details
    - -
    -
      -
    • - User -
    • -
    • - -
    • -
    • - Password -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - Trends -
    • -
    • - Enable  - - -
    • -
    • - Use trends from -
    • -
    • - -
    • -
    -
    -
    -
    -
      -
    • - Metrics limit -
    • -
    • - -
    • -
    -
    -
    diff --git a/zabbix/partials/query.editor.html b/zabbix/partials/query.editor.html deleted file mode 100644 index 5a85dc54c..000000000 --- a/zabbix/partials/query.editor.html +++ /dev/null @@ -1,227 +0,0 @@ -
    - - -
      -
    • - {{target.refId}} -
    • -
    • - - - -
    • -
    - - - - - - -
    -
    - -
    - - -
    -
    - diff --git a/zabbix/plugin.json b/zabbix/plugin.json deleted file mode 100644 index b5a3489d0..000000000 --- a/zabbix/plugin.json +++ /dev/null @@ -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 -} diff --git a/zabbix/queryCtrl.js b/zabbix/queryCtrl.js deleted file mode 100644 index e3cd92949..000000000 --- a/zabbix/queryCtrl.js +++ /dev/null @@ -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(); - - }); - - }); diff --git a/zabbix/zabbixAPIWrapper.js b/zabbix/zabbixAPIWrapper.js deleted file mode 100644 index 8a89ccc02..000000000 --- a/zabbix/zabbixAPIWrapper.js +++ /dev/null @@ -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; - - }); - -});