├── .codeclimate.yml
├── .eslintrc
├── .gitignore
├── .prettierrc.js
├── GNUmakefile
├── LICENSE
├── README.md
├── appveyor.yml
├── build
├── webpack.base.conf.js
├── webpack.dev.conf.js
└── webpack.prod.conf.js
├── dist
├── LICENSE
├── README.md
├── config
│ └── template.html
├── datasource
│ ├── module.js
│ ├── module.js.map
│ ├── partials
│ │ ├── add_model.html
│ │ ├── config.html
│ │ ├── edit_model.html
│ │ └── query_ctrl.html
│ └── plugin.json
├── img
│ ├── logo.png
│ ├── loudml_grafana_datasource.png
│ └── loudml_grafana_panel.png
├── index.html
├── module.js
├── module.js.map
├── panel
│ ├── module.js
│ ├── module.js.map
│ └── plugin.json
└── plugin.json
├── docs
├── README.md
├── _config.yml
├── loudml_annotations.png
├── loudml_grafana_app.png
├── loudml_grafana_datasource.png
├── loudml_grafana_panel.png
└── loudml_props.png
├── grafana-loudml-app-1.1.0.zip
├── grafana-loudml-app-1.2.0.zip
├── grafana-loudml-app-1.3.0.zip
├── jest.config.js
├── loudml-grafana-app-1.4.0-grafana7.zip
├── loudml-grafana-app-1.4.0.zip
├── loudml-grafana-app-1.6.0-grafana6.zip
├── loudml-grafana-app-1.7.0dev-grafana6.zip
├── loudml-grafana-app-1.7.1.zip
├── loudml-grafana-app-1.7.2.zip
├── package.json
├── src
├── config
│ ├── config_ctrl.ts
│ └── template.html
├── datasource
│ ├── config_ctrl.ts
│ ├── datasource.ts
│ ├── loudml_api.ts
│ ├── module.ts
│ ├── partials
│ │ ├── add_job.html
│ │ ├── add_model.html
│ │ ├── config.html
│ │ ├── edit_model.html
│ │ └── query_ctrl.html
│ ├── plugin.json
│ ├── query_ctrl.ts
│ └── types.ts
├── img
│ ├── logo.png
│ ├── loudml_grafana_datasource.png
│ └── loudml_grafana_panel.png
├── module.ts
├── module_raw.d.ts
├── panel
│ ├── Graph2.tsx
│ ├── GraphLegendEditor.tsx
│ ├── GraphPanel.tsx
│ ├── GraphPanelController.tsx
│ ├── GraphPanelEditor.tsx
│ ├── GraphWithLegend2.tsx
│ ├── extractors.ts
│ ├── getGraphSeriesModel.ts
│ ├── module.test.ts
│ ├── module.ts
│ ├── plugin.json
│ └── types.ts
├── plugin.json
└── utils.ts
├── tsconfig.json
└── yarn.lock
/.codeclimate.yml:
--------------------------------------------------------------------------------
1 | exclude_patterns:
2 | - "dist"
3 | - "**/node_modules/"
4 | - "**/*.d.ts"
5 | - "src/libs/"
6 | - "src/images/"
7 | - "src/img/"
8 | - "src/screenshots/"
9 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@grafana/eslint-config"],
3 | "root": true,
4 | "overrides": [
5 | {
6 | "files": ["packages/**/*.{ts,tsx}", "public/app/**/*.{ts,tsx}"],
7 | "rules": {
8 | "react-hooks/rules-of-hooks": "off",
9 | "react-hooks/exhaustive-deps": "off"
10 | }
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | node_modules/
9 |
10 | # Runtime data
11 | pids
12 | *.pid
13 | *.seed
14 | *.pid.lock
15 |
16 | # Directory for instrumented libs generated by jscoverage/JSCover
17 | lib-cov
18 |
19 | # Coverage directory used by tools like istanbul
20 | coverage
21 |
22 | # Compiled binary addons (https://nodejs.org/api/addons.html)
23 | dist/
24 | artifacts/
25 | work/
26 | ci/
27 | e2e-results/
28 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | ...require("./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json"),
3 | };
4 |
--------------------------------------------------------------------------------
/GNUmakefile:
--------------------------------------------------------------------------------
1 | default: run
2 |
3 | build:
4 | mkdir loudml-grafana-app
5 | rsync -rpE dist/ loudml-grafana-app/
6 | zip -y -r -q loudml-grafana-app-1.7.2.zip loudml-grafana-app
7 | rm -rf loudml-grafana-app
8 |
9 | run:
10 | yarn watch
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Volodymyr Sergeyev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # LoudML Grafana Application
2 |
3 | Visualization panel and datasource for Grafana 6.7.x - 7.x to connect with Loud ML AI solution for ICT and IoT
4 | automation. https://loudml.io
5 |
6 | 
7 |
8 | 
9 |
10 | Loud ML is an open source inference engine for metrics and events, and the fastest way to embed machine learning in your time series application. This includes APIs for storing and querying data, processing it in the background for ML or detecting outliers for alerting purposes, and more.
11 | https://github.com/regel/loudml
12 |
13 | # Installation
14 |
15 | Repository conventions:
16 |
17 | * `master` branch is for Grafana 7
18 | * `grafana/6.x` branch is for Grafana 6
19 |
20 |
21 | ZIP files has packaged plugin for each of Grafana version supported.
22 |
23 | A) Give it a try with Docker
24 |
25 | docker run -d \
26 | -p 3000:3000 \
27 | --name=grafana \
28 | -e "GF_INSTALL_PLUGINS=https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip;loudml-grafana-app" \
29 | grafana/grafana
30 |
31 | Setup LoudML if needed (please refer to https://hub.docker.com/r/loudml/loudml for config.yml setup)
32 |
33 | docker run -p 8077:8077 \
34 | -v $PWD/lib/loudml:/var/lib/loudml:rw \
35 | -v $PWD/config.yml:/etc/loudml/config.yml:ro \
36 | -ti \
37 | loudml/loudml
38 |
39 | B) In existing Grafana container
40 |
41 | * Connect to your Grafana server if necessary (e.g. via SSH).
42 | * Go to plugins directory (usually data/plugins under Grafana installation or /var/lib/grafana/plugins)
43 |
44 | cd /var/lib/grafana/plugins
45 | * Download loudml-grafana-app-latest.zip zip file:
46 |
47 | wget https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip
48 | * Unpack it there
49 |
50 | unzip loudml-grafana-app-latest.zip
51 | * You may remove the downloaded archive
52 | * Restart Grafana
53 |
54 | C) From sources (note - default `master` branch is for Grafana 7.x)
55 |
56 | * Plugin should be placed in `.../grafana/data/plugins`
57 | * git clone https://github.com/vsergeyev/loudml-grafana-app.git
58 | * cd loudml-grafana-app
59 | * yarn
60 | * yarn dev --watch
61 | * restart Grafana
62 | * LoudML app should be in plugins list, you may need to activate it
63 | * enjoy :)
64 |
65 | # Whats inside
66 |
67 | Loud ML Panel - is a version of Grafana's default Graph Panel with a "Create Baseline" button
68 | to create ML model in 1-click.
69 |
70 | Currently 1-click ML button ("Create Baseline") can produce model from:
71 |
72 | * InfluxDB datasource
73 | * OpenTSDB datasource
74 | * Elasticsearch datasource (beta)
75 | * Prometheus datasource (very draft)
76 |
77 | Loud ML Datasource - is a connector to Loud ML server. It has capabilities to show models and jobs on server. You can add new and edit existing models.
78 |
79 | # Prerequisites
80 |
81 | * Loud ML server https://github.com/regel/loudml
82 | * Grafana >= 5.4.0
83 |
84 | # Configuration
85 |
86 | In order to use Loud ML with Grafana you need to have a buckets in **loudml.yml** to reflect Grafana datasource(s) used in LoudML Graph
87 |
88 | 
89 |
90 | Example: I have InfluxDB datasource with **telegraf** database as an input and will use **loudml** database as output for ML model predictions/forecasting/anomalies:
91 |
92 | buckets:
93 | - name: loudml
94 | type: influxdb
95 | addr: 127.0.0.1:8086
96 | database: loudml
97 | retention_policy: autogen
98 | measurement: loudml
99 | annotation_db: loudmlannotations
100 | - name: influxdb1
101 | type: influxdb
102 | addr: 127.0.0.1:8086
103 | database: telegraf
104 | retention_policy: autogen
105 | measurement: loudml
106 | - name: data
107 | type: influxdb
108 | addr: 127.0.0.1:8086
109 | database: data
110 | retention_policy: autogen
111 | measurement: sinus
112 | - name: opentsdb1
113 | type: opentsdb
114 | addr: 127.0.0.1:4242
115 | retention_policy: autogen
116 | - name: prom1
117 | type: prometheus
118 | addr: 127.0.0.1:9090
119 | retention_policy: autogen
120 |
121 | InfluxDB **loudmlannotations** here specified to store annotations. (By default Loud ML server will store annotations in **chronograf** database). So on Grafana dashboard annotations/anomalies from Loud ML should be configured as:
122 |
123 | SELECT "text" FROM "autogen"."annotations" WHERE $timeFilter
124 |
125 | 
126 |
127 | # Support
128 |
129 | Please post issue to tracker or contact me via vova.sergeyev at gmail.com
130 |
131 | # Changelog
132 |
133 | * 1.7.2 Fixed compatibility issue with Grafana 7.x
134 | * 1.7.0dev Fixed issue with updating model on a server (#19). Fixed datasource config page in Grafana 7.x (#12).
135 | * 1.6.0 Better Grafana 6.x compatibility. Fixed issue with output bucket.
136 | * 1.5.0 Added capability to add and edit models on Loud ML Datasource page.
137 | * 1.4.0 Changed ID to correct format "loudml-grafana-app"; Fixes code style follow guidelines.
138 | * 1.3.0 Fixed issue #5 with fill(0); New capabilities: multiple metrics/features per ML model (for InfluxDB data).
139 | * 1.2.0 New capabilities: LoudML datasource - add scheduled job; list of scheduled jobs.
140 | * 1.1.0 Initial public release
141 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | # Test against the latest version of this Node.js version
2 | environment:
3 | nodejs_version: "12"
4 |
5 | # Local NPM Modules
6 | cache:
7 | - node_modules
8 |
9 | # Install scripts. (runs after repo cloning)
10 | install:
11 | # Get the latest stable version of Node.js or io.js
12 | - ps: Install-Product node $env:nodejs_version
13 | # install modules
14 | - npm install -g yarn --quiet
15 | - yarn install --pure-lockfile
16 |
17 | # Post-install test scripts.
18 | test_script:
19 | # Output useful info for debugging.
20 | - node --version
21 | - npm --version
22 |
23 | # Run the build
24 | build_script:
25 | - yarn dev # This will also run prettier!
26 | - yarn build # make sure both scripts work
27 |
--------------------------------------------------------------------------------
/build/webpack.base.conf.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 | const webpack = require('webpack');
3 | const CopyWebpackPlugin = require('copy-webpack-plugin');
4 |
5 | function resolve(dir) {
6 | return path.join(__dirname, '..', dir)
7 | }
8 |
9 | module.exports = {
10 | target: 'node',
11 | context: resolve('src'),
12 | entry: {
13 | './module': './module.ts',
14 | './panel/module': './panel/module.ts',
15 | './datasource/module': './datasource/module.ts'
16 | },
17 | output: {
18 | filename: '[name].js',
19 | path: resolve('dist'),
20 | libraryTarget: 'amd',
21 | publicPath: '/'
22 | },
23 | externals: [
24 | 'lodash',
25 | 'jquery',
26 | 'moment',
27 | 'slate',
28 | 'emotion',
29 | 'prismjs',
30 | 'slate-plain-serializer',
31 | '@grafana/slate-react',
32 | 'react',
33 | 'react-dom',
34 | 'react-redux',
35 | 'redux',
36 | 'rxjs',
37 | 'd3',
38 | 'angular',
39 | '@grafana/ui',
40 | '@grafana/runtime',
41 | '@grafana/data',
42 | function(context, request, callback) {
43 | var prefix = 'grafana/';
44 | if (request.indexOf(prefix) === 0) {
45 | return callback(null, request.substr(prefix.length));
46 | }
47 | callback();
48 | }
49 | ],
50 | // stats: 'none',
51 | plugins: [
52 | new webpack.optimize.OccurrenceOrderPlugin(),
53 | new CopyWebpackPlugin([
54 | { from: 'plugin.json' },
55 | { from: 'img/*' },
56 | { from: 'panel/plugin.json' },
57 | { from: 'datasource/plugin.json' },
58 | ])
59 | ],
60 | resolve: {
61 | extensions: ['.ts', '.tsx', '.js'],
62 | },
63 | module: {
64 | rules: [
65 | {
66 | test: /\.tsx?$/,
67 | loaders: [
68 | 'ts-loader'
69 | ],
70 | exclude: /node_modules/,
71 | },
72 | {
73 | test: /\.html$/,
74 | use: 'raw-loader'
75 | },
76 | {
77 | test: /jquery\.flot\.(?!events)/,
78 | loaders: [
79 | 'imports-loader?jQuery=jquery'
80 | ]
81 | },
82 | {
83 | test: /jquery\.flot\.events/,
84 | loaders: [
85 | 'imports-loader?jQuery=jquery,lodash=lodash,angular=angular,tetherDrop=tether-drop'
86 | ]
87 | }
88 | ]
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/build/webpack.dev.conf.js:
--------------------------------------------------------------------------------
1 | const baseWebpackConfig = require('./webpack.base.conf');
2 |
3 | var conf = baseWebpackConfig;
4 | conf.devtool = "source-map";
5 | conf.watch = true;
6 | conf.mode = 'development';
7 |
8 | module.exports = conf;
9 |
10 |
--------------------------------------------------------------------------------
/build/webpack.prod.conf.js:
--------------------------------------------------------------------------------
1 | const baseWebpackConfig = require('./webpack.base.conf');
2 |
3 | var conf = baseWebpackConfig;
4 | conf.mode = 'development';
5 |
6 | module.exports = baseWebpackConfig;
7 |
--------------------------------------------------------------------------------
/dist/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Volodymyr Sergeyev
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/dist/README.md:
--------------------------------------------------------------------------------
1 | # LoudML Grafana Application
2 |
3 | Visualization panel and datasource for Grafana 6.7.x - 7.x to connect with Loud ML AI solution for ICT and IoT
4 | automation. https://loudml.io
5 |
6 | 
7 |
8 | 
9 |
10 | Loud ML is an open source inference engine for metrics and events, and the fastest way to embed machine learning in your time series application. This includes APIs for storing and querying data, processing it in the background for ML or detecting outliers for alerting purposes, and more.
11 | https://github.com/regel/loudml
12 |
13 | # Installation
14 |
15 | Repository conventions:
16 |
17 | * `master` branch is for Grafana 7
18 | * `grafana/6.x` branch is for Grafana 6
19 |
20 |
21 | ZIP files has packaged plugin for each of Grafana version supported.
22 |
23 | A) Give it a try with Docker
24 |
25 | docker run -d \
26 | -p 3000:3000 \
27 | --name=grafana \
28 | -e "GF_INSTALL_PLUGINS=https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip;loudml-grafana-app" \
29 | grafana/grafana
30 |
31 | Setup LoudML if needed (please refer to https://hub.docker.com/r/loudml/loudml for config.yml setup)
32 |
33 | docker run -p 8077:8077 \
34 | -v $PWD/lib/loudml:/var/lib/loudml:rw \
35 | -v $PWD/config.yml:/etc/loudml/config.yml:ro \
36 | -ti \
37 | loudml/loudml
38 |
39 | B) In existing Grafana container
40 |
41 | * Connect to your Grafana server if necessary (e.g. via SSH).
42 | * Go to plugins directory (usually data/plugins under Grafana installation or /var/lib/grafana/plugins)
43 |
44 | cd /var/lib/grafana/plugins
45 | * Download loudml-grafana-app-latest.zip zip file:
46 |
47 | wget https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip
48 | * Unpack it there
49 |
50 | unzip loudml-grafana-app-latest.zip
51 | * You may remove the downloaded archive
52 | * Restart Grafana
53 |
54 | C) From sources (note - default `master` branch is for Grafana 7.x)
55 |
56 | * Plugin should be placed in `.../grafana/data/plugins`
57 | * git clone https://github.com/vsergeyev/loudml-grafana-app.git
58 | * cd loudml-grafana-app
59 | * yarn
60 | * yarn dev --watch
61 | * restart Grafana
62 | * LoudML app should be in plugins list, you may need to activate it
63 | * enjoy :)
64 |
65 | # Whats inside
66 |
67 | Loud ML Panel - is a version of Grafana's default Graph Panel with a "Create Baseline" button
68 | to create ML model in 1-click.
69 |
70 | Currently 1-click ML button ("Create Baseline") can produce model from:
71 |
72 | * InfluxDB datasource
73 | * OpenTSDB datasource
74 | * Elasticsearch datasource (beta)
75 | * Prometheus datasource (very draft)
76 |
77 | Loud ML Datasource - is a connector to Loud ML server. It has capabilities to show models and jobs on server. You can add new and edit existing models.
78 |
79 | # Prerequisites
80 |
81 | * Loud ML server https://github.com/regel/loudml
82 | * Grafana >= 5.4.0
83 |
84 | # Configuration
85 |
86 | In order to use Loud ML with Grafana you need to have a buckets in **loudml.yml** to reflect Grafana datasource(s) used in LoudML Graph
87 |
88 | 
89 |
90 | Example: I have InfluxDB datasource with **telegraf** database as an input and will use **loudml** database as output for ML model predictions/forecasting/anomalies:
91 |
92 | buckets:
93 | - name: loudml
94 | type: influxdb
95 | addr: 127.0.0.1:8086
96 | database: loudml
97 | retention_policy: autogen
98 | measurement: loudml
99 | annotation_db: loudmlannotations
100 | - name: influxdb1
101 | type: influxdb
102 | addr: 127.0.0.1:8086
103 | database: telegraf
104 | retention_policy: autogen
105 | measurement: loudml
106 | - name: data
107 | type: influxdb
108 | addr: 127.0.0.1:8086
109 | database: data
110 | retention_policy: autogen
111 | measurement: sinus
112 | - name: opentsdb1
113 | type: opentsdb
114 | addr: 127.0.0.1:4242
115 | retention_policy: autogen
116 | - name: prom1
117 | type: prometheus
118 | addr: 127.0.0.1:9090
119 | retention_policy: autogen
120 |
121 | InfluxDB **loudmlannotations** here specified to store annotations. (By default Loud ML server will store annotations in **chronograf** database). So on Grafana dashboard annotations/anomalies from Loud ML should be configured as:
122 |
123 | SELECT "text" FROM "autogen"."annotations" WHERE $timeFilter
124 |
125 | 
126 |
127 | # Support
128 |
129 | Please post issue to tracker or contact me via vova.sergeyev at gmail.com
130 |
131 | # Changelog
132 |
133 | * 1.7.2 Fixed compatibility issue with Grafana 7.x
134 | * 1.7.0dev Fixed issue with updating model on a server (#19). Fixed datasource config page in Grafana 7.x (#12).
135 | * 1.6.0 Better Grafana 6.x compatibility. Fixed issue with output bucket.
136 | * 1.5.0 Added capability to add and edit models on Loud ML Datasource page.
137 | * 1.4.0 Changed ID to correct format "loudml-grafana-app"; Fixes code style follow guidelines.
138 | * 1.3.0 Fixed issue #5 with fill(0); New capabilities: multiple metrics/features per ML model (for InfluxDB data).
139 | * 1.2.0 New capabilities: LoudML datasource - add scheduled job; list of scheduled jobs.
140 | * 1.1.0 Initial public release
141 |
--------------------------------------------------------------------------------
/dist/config/template.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/dist/datasource/partials/add_model.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
112 |
113 |
197 |
198 |
249 |
250 |
251 | Save Model
252 |
253 |
254 |
255 |
256 |
257 |
--------------------------------------------------------------------------------
/dist/datasource/partials/config.html:
--------------------------------------------------------------------------------
1 |
86 |
87 |
235 |
--------------------------------------------------------------------------------
/dist/datasource/partials/edit_model.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
27 |
28 |
86 |
87 |
145 |
146 |
170 |
171 |
196 |
197 |
198 | Save Model
199 |
200 |
201 |
202 |
203 |
204 |
--------------------------------------------------------------------------------
/dist/datasource/partials/query_ctrl.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/dist/datasource/partials/query_ctrl.html
--------------------------------------------------------------------------------
/dist/datasource/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "datasource",
3 | "name": "Loud ML Datasource",
4 | "id": "loudml-datasource",
5 | "metrics": true,
6 | "info": {
7 | "logos": {
8 | "small": "../loudml-grafana-app/img/logo.png",
9 | "large": "../loudml-grafana-app/img/logo.png"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/dist/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/dist/img/logo.png
--------------------------------------------------------------------------------
/dist/img/loudml_grafana_datasource.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/dist/img/loudml_grafana_datasource.png
--------------------------------------------------------------------------------
/dist/img/loudml_grafana_panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/dist/img/loudml_grafana_panel.png
--------------------------------------------------------------------------------
/dist/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Webpack App
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/dist/module.js:
--------------------------------------------------------------------------------
1 | define(function() { return /******/ (function(modules) { // webpackBootstrap
2 | /******/ // The module cache
3 | /******/ var installedModules = {};
4 | /******/
5 | /******/ // The require function
6 | /******/ function __webpack_require__(moduleId) {
7 | /******/
8 | /******/ // Check if module is in cache
9 | /******/ if(installedModules[moduleId]) {
10 | /******/ return installedModules[moduleId].exports;
11 | /******/ }
12 | /******/ // Create a new module (and put it into the cache)
13 | /******/ var module = installedModules[moduleId] = {
14 | /******/ i: moduleId,
15 | /******/ l: false,
16 | /******/ exports: {}
17 | /******/ };
18 | /******/
19 | /******/ // Execute the module function
20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
21 | /******/
22 | /******/ // Flag the module as loaded
23 | /******/ module.l = true;
24 | /******/
25 | /******/ // Return the exports of the module
26 | /******/ return module.exports;
27 | /******/ }
28 | /******/
29 | /******/
30 | /******/ // expose the modules object (__webpack_modules__)
31 | /******/ __webpack_require__.m = modules;
32 | /******/
33 | /******/ // expose the module cache
34 | /******/ __webpack_require__.c = installedModules;
35 | /******/
36 | /******/ // define getter function for harmony exports
37 | /******/ __webpack_require__.d = function(exports, name, getter) {
38 | /******/ if(!__webpack_require__.o(exports, name)) {
39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
40 | /******/ }
41 | /******/ };
42 | /******/
43 | /******/ // define __esModule on exports
44 | /******/ __webpack_require__.r = function(exports) {
45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
47 | /******/ }
48 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
49 | /******/ };
50 | /******/
51 | /******/ // create a fake namespace object
52 | /******/ // mode & 1: value is a module id, require it
53 | /******/ // mode & 2: merge all properties of value into the ns
54 | /******/ // mode & 4: return value when already ns object
55 | /******/ // mode & 8|1: behave like require
56 | /******/ __webpack_require__.t = function(value, mode) {
57 | /******/ if(mode & 1) value = __webpack_require__(value);
58 | /******/ if(mode & 8) return value;
59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
60 | /******/ var ns = Object.create(null);
61 | /******/ __webpack_require__.r(ns);
62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
64 | /******/ return ns;
65 | /******/ };
66 | /******/
67 | /******/ // getDefaultExport function for compatibility with non-harmony modules
68 | /******/ __webpack_require__.n = function(module) {
69 | /******/ var getter = module && module.__esModule ?
70 | /******/ function getDefault() { return module['default']; } :
71 | /******/ function getModuleExports() { return module; };
72 | /******/ __webpack_require__.d(getter, 'a', getter);
73 | /******/ return getter;
74 | /******/ };
75 | /******/
76 | /******/ // Object.prototype.hasOwnProperty.call
77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
78 | /******/
79 | /******/ // __webpack_public_path__
80 | /******/ __webpack_require__.p = "/";
81 | /******/
82 | /******/
83 | /******/ // Load entry module and return exports
84 | /******/ return __webpack_require__(__webpack_require__.s = "./module.ts");
85 | /******/ })
86 | /************************************************************************/
87 | /******/ ({
88 |
89 | /***/ "../node_modules/tslib/tslib.es6.js":
90 | /*!******************************************!*\
91 | !*** ../node_modules/tslib/tslib.es6.js ***!
92 | \******************************************/
93 | /*! exports provided: __extends, __assign, __rest, __decorate, __param, __metadata, __awaiter, __generator, __createBinding, __exportStar, __values, __read, __spread, __spreadArrays, __await, __asyncGenerator, __asyncDelegator, __asyncValues, __makeTemplateObject, __importStar, __importDefault, __classPrivateFieldGet, __classPrivateFieldSet */
94 | /***/ (function(module, __webpack_exports__, __webpack_require__) {
95 |
96 | "use strict";
97 | __webpack_require__.r(__webpack_exports__);
98 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__extends", function() { return __extends; });
99 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__assign", function() { return __assign; });
100 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__rest", function() { return __rest; });
101 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__decorate", function() { return __decorate; });
102 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__param", function() { return __param; });
103 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__metadata", function() { return __metadata; });
104 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__awaiter", function() { return __awaiter; });
105 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__generator", function() { return __generator; });
106 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__createBinding", function() { return __createBinding; });
107 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__exportStar", function() { return __exportStar; });
108 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__values", function() { return __values; });
109 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__read", function() { return __read; });
110 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__spread", function() { return __spread; });
111 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__spreadArrays", function() { return __spreadArrays; });
112 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__await", function() { return __await; });
113 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__asyncGenerator", function() { return __asyncGenerator; });
114 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__asyncDelegator", function() { return __asyncDelegator; });
115 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__asyncValues", function() { return __asyncValues; });
116 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__makeTemplateObject", function() { return __makeTemplateObject; });
117 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__importStar", function() { return __importStar; });
118 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__importDefault", function() { return __importDefault; });
119 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__classPrivateFieldGet", function() { return __classPrivateFieldGet; });
120 | /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "__classPrivateFieldSet", function() { return __classPrivateFieldSet; });
121 | /*! *****************************************************************************
122 | Copyright (c) Microsoft Corporation.
123 |
124 | Permission to use, copy, modify, and/or distribute this software for any
125 | purpose with or without fee is hereby granted.
126 |
127 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
128 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
129 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
130 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
131 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
132 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
133 | PERFORMANCE OF THIS SOFTWARE.
134 | ***************************************************************************** */
135 | /* global Reflect, Promise */
136 |
137 | var extendStatics = function(d, b) {
138 | extendStatics = Object.setPrototypeOf ||
139 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
140 | function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
141 | return extendStatics(d, b);
142 | };
143 |
144 | function __extends(d, b) {
145 | extendStatics(d, b);
146 | function __() { this.constructor = d; }
147 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
148 | }
149 |
150 | var __assign = function() {
151 | __assign = Object.assign || function __assign(t) {
152 | for (var s, i = 1, n = arguments.length; i < n; i++) {
153 | s = arguments[i];
154 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
155 | }
156 | return t;
157 | }
158 | return __assign.apply(this, arguments);
159 | }
160 |
161 | function __rest(s, e) {
162 | var t = {};
163 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
164 | t[p] = s[p];
165 | if (s != null && typeof Object.getOwnPropertySymbols === "function")
166 | for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
167 | if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
168 | t[p[i]] = s[p[i]];
169 | }
170 | return t;
171 | }
172 |
173 | function __decorate(decorators, target, key, desc) {
174 | var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
175 | if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
176 | else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
177 | return c > 3 && r && Object.defineProperty(target, key, r), r;
178 | }
179 |
180 | function __param(paramIndex, decorator) {
181 | return function (target, key) { decorator(target, key, paramIndex); }
182 | }
183 |
184 | function __metadata(metadataKey, metadataValue) {
185 | if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(metadataKey, metadataValue);
186 | }
187 |
188 | function __awaiter(thisArg, _arguments, P, generator) {
189 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
190 | return new (P || (P = Promise))(function (resolve, reject) {
191 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
192 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
193 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
194 | step((generator = generator.apply(thisArg, _arguments || [])).next());
195 | });
196 | }
197 |
198 | function __generator(thisArg, body) {
199 | var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
200 | return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
201 | function verb(n) { return function (v) { return step([n, v]); }; }
202 | function step(op) {
203 | if (f) throw new TypeError("Generator is already executing.");
204 | while (_) try {
205 | if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
206 | if (y = 0, t) op = [op[0] & 2, t.value];
207 | switch (op[0]) {
208 | case 0: case 1: t = op; break;
209 | case 4: _.label++; return { value: op[1], done: false };
210 | case 5: _.label++; y = op[1]; op = [0]; continue;
211 | case 7: op = _.ops.pop(); _.trys.pop(); continue;
212 | default:
213 | if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
214 | if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
215 | if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
216 | if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
217 | if (t[2]) _.ops.pop();
218 | _.trys.pop(); continue;
219 | }
220 | op = body.call(thisArg, _);
221 | } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
222 | if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
223 | }
224 | }
225 |
226 | function __createBinding(o, m, k, k2) {
227 | if (k2 === undefined) k2 = k;
228 | o[k2] = m[k];
229 | }
230 |
231 | function __exportStar(m, exports) {
232 | for (var p in m) if (p !== "default" && !exports.hasOwnProperty(p)) exports[p] = m[p];
233 | }
234 |
235 | function __values(o) {
236 | var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0;
237 | if (m) return m.call(o);
238 | if (o && typeof o.length === "number") return {
239 | next: function () {
240 | if (o && i >= o.length) o = void 0;
241 | return { value: o && o[i++], done: !o };
242 | }
243 | };
244 | throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
245 | }
246 |
247 | function __read(o, n) {
248 | var m = typeof Symbol === "function" && o[Symbol.iterator];
249 | if (!m) return o;
250 | var i = m.call(o), r, ar = [], e;
251 | try {
252 | while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
253 | }
254 | catch (error) { e = { error: error }; }
255 | finally {
256 | try {
257 | if (r && !r.done && (m = i["return"])) m.call(i);
258 | }
259 | finally { if (e) throw e.error; }
260 | }
261 | return ar;
262 | }
263 |
264 | function __spread() {
265 | for (var ar = [], i = 0; i < arguments.length; i++)
266 | ar = ar.concat(__read(arguments[i]));
267 | return ar;
268 | }
269 |
270 | function __spreadArrays() {
271 | for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;
272 | for (var r = Array(s), k = 0, i = 0; i < il; i++)
273 | for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)
274 | r[k] = a[j];
275 | return r;
276 | };
277 |
278 | function __await(v) {
279 | return this instanceof __await ? (this.v = v, this) : new __await(v);
280 | }
281 |
282 | function __asyncGenerator(thisArg, _arguments, generator) {
283 | if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
284 | var g = generator.apply(thisArg, _arguments || []), i, q = [];
285 | return i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i;
286 | function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }
287 | function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }
288 | function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }
289 | function fulfill(value) { resume("next", value); }
290 | function reject(value) { resume("throw", value); }
291 | function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }
292 | }
293 |
294 | function __asyncDelegator(o) {
295 | var i, p;
296 | return i = {}, verb("next"), verb("throw", function (e) { throw e; }), verb("return"), i[Symbol.iterator] = function () { return this; }, i;
297 | function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === "return" } : f ? f(v) : v; } : f; }
298 | }
299 |
300 | function __asyncValues(o) {
301 | if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
302 | var m = o[Symbol.asyncIterator], i;
303 | return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
304 | function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
305 | function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
306 | }
307 |
308 | function __makeTemplateObject(cooked, raw) {
309 | if (Object.defineProperty) { Object.defineProperty(cooked, "raw", { value: raw }); } else { cooked.raw = raw; }
310 | return cooked;
311 | };
312 |
313 | function __importStar(mod) {
314 | if (mod && mod.__esModule) return mod;
315 | var result = {};
316 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
317 | result.default = mod;
318 | return result;
319 | }
320 |
321 | function __importDefault(mod) {
322 | return (mod && mod.__esModule) ? mod : { default: mod };
323 | }
324 |
325 | function __classPrivateFieldGet(receiver, privateMap) {
326 | if (!privateMap.has(receiver)) {
327 | throw new TypeError("attempted to get private field on non-instance");
328 | }
329 | return privateMap.get(receiver);
330 | }
331 |
332 | function __classPrivateFieldSet(receiver, privateMap, value) {
333 | if (!privateMap.has(receiver)) {
334 | throw new TypeError("attempted to set private field on non-instance");
335 | }
336 | privateMap.set(receiver, value);
337 | return value;
338 | }
339 |
340 |
341 | /***/ }),
342 |
343 | /***/ "./config/config_ctrl.ts":
344 | /*!*******************************!*\
345 | !*** ./config/config_ctrl.ts ***!
346 | \*******************************/
347 | /*! no static exports found */
348 | /***/ (function(module, exports, __webpack_require__) {
349 |
350 | "use strict";
351 |
352 |
353 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
354 |
355 | Object.defineProperty(exports, "__esModule", {
356 | value: true
357 | });
358 |
359 | var tslib_1 = __webpack_require__(/*! tslib */ "../node_modules/tslib/tslib.es6.js");
360 |
361 | var template_html_1 = tslib_1.__importDefault(__webpack_require__(/*! ./template.html */ "./config/template.html"));
362 |
363 | var ConfigCtrl = function ConfigCtrl() {
364 | _classCallCheck(this, ConfigCtrl);
365 |
366 | if (this.appModel.jsonData === undefined) {
367 | this.appModel.jsonData = {};
368 | }
369 | };
370 |
371 | exports.ConfigCtrl = ConfigCtrl;
372 | ConfigCtrl.template = template_html_1["default"];
373 |
374 | /***/ }),
375 |
376 | /***/ "./config/template.html":
377 | /*!******************************!*\
378 | !*** ./config/template.html ***!
379 | \******************************/
380 | /*! no static exports found */
381 | /***/ (function(module, exports) {
382 |
383 | module.exports = "\r\n";
384 |
385 | /***/ }),
386 |
387 | /***/ "./module.ts":
388 | /*!*******************!*\
389 | !*** ./module.ts ***!
390 | \*******************/
391 | /*! no static exports found */
392 | /***/ (function(module, exports, __webpack_require__) {
393 |
394 | "use strict";
395 |
396 |
397 | Object.defineProperty(exports, "__esModule", {
398 | value: true
399 | });
400 |
401 | var config_ctrl_1 = __webpack_require__(/*! ./config/config_ctrl */ "./config/config_ctrl.ts");
402 |
403 | exports.ConfigCtrl = config_ctrl_1.ConfigCtrl;
404 |
405 | /***/ })
406 |
407 | /******/ })});;
408 | //# sourceMappingURL=module.js.map
--------------------------------------------------------------------------------
/dist/module.js.map:
--------------------------------------------------------------------------------
1 | {"version":3,"sources":["webpack:///webpack/bootstrap","webpack:///../node_modules/tslib/tslib.es6.js","webpack:///./config/config_ctrl.ts","webpack:///./config/template.html","webpack:///./module.ts"],"names":[],"mappings":";QAAA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;;QAEA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;;;QAGA;QACA;;QAEA;QACA;;QAEA;QACA;QACA;QACA,0CAA0C,gCAAgC;QAC1E;QACA;;QAEA;QACA;QACA;QACA,wDAAwD,kBAAkB;QAC1E;QACA,iDAAiD,cAAc;QAC/D;;QAEA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA,yCAAyC,iCAAiC;QAC1E,gHAAgH,mBAAmB,EAAE;QACrI;QACA;;QAEA;QACA;QACA;QACA,2BAA2B,0BAA0B,EAAE;QACvD,iCAAiC,eAAe;QAChD;QACA;QACA;;QAEA;QACA,sDAAsD,+DAA+D;;QAErH;QACA;;;QAGA;QACA;;;;;;;;;;;;;AClFA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA,UAAU,gBAAgB,sCAAsC,iBAAiB,EAAE;AACnF,yBAAyB,uDAAuD;AAChF;AACA;;AAEO;AACP;AACA,mBAAmB,sBAAsB;AACzC;AACA;;AAEO;AACP;AACA,gDAAgD,OAAO;AACvD;AACA;AACA;AACA;AACA;AACA;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA,4DAA4D,cAAc;AAC1E;AACA;AACA;AACA;AACA;;AAEO;AACP;AACA;AACA,4CAA4C,QAAQ;AACpD;AACA;;AAEO;AACP,mCAAmC,oCAAoC;AACvE;;AAEO;AACP;AACA;;AAEO;AACP,2BAA2B,+DAA+D,gBAAgB,EAAE,EAAE;AAC9G;AACA,mCAAmC,MAAM,6BAA6B,EAAE,YAAY,WAAW,EAAE;AACjG,kCAAkC,MAAM,iCAAiC,EAAE,YAAY,WAAW,EAAE;AACpG,+BAA+B,qFAAqF;AACpH;AACA,KAAK;AACL;;AAEO;AACP,aAAa,6BAA6B,0BAA0B,aAAa,EAAE,qBAAqB;AACxG,gBAAgB,qDAAqD,oEAAoE,aAAa,EAAE;AACxJ,sBAAsB,sBAAsB,qBAAqB,GAAG;AACpE;AACA;AACA;AACA;AACA;AACA;AACA,uCAAuC;AACvC,kCAAkC,SAAS;AAC3C,kCAAkC,WAAW,UAAU;AACvD,yCAAyC,cAAc;AACvD;AACA,6GAA6G,OAAO,UAAU;AAC9H,gFAAgF,iBAAiB,OAAO;AACxG,wDAAwD,gBAAgB,QAAQ,OAAO;AACvF,8CAA8C,gBAAgB,gBAAgB,OAAO;AACrF;AACA,iCAAiC;AACjC;AACA;AACA,SAAS,YAAY,aAAa,OAAO,EAAE,UAAU,WAAW;AAChE,mCAAmC,SAAS;AAC5C;AACA;;AAEO;AACP;AACA;AACA;;AAEO;AACP;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA,oBAAoB;AACpB;AACA;AACA;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;AACA,mBAAmB,MAAM,gBAAgB;AACzC;AACA;AACA;AACA;AACA,iBAAiB,sBAAsB;AACvC;AACA;AACA;;AAEO;AACP,4BAA4B,sBAAsB;AAClD;AACA;AACA;;AAEO;AACP,iDAAiD,QAAQ;AACzD,wCAAwC,QAAQ;AAChD,wDAAwD,QAAQ;AAChE;AACA;AACA;;AAEO;AACP;AACA;;AAEO;AACP;AACA;AACA,iBAAiB,sFAAsF,aAAa,EAAE;AACtH,sBAAsB,gCAAgC,qCAAqC,0CAA0C,EAAE,EAAE,GAAG;AAC5I,2BAA2B,MAAM,eAAe,EAAE,YAAY,oBAAoB,EAAE;AACpF,sBAAsB,oGAAoG;AAC1H,6BAA6B,uBAAuB;AACpD,4BAA4B,wBAAwB;AACpD,2BAA2B,yDAAyD;AACpF;;AAEO;AACP;AACA,iBAAiB,4CAA4C,SAAS,EAAE,qDAAqD,aAAa,EAAE;AAC5I,yBAAyB,6BAA6B,oBAAoB,gDAAgD,gBAAgB,EAAE,KAAK;AACjJ;;AAEO;AACP;AACA;AACA,2GAA2G,sFAAsF,aAAa,EAAE;AAChN,sBAAsB,8BAA8B,gDAAgD,uDAAuD,EAAE,EAAE,GAAG;AAClK,4CAA4C,sCAAsC,UAAU,oBAAoB,EAAE,EAAE,UAAU;AAC9H;;AAEO;AACP,gCAAgC,uCAAuC,aAAa,EAAE,EAAE,OAAO,kBAAkB;AACjH;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;;AAEO;AACP,4CAA4C;AAC5C;;AAEO;AACP;AACA;AACA;AACA;AACA;;AAEO;AACP;AACA;AACA;AACA;AACA;AACA;;;;;;;;;;;;;;;;;;;;;;;ACzNA;;IAEM,U,GAIJ;AAAA;;AACE,MAAI,KAAK,QAAL,CAAc,QAAd,KAA2B,SAA/B,EAA0C;AACxC,SAAK,QAAL,CAAc,QAAd,GAAyB,EAAzB;AACD;AACF,C;;AAGM;AAVA,sBAAW,0BAAX,C;;;;;;;;;;;ACHT,wB;;;;;;;;;;;;;;;;;;ACAA;;AAES,qBAFA,wBAEA,C","file":"module.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = \"./module.ts\");\n","/*! *****************************************************************************\r\nCopyright (c) Microsoft Corporation.\r\n\r\nPermission to use, copy, modify, and/or distribute this software for any\r\npurpose with or without fee is hereby granted.\r\n\r\nTHE SOFTWARE IS PROVIDED \"AS IS\" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH\r\nREGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY\r\nAND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,\r\nINDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM\r\nLOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR\r\nOTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR\r\nPERFORMANCE OF THIS SOFTWARE.\r\n***************************************************************************** */\r\n/* global Reflect, Promise */\r\n\r\nvar extendStatics = function(d, b) {\r\n extendStatics = Object.setPrototypeOf ||\r\n ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||\r\n function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };\r\n return extendStatics(d, b);\r\n};\r\n\r\nexport function __extends(d, b) {\r\n extendStatics(d, b);\r\n function __() { this.constructor = d; }\r\n d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());\r\n}\r\n\r\nexport var __assign = function() {\r\n __assign = Object.assign || function __assign(t) {\r\n for (var s, i = 1, n = arguments.length; i < n; i++) {\r\n s = arguments[i];\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];\r\n }\r\n return t;\r\n }\r\n return __assign.apply(this, arguments);\r\n}\r\n\r\nexport function __rest(s, e) {\r\n var t = {};\r\n for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)\r\n t[p] = s[p];\r\n if (s != null && typeof Object.getOwnPropertySymbols === \"function\")\r\n for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {\r\n if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))\r\n t[p[i]] = s[p[i]];\r\n }\r\n return t;\r\n}\r\n\r\nexport function __decorate(decorators, target, key, desc) {\r\n var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;\r\n if (typeof Reflect === \"object\" && typeof Reflect.decorate === \"function\") r = Reflect.decorate(decorators, target, key, desc);\r\n else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;\r\n return c > 3 && r && Object.defineProperty(target, key, r), r;\r\n}\r\n\r\nexport function __param(paramIndex, decorator) {\r\n return function (target, key) { decorator(target, key, paramIndex); }\r\n}\r\n\r\nexport function __metadata(metadataKey, metadataValue) {\r\n if (typeof Reflect === \"object\" && typeof Reflect.metadata === \"function\") return Reflect.metadata(metadataKey, metadataValue);\r\n}\r\n\r\nexport function __awaiter(thisArg, _arguments, P, generator) {\r\n function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }\r\n return new (P || (P = Promise))(function (resolve, reject) {\r\n function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }\r\n function rejected(value) { try { step(generator[\"throw\"](value)); } catch (e) { reject(e); } }\r\n function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }\r\n step((generator = generator.apply(thisArg, _arguments || [])).next());\r\n });\r\n}\r\n\r\nexport function __generator(thisArg, body) {\r\n var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;\r\n return g = { next: verb(0), \"throw\": verb(1), \"return\": verb(2) }, typeof Symbol === \"function\" && (g[Symbol.iterator] = function() { return this; }), g;\r\n function verb(n) { return function (v) { return step([n, v]); }; }\r\n function step(op) {\r\n if (f) throw new TypeError(\"Generator is already executing.\");\r\n while (_) try {\r\n if (f = 1, y && (t = op[0] & 2 ? y[\"return\"] : op[0] ? y[\"throw\"] || ((t = y[\"return\"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;\r\n if (y = 0, t) op = [op[0] & 2, t.value];\r\n switch (op[0]) {\r\n case 0: case 1: t = op; break;\r\n case 4: _.label++; return { value: op[1], done: false };\r\n case 5: _.label++; y = op[1]; op = [0]; continue;\r\n case 7: op = _.ops.pop(); _.trys.pop(); continue;\r\n default:\r\n if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }\r\n if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }\r\n if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }\r\n if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }\r\n if (t[2]) _.ops.pop();\r\n _.trys.pop(); continue;\r\n }\r\n op = body.call(thisArg, _);\r\n } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }\r\n if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };\r\n }\r\n}\r\n\r\nexport function __createBinding(o, m, k, k2) {\r\n if (k2 === undefined) k2 = k;\r\n o[k2] = m[k];\r\n}\r\n\r\nexport function __exportStar(m, exports) {\r\n for (var p in m) if (p !== \"default\" && !exports.hasOwnProperty(p)) exports[p] = m[p];\r\n}\r\n\r\nexport function __values(o) {\r\n var s = typeof Symbol === \"function\" && Symbol.iterator, m = s && o[s], i = 0;\r\n if (m) return m.call(o);\r\n if (o && typeof o.length === \"number\") return {\r\n next: function () {\r\n if (o && i >= o.length) o = void 0;\r\n return { value: o && o[i++], done: !o };\r\n }\r\n };\r\n throw new TypeError(s ? \"Object is not iterable.\" : \"Symbol.iterator is not defined.\");\r\n}\r\n\r\nexport function __read(o, n) {\r\n var m = typeof Symbol === \"function\" && o[Symbol.iterator];\r\n if (!m) return o;\r\n var i = m.call(o), r, ar = [], e;\r\n try {\r\n while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);\r\n }\r\n catch (error) { e = { error: error }; }\r\n finally {\r\n try {\r\n if (r && !r.done && (m = i[\"return\"])) m.call(i);\r\n }\r\n finally { if (e) throw e.error; }\r\n }\r\n return ar;\r\n}\r\n\r\nexport function __spread() {\r\n for (var ar = [], i = 0; i < arguments.length; i++)\r\n ar = ar.concat(__read(arguments[i]));\r\n return ar;\r\n}\r\n\r\nexport function __spreadArrays() {\r\n for (var s = 0, i = 0, il = arguments.length; i < il; i++) s += arguments[i].length;\r\n for (var r = Array(s), k = 0, i = 0; i < il; i++)\r\n for (var a = arguments[i], j = 0, jl = a.length; j < jl; j++, k++)\r\n r[k] = a[j];\r\n return r;\r\n};\r\n\r\nexport function __await(v) {\r\n return this instanceof __await ? (this.v = v, this) : new __await(v);\r\n}\r\n\r\nexport function __asyncGenerator(thisArg, _arguments, generator) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var g = generator.apply(thisArg, _arguments || []), i, q = [];\r\n return i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i;\r\n function verb(n) { if (g[n]) i[n] = function (v) { return new Promise(function (a, b) { q.push([n, v, a, b]) > 1 || resume(n, v); }); }; }\r\n function resume(n, v) { try { step(g[n](v)); } catch (e) { settle(q[0][3], e); } }\r\n function step(r) { r.value instanceof __await ? Promise.resolve(r.value.v).then(fulfill, reject) : settle(q[0][2], r); }\r\n function fulfill(value) { resume(\"next\", value); }\r\n function reject(value) { resume(\"throw\", value); }\r\n function settle(f, v) { if (f(v), q.shift(), q.length) resume(q[0][0], q[0][1]); }\r\n}\r\n\r\nexport function __asyncDelegator(o) {\r\n var i, p;\r\n return i = {}, verb(\"next\"), verb(\"throw\", function (e) { throw e; }), verb(\"return\"), i[Symbol.iterator] = function () { return this; }, i;\r\n function verb(n, f) { i[n] = o[n] ? function (v) { return (p = !p) ? { value: __await(o[n](v)), done: n === \"return\" } : f ? f(v) : v; } : f; }\r\n}\r\n\r\nexport function __asyncValues(o) {\r\n if (!Symbol.asyncIterator) throw new TypeError(\"Symbol.asyncIterator is not defined.\");\r\n var m = o[Symbol.asyncIterator], i;\r\n return m ? m.call(o) : (o = typeof __values === \"function\" ? __values(o) : o[Symbol.iterator](), i = {}, verb(\"next\"), verb(\"throw\"), verb(\"return\"), i[Symbol.asyncIterator] = function () { return this; }, i);\r\n function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }\r\n function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }\r\n}\r\n\r\nexport function __makeTemplateObject(cooked, raw) {\r\n if (Object.defineProperty) { Object.defineProperty(cooked, \"raw\", { value: raw }); } else { cooked.raw = raw; }\r\n return cooked;\r\n};\r\n\r\nexport function __importStar(mod) {\r\n if (mod && mod.__esModule) return mod;\r\n var result = {};\r\n if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];\r\n result.default = mod;\r\n return result;\r\n}\r\n\r\nexport function __importDefault(mod) {\r\n return (mod && mod.__esModule) ? mod : { default: mod };\r\n}\r\n\r\nexport function __classPrivateFieldGet(receiver, privateMap) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to get private field on non-instance\");\r\n }\r\n return privateMap.get(receiver);\r\n}\r\n\r\nexport function __classPrivateFieldSet(receiver, privateMap, value) {\r\n if (!privateMap.has(receiver)) {\r\n throw new TypeError(\"attempted to set private field on non-instance\");\r\n }\r\n privateMap.set(receiver, value);\r\n return value;\r\n}\r\n","import template from './template.html';\r\n\r\nclass ConfigCtrl {\r\n static template = template;\r\n appModel: any;\r\n\r\n constructor() {\r\n if (this.appModel.jsonData === undefined) {\r\n this.appModel.jsonData = {};\r\n }\r\n }\r\n}\r\n\r\nexport { ConfigCtrl };\r\n","module.exports = \"\\r\\n\";","import { ConfigCtrl } from './config/config_ctrl';\r\n\r\nexport { ConfigCtrl };\r\n"],"sourceRoot":""}
--------------------------------------------------------------------------------
/dist/panel/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "panel",
3 | "name": "Loud ML Graph",
4 | "id": "loudml-graph-panel",
5 | "info": {
6 | "logos": {
7 | "small": "../loudml-grafana-app/img/logo.png",
8 | "large": "../loudml-grafana-app/img/logo.png"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/dist/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "app",
3 | "name": "Loud ML app",
4 | "id": "loudml-grafana-app",
5 |
6 | "info": {
7 | "description": "Loud ML app: Visualization panel and datasource for Grafana to connect with Loud ML AI",
8 | "author": {
9 | "name": "Volodymyr Sergeyev"
10 | },
11 | "keywords": ["LoudML", "ML", "AI"],
12 | "logos": {
13 | "small": "img/logo.png",
14 | "large": "img/logo.png"
15 | },
16 | "links": [
17 | {"name": "Website", "url": "https://github.com/vsergeyev/loudml-grafana-app"},
18 | {"name": "License", "url": "https://github.com/vsergeyev/loudml-grafana-app/blob/master/LICENSE"}
19 | ],
20 | "screenshots": [
21 | {"name": "LoudML graph panel", "path": "img/loudml_grafana_panel.png"},
22 | {"name": "LoudML datasource", "path": "img/loudml_grafana_datasource.png"}
23 | ],
24 | "version": "1.7.2",
25 | "updated": "2021-02-01"
26 | },
27 |
28 | "includes": [
29 | { "type": "panel", "name": "Loud ML Graph Panel" },
30 | { "type": "datasource", "name": "Loud ML Datasource" }
31 | ],
32 |
33 | "dependencies": {
34 | "grafanaDependency": ">=7.0.0",
35 | "plugins": []
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | LoudML Grafana Application
2 | ==========================
3 | https://github.com/vsergeyev/loudml-grafana-app
4 |
5 | Datasource and Graph panel visualization to connect with Loud ML Machine Learning server.
6 |
7 | 
8 |
9 | Create a ML models in 1-click with "Create Baseline" button on graph.
10 |
11 | * select a time frame
12 | * click "Create Baseline"
13 | * train model
14 | * run a prediction
15 | * observe anomalies (as annotations on graph)
16 |
17 | Currently 1-click ML button ("Create Baseline") can produce model from:
18 |
19 | * InfluxDB datasource
20 | * OpenTSDB datasource
21 | * Elasticsearch datasource (beta)
22 | * Prometheus datasource (very draft)
23 |
24 | Watch this video on YouTube:
25 |
26 |
29 |
30 | Loud ML use a Tensor Flow and Keras as a backend. It works with VAE models, combines the best of unsupervised and supervised learning. Based on a work "Unsupervised Anomaly Detection via Variational Auto-Encoderfor Seasonal KPIs in Web Applications" algorythm is best suitable to closely monitor various KPIs (DB connections, page views, number of online users, number of orders, etc).
31 |
32 | Per ML algorythm documentation:
33 |
34 | `Donut is an unsupervisedanomaly detection algorithm based on VAE. It greatly outperforms a state-of-arts super-vised ensemble approach and a baseline VAE approach, and its best F-scores range from 0.75 to 0.9 for the studied KPIs from a top global Internet company. ... Unlike discriminative models which are designed for just one pur-pose (e.g., a classifier is designed for just computing the classifi-cation probabilityp(y|x)), generative models like VAE can derivevarious outputs. `
35 |
36 | # Installation
37 |
38 | Default assumption - you use Grafana 6.x. For Grafana 7.x please use appropriate ZIP file.
39 |
40 | A) Give it a try with Docker
41 |
42 | docker run -d \
43 | -p 3000:3000 \
44 | --name=grafana \
45 | -e "GF_INSTALL_PLUGINS=https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip;loudml-grafana-app" \
46 | grafana/grafana
47 |
48 | Setup LoudML if needed (please refer to https://hub.docker.com/r/loudml/loudml for config.yml setup)
49 |
50 | docker run -p 8077:8077 \
51 | -v $PWD/lib/loudml:/var/lib/loudml:rw \
52 | -v $PWD/config.yml:/etc/loudml/config.yml:ro \
53 | -ti \
54 | loudml/loudml
55 |
56 | B) In existing Grafana container
57 |
58 | * Connect to your Grafana server if necessary (e.g. via SSH).
59 | * Go to plugins directory (usually data/plugins under Grafana installation or /var/lib/grafana/plugins)
60 |
61 | cd /var/lib/grafana/plugins
62 | * Download loudml-grafana-app-latest.zip zip file:
63 |
64 | wget https://github.com/vsergeyev/loudml-grafana-app/raw/master/loudml-grafana-app-latest.zip
65 | * Unpack it there
66 |
67 | unzip loudml-grafana-app-latest.zip
68 | * You may remove the downloaded archive
69 | * Restart Grafana
70 |
71 | C) From sources (note - github latest is for Grafana 7.x)
72 |
73 | * Plugin should be placed in `.../grafana/data/plugins`
74 | * git clone https://github.com/vsergeyev/loudml-grafana-app.git
75 | * cd loudml-grafana-app
76 | * yarn
77 | * yarn dev --watch
78 | * restart Grafana
79 | * LoudML app should be in plugins list, you may need to activate it
80 | * enjoy :)
81 |
82 | # Prerequisites
83 |
84 | * Loud ML server https://github.com/regel/loudml
85 | * Grafana >= 5.4.0
86 |
87 | # Configuration
88 |
89 | In order to use Loud ML with Grafana you need to have a buckets in **loudml.yml** to reflect Grafana datasource(s) used in LoudML Graph
90 |
91 | 
92 |
93 | Example: I have InfluxDB datasource with **telegraf** database as an input and will use **loudml** database as output for ML model predictions/forecasting/anomalies:
94 |
95 | buckets:
96 | - name: loudml
97 | type: influxdb
98 | addr: 127.0.0.1:8086
99 | database: loudml
100 | retention_policy: autogen
101 | measurement: loudml
102 | annotation_db: loudmlannotations
103 | - name: influxdb1
104 | type: influxdb
105 | addr: 127.0.0.1:8086
106 | database: telegraf
107 | retention_policy: autogen
108 | measurement: loudml
109 | - name: data
110 | type: influxdb
111 | addr: 127.0.0.1:8086
112 | database: data
113 | retention_policy: autogen
114 | measurement: sinus
115 | - name: opentsdb1
116 | type: opentsdb
117 | addr: 127.0.0.1:4242
118 | retention_policy: autogen
119 | - name: prom1
120 | type: prometheus
121 | addr: 127.0.0.1:9090
122 | retention_policy: autogen
123 |
124 | InfluxDB **loudmlannotations** here specified to store annotations. (By default Loud ML server will store annotations in **chronograf** database). So on Grafana dashboard annotations/anomalies from Loud ML should be configured as:
125 |
126 | SELECT "text" FROM "autogen"."annotations" WHERE $timeFilter
127 |
128 | 
129 |
130 | # Links
131 |
132 | * Creating a model for system usage metric
133 | * Loud ML
134 | * Unsupervised Anomaly Detection via Variational Auto-Encoderfor Seasonal KPIs in Web Applications
135 | * Forecasting time series with 1-click machine learning
136 | * Applying Machine Learning Models to InfluxDB
137 |
138 | # Changelog
139 |
140 | * 1.6.0 Better Grafana 6.x compatibility. Fixed issue with output bucket.
141 | * 1.5.0 Added capability to add and edit models on Loud ML Datasource page.
142 | * 1.4.0 Changed ID to correct format "loudml-grafana-app"; Fixes code style follow guidelines.
143 | * 1.3.0 Fixed issue #5 with fill(0); New capabilities: multiple metrics/features per ML model (for InfluxDB data).
144 | * 1.2.0 New capabilities: LoudML datasource - add scheduled job; list of scheduled jobs.
145 | * 1.1.0 Initial public release
146 |
--------------------------------------------------------------------------------
/docs/_config.yml:
--------------------------------------------------------------------------------
1 | theme: jekyll-theme-slate
--------------------------------------------------------------------------------
/docs/loudml_annotations.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_annotations.png
--------------------------------------------------------------------------------
/docs/loudml_grafana_app.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_grafana_app.png
--------------------------------------------------------------------------------
/docs/loudml_grafana_datasource.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_grafana_datasource.png
--------------------------------------------------------------------------------
/docs/loudml_grafana_panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_grafana_panel.png
--------------------------------------------------------------------------------
/docs/loudml_props.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/docs/loudml_props.png
--------------------------------------------------------------------------------
/grafana-loudml-app-1.1.0.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/grafana-loudml-app-1.1.0.zip
--------------------------------------------------------------------------------
/grafana-loudml-app-1.2.0.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/grafana-loudml-app-1.2.0.zip
--------------------------------------------------------------------------------
/grafana-loudml-app-1.3.0.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/grafana-loudml-app-1.3.0.zip
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | // This file is needed because it is used by vscode and other tools that
2 | // call `jest` directly. However, unless you are doing anything special
3 | // do not edit this file
4 |
5 | const standard = require('@grafana/toolkit/src/config/jest.plugin.config');
6 |
7 | // This process will use the same config that `yarn test` is using
8 | module.exports = standard.jestConfig();
9 |
--------------------------------------------------------------------------------
/loudml-grafana-app-1.4.0-grafana7.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.4.0-grafana7.zip
--------------------------------------------------------------------------------
/loudml-grafana-app-1.4.0.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.4.0.zip
--------------------------------------------------------------------------------
/loudml-grafana-app-1.6.0-grafana6.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.6.0-grafana6.zip
--------------------------------------------------------------------------------
/loudml-grafana-app-1.7.0dev-grafana6.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.7.0dev-grafana6.zip
--------------------------------------------------------------------------------
/loudml-grafana-app-1.7.1.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.7.1.zip
--------------------------------------------------------------------------------
/loudml-grafana-app-1.7.2.zip:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/loudml-grafana-app-1.7.2.zip
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "loudml-grafana-app",
3 | "version": "1.7.2",
4 | "description": "Loud ML app: visualization panel for Grafana to connect with Loud ML AI",
5 | "scripts": {
6 | "build": "grafana-toolkit plugin:build",
7 | "test": "grafana-toolkit plugin:test",
8 | "dev": "grafana-toolkit plugin:dev",
9 | "watch": "grafana-toolkit plugin:dev --watch"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "git+https://github.com/vsergeyev/loudml-grafana-app"
14 | },
15 | "author": "Volodymyr Sergeyev ",
16 | "license": "MIT",
17 | "bugs": {
18 | "url": "https://github.com/vsergeyev/loudml-grafana-app/issues",
19 | "email": "vova.sergeyev@gmail.com"
20 | },
21 | "devDependencies": {
22 | "react": ">=16.8.0",
23 | "react-dom": ">=16.8.0",
24 | "slate": ">=0.55.0",
25 | "@grafana/data": "^6.7.1",
26 | "@grafana/runtime": "^6.7.1",
27 | "@grafana/toolkit": "^6.7.1",
28 | "@grafana/ui": "^6.7.1",
29 | "@babel/core": "^7.0.0-0",
30 | "slate-react": ">=0.22.0",
31 | "babel-loader": ">=8.1.0",
32 | "webpack": ">=2",
33 | "@babel/plugin-proposal-numeric-separator": "^7.8.3"
34 | },
35 | "dependencies": {
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/config/config_ctrl.ts:
--------------------------------------------------------------------------------
1 | import template from './template.html';
2 |
3 | class ConfigCtrl {
4 | static template = template;
5 | appModel: any;
6 |
7 | constructor() {
8 | if (this.appModel.jsonData === undefined) {
9 | this.appModel.jsonData = {};
10 | }
11 | }
12 | }
13 |
14 | export { ConfigCtrl };
15 |
--------------------------------------------------------------------------------
/src/config/template.html:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/datasource/config_ctrl.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { getDataSourceSrv } from '@grafana/runtime';
3 | import { AppEvents } from '@grafana/data';
4 | import { CoreEvents } from 'grafana/app/types';
5 | import appEvents from 'grafana/app/core/app_events';
6 |
7 | import LoudMLAPI from './loudml_api';
8 | import configTemplate from './partials/config.html';
9 | import LoudMLDatasource from './datasource';
10 | import { DEFAULT_MODEL, DEFAULT_JOB, DEFAULT_FEATURE, ANOMALY_HOOK_NAME, ANOMALY_HOOK } from './types';
11 |
12 | const POST_A_BUG_SAVE_PLANET =
13 | 'Be aware, it may be an alien bug. If so - save planet, post bug report at https://github.com/vsergeyev/loudml-grafana-app/issues (starship troopers will do the rest of bloody job for you)';
14 |
15 | function sorry_its_error(err) {
16 | // Guys, it's really sorry
17 | window.console.log('Model update error.');
18 | window.console.log(POST_A_BUG_SAVE_PLANET);
19 | window.console.log(err);
20 | appEvents.emit(AppEvents.alertError, ['Model update error', err.data]);
21 | }
22 |
23 | export class LoudMLConfigCtrl {
24 | static template = configTemplate;
25 | ACCESS_OPTIONS = [
26 | { key: 'proxy', value: 'Server (Default)' },
27 | { key: 'direct', value: 'Browser' },
28 | ];
29 |
30 | current: any;
31 |
32 | showAccessHelp = false;
33 | modelsList = [];
34 | jobsList = [];
35 | scheduledList = [];
36 | buckets = [];
37 | job: any;
38 | model: any;
39 |
40 | constructor(private $scope: any) {
41 | if (this.$scope.current === undefined) {
42 | this.$scope.current = {
43 | url: '',
44 | access: 'proxy',
45 | };
46 | }
47 | }
48 |
49 | toggleAccessHelp() {
50 | this.showAccessHelp = !this.showAccessHelp;
51 | }
52 |
53 | /**
54 | * Displays list of ML models and jobs on server
55 | */
56 | async refreshModels() {
57 | this.$scope.ctrl.modelsList = [{ is_loading: true, settings: { name: 'Loading...' }, state: { trained: '' } }];
58 |
59 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource;
60 |
61 | try {
62 | ds.query({ url: '/models', params: {} })
63 | .then(response => {
64 | this.$scope.ctrl.modelsList = response;
65 | this.$scope.$apply();
66 | })
67 | .catch(err => {
68 | console.error(err.statusText);
69 | console.error('Long time ago, in a galaxy far far away...');
70 | console.log('https://www.google.com/search?q=parallel+worlds+michio+kaku');
71 | appEvents.emit(AppEvents.alertError, [err.statusText]);
72 | });
73 |
74 | ds.query({ url: '/jobs', params: {} })
75 | .then(response => {
76 | this.$scope.ctrl.jobsList = response;
77 | this.$scope.$apply();
78 | })
79 | .catch(err => {
80 | appEvents.emit(AppEvents.alertError, [err.statusText]);
81 | });
82 |
83 | ds.query({ url: '/scheduled_jobs', params: {} })
84 | .then(response => {
85 | this.$scope.ctrl.scheduledList = response;
86 | this.$scope.$apply();
87 | })
88 | .catch(err => {
89 | appEvents.emit(AppEvents.alertError, [err.statusText]);
90 | });
91 |
92 | ds.query({ url: '/buckets', params: {} })
93 | .then(response => {
94 | this.$scope.ctrl.buckets = response;
95 | })
96 | .catch(err => {
97 | appEvents.emit(AppEvents.alertError, [err.statusText]);
98 | });
99 | } catch (err) {
100 | console.error(err);
101 | appEvents.emit(AppEvents.alertError, [err]);
102 | }
103 | }
104 |
105 | /**
106 | * Displays new ML model dialog
107 | */
108 | addModel() {
109 | this.model = Object.assign({
110 | ...DEFAULT_MODEL,
111 | features: [
112 | {
113 | ...DEFAULT_FEATURE,
114 | },
115 | ],
116 | });
117 | appEvents.emit('show-modal', {
118 | src: '/public/plugins/loudml-grafana-app/datasource/partials/add_model.html',
119 | modalClass: 'confirm-modal',
120 | model: this,
121 | });
122 | }
123 |
124 | editModel(name: any) {
125 | const model = this.$scope.ctrl.modelsList.find(el => el.settings.name === name);
126 | this.model = model.settings;
127 | // appEvents.emit(CoreEvents.showModal, {
128 | appEvents.emit('show-modal', {
129 | src: '/public/plugins/loudml-grafana-app/datasource/partials/add_model.html',
130 | modalClass: 'confirm-modal',
131 | model: this,
132 | });
133 | }
134 |
135 | /**
136 | * Posts dialog data to LoudML server
137 | */
138 | async addModelPost() {
139 | console.log(this.model);
140 |
141 | if (this.model.features[0].default !== 'previous') {
142 | this.model.features[0].default = 0;
143 | }
144 |
145 | delete this.model.features[0].$$hashKey;
146 |
147 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource;
148 | ds.loudml
149 | .getModel(this.model.name)
150 | .then(result => {
151 | console.log('Model exists, updating it...');
152 | ds.loudml
153 | .patchModel(this.model.name, this.model)
154 | .then(result => {})
155 | .catch(err => {
156 | sorry_its_error(err);
157 | return;
158 | });
159 | // // Let remove it and recreate
160 | // ds.loudml.deleteModel(this.model.name).then(response => {
161 | // ds.loudml
162 | // .createModel(this.model)
163 | // .then(result => {
164 | // ds.loudml
165 | // .createModelHook(this.model.name, ds.loudml.createHook(ANOMALY_HOOK, this.model.default_bucket))
166 | // .then(result => {
167 | // appEvents.emit(AppEvents.alertSuccess, ['Model has been updated on Loud ML server']);
168 | // this.refreshModels();
169 | // })
170 | // .catch(err => {
171 | // window.console.log('createModelHook error', err);
172 | // appEvents.emit(AppEvents.alertError, [err.message]);
173 | // return;
174 | // });
175 | // })
176 | // .catch(err => {
177 | // window.console.log('Model create error', err);
178 | // appEvents.emit(AppEvents.alertError, ['Model create error', err.data]);
179 | // return;
180 | // });
181 | // });
182 | })
183 | .catch(err => {
184 | // New model
185 | console.log('New model, creating it...');
186 | ds.loudml
187 | .createModel(this.model)
188 | .then(result => {
189 | ds.loudml
190 | .createModelHook(this.model.name, ds.loudml.createHook(ANOMALY_HOOK, this.model.default_bucket))
191 | .then(result => {
192 | appEvents.emit(AppEvents.alertSuccess, ['Model has been created on Loud ML server']);
193 | this.refreshModels();
194 | })
195 | .catch(err => {
196 | window.console.log('createModelHook error', err);
197 | appEvents.emit(AppEvents.alertError, [err.message]);
198 | return;
199 | });
200 | })
201 | .catch(err => {
202 | window.console.log('createModel error', err);
203 | appEvents.emit(AppEvents.alertError, ['Model create error', err.data]);
204 | return;
205 | });
206 | });
207 | }
208 |
209 | /**
210 | * Displays new job dialog
211 | */
212 | addJob() {
213 | this.job = Object.assign({}, DEFAULT_JOB);
214 |
215 | appEvents.emit('show-modal', {
216 | src: '/public/plugins/loudml-grafana-app/datasource/partials/add_job.html',
217 | modalClass: 'confirm-modal',
218 | model: this,
219 | });
220 | }
221 |
222 | /**
223 | * Displays edit job dialog
224 | */
225 | editJob(name: any) {
226 | this.job = this.$scope.ctrl.scheduledList.find(el => el.name === name);
227 | if (this.job.params) {
228 | this.job.params = JSON.stringify(this.job.params);
229 | }
230 |
231 | if (this.job.json) {
232 | this.job.json = JSON.stringify(this.job.json);
233 | }
234 |
235 | appEvents.emit('show-modal', {
236 | src: '/public/plugins/loudml-grafana-app/datasource/partials/add_job.html',
237 | modalClass: 'confirm-modal',
238 | model: this,
239 | });
240 | }
241 |
242 | /**
243 | * Posts job data to LoudML server
244 | */
245 | async scheduleJob() {
246 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource;
247 | ds.loudml
248 | .scheduleJob(this.job)
249 | .then(response => {
250 | window.console.log(response);
251 | appEvents.emit(AppEvents.alertSuccess, ['Job has been scheduled on Loud ML server']);
252 | this.refreshModels();
253 | })
254 | .catch(error => {
255 | console.log(error);
256 | appEvents.emit(AppEvents.alertError, ['Job schedule error', error.statusText]);
257 | });
258 | }
259 |
260 | /**
261 | * Deletes job on LoudML server
262 | */
263 | async deleteJob(name: any) {
264 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource;
265 | ds.loudml
266 | .deleteJob(name)
267 | .then(response => {
268 | window.console.log(response);
269 | appEvents.emit(AppEvents.alertSuccess, ['Scheduled job has been deleted on Loud ML server']);
270 | this.refreshModels();
271 | })
272 | .catch(error => {
273 | appEvents.emit(AppEvents.alertError, ['Job delete error', error.statusText]);
274 | });
275 | }
276 |
277 | async startModel(name: any) {
278 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource;
279 |
280 | try {
281 | ds.loudml.startModel(name).then(response => {
282 | window.console.log(response);
283 | appEvents.emit(AppEvents.alertSuccess, ['Model has been started on Loud ML server']);
284 | this.refreshModels();
285 | });
286 | } catch (err) {
287 | console.error(err);
288 | appEvents.emit(AppEvents.alertError, ['Model start error', err]);
289 | }
290 | }
291 |
292 | async stopModel(name: any) {
293 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource;
294 |
295 | try {
296 | ds.loudml.stopModel(name).then(response => {
297 | window.console.log(response);
298 | appEvents.emit(AppEvents.alertSuccess, ['Model has been stoped on Loud ML server']);
299 | this.refreshModels();
300 | });
301 | } catch (err) {
302 | console.error(err);
303 | appEvents.emit(AppEvents.alertError, ['Model stop error', err]);
304 | }
305 | }
306 |
307 | async forecastModel(name: any) {
308 | window.console.log('FORECAST MODEL');
309 | }
310 |
311 | async trainModel(name: any) {
312 | window.console.log('TRAIN MODEL');
313 | }
314 |
315 | async deleteModel(name: any) {
316 | const ds = (await getDataSourceSrv().loadDatasource(this.current.name)) as LoudMLDatasource;
317 |
318 | try {
319 | ds.loudml.deleteModel(name).then(response => {
320 | window.console.log(response);
321 | appEvents.emit(AppEvents.alertSuccess, ['Model has been deleted on Loud ML server']);
322 | this.refreshModels();
323 | });
324 | } catch (err) {
325 | console.error(err);
326 | appEvents.emit(AppEvents.alertError, ['Model delete error', err]);
327 | }
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/src/datasource/datasource.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { BackendSrv } from 'grafana/app/core/services/backend_srv';
3 |
4 | import LoudMLAPI from './loudml_api';
5 |
6 | export default class LoudMLDatasource {
7 | loudml: LoudMLAPI;
8 | bucket: string;
9 | jsonData: any;
10 |
11 | /** @ngInject */
12 | constructor(instanceSettings: any, backendSrv: BackendSrv) {
13 | this.bucket = (instanceSettings.jsonData || {}).bucket;
14 | this.loudml = new LoudMLAPI(instanceSettings, backendSrv);
15 | }
16 |
17 | async query(options: any) {
18 | const { url, params } = options;
19 | const response = await this.loudml.get(url, params);
20 | return response;
21 | }
22 |
23 | async testDatasource() {
24 | const response = await this.loudml.get('/');
25 | return response.version
26 | ? { status: 'success', message: 'Data source is working, Loud ML server version ' + response.version }
27 | : { status: 'error', message: response.error };
28 | }
29 |
30 | metricFindQuery(options: any) {
31 | return [];
32 | }
33 | }
34 |
35 | export { LoudMLDatasource };
36 |
--------------------------------------------------------------------------------
/src/datasource/loudml_api.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // Loud ML API class
3 | // Connects to Loud ML server
4 | //
5 | // https://loudml.io
6 | // http://github.com/regel/loudml
7 |
8 | import { BackendSrv } from 'grafana/app/core/services/backend_srv';
9 |
10 | import {
11 | DEFAULT_LOUDML_RP,
12 | MODEL_TYPE_LIST,
13 | DEFAULT_MODEL,
14 | DEFAULT_FEATURE,
15 | DEFAULT_START_OPTIONS,
16 | MIN_INTERVAL_SECOND,
17 | MIN_INTERVAL_UNIT,
18 | MAX_INTERVAL_SECOND,
19 | MAX_INTERVAL_UNIT,
20 | MIN_SPAN,
21 | MAX_SPAN,
22 | DEFAULT_ANOMALY_TYPE,
23 | ANOMALY_HOOK_NAME,
24 | ANOMALY_HOOK,
25 | } from './types';
26 |
27 | export default class LoudMLAPI {
28 | private url: string;
29 |
30 | constructor(instanceSettings: any, private backendSrv: BackendSrv) {
31 | this.url = instanceSettings.url;
32 | }
33 |
34 | async get(url: string, params?: any) {
35 | return this._query('GET', url, params);
36 | }
37 |
38 | private async _query(method: string, url: string, data?: any, data_as_params?: false) {
39 | method = method.toUpperCase();
40 | let options: any = {
41 | method,
42 | url: this.url + url,
43 | headers: {},
44 | };
45 | if (method === 'GET' || method === 'DELETE' || data_as_params) {
46 | options.params = data;
47 | } else {
48 | options.data = data;
49 | options.headers['Content-Type'] = 'application/json';
50 | }
51 |
52 | const response = await this.backendSrv.datasourceRequest(options);
53 | const responseData = response.data;
54 |
55 | return responseData;
56 | }
57 |
58 | createAndGetBucket = async (database, retentionPolicy, measurement, source) => {
59 | const { host, port } = this.splitAddr('http://localhost:8086', 8086); //source.url, 8086)
60 | const bucketName = [database, retentionPolicy, measurement].join('_');
61 | const settings = {
62 | type: source.type,
63 | name: bucketName,
64 | retention_policy: retentionPolicy,
65 | database,
66 | measurement,
67 | addr: `${host}:${port}`,
68 | username: source.username,
69 | password: source.password,
70 | };
71 |
72 | await this._query('POST', '/buckets', settings);
73 | const response = await this._query('GET', `/buckets/${bucketName}`);
74 |
75 | return response[0];
76 | };
77 |
78 | splitAddr = (url, port) => {
79 | // extract host and port from url address
80 | const re = /(https?:)?(\/\/)?([\w\.]*):?(\d*)?/;
81 | const res = re.exec(url);
82 | return {
83 | host: res[3],
84 | port: res[4] || port,
85 | };
86 | };
87 |
88 | createModel = async model => {
89 | // POST model JSON to /models
90 | return this._query('POST', '/models', model);
91 | };
92 |
93 | patchModel = async (name, model) => {
94 | return this._query('PATCH', `/models/${name}`, model);
95 | };
96 |
97 | getModel = async name => {
98 | return this._query('GET', `/models/${name}`);
99 | };
100 |
101 | deleteModel = async name => {
102 | return this._query('DELETE', `/models/${name}`);
103 | };
104 |
105 | createHook = (hook, bucket) => {
106 | const h = { ...hook };
107 | h.config.bucket = bucket;
108 | return h;
109 | };
110 |
111 | createModelHook = async (name, hook) => {
112 | // POST model hook to /models/${name}/hooks
113 | return this._query('POST', `/models/${name}/hooks`, hook);
114 | };
115 |
116 | trainAndStartModel = async (name, from, to, output_bucket) => {
117 | const params = {
118 | ...DEFAULT_START_OPTIONS,
119 | from,
120 | to,
121 | output_bucket: output_bucket,
122 | };
123 | return this._query('POST', `/models/${name}/_train`, params, true);
124 | };
125 |
126 | forecastModel = async (name, data, output_bucket) => {
127 | const { from, to } = data.timeRange.raw;
128 | const params = {
129 | from,
130 | to,
131 | save_output_data: true,
132 | output_bucket: output_bucket,
133 | bg: true,
134 | };
135 | return this._query('POST', `/models/${name}/_forecast`, params, true);
136 | };
137 |
138 | trainModel = async (name, data, output_bucket) => {
139 | const { lower, upper } = this.convertTimeRange(data.timeRange);
140 | return await this.trainAndStartModel(name, lower, upper, output_bucket);
141 | };
142 |
143 | startModel = async (name, output_bucket) => {
144 | const params = {
145 | ...DEFAULT_START_OPTIONS,
146 | output_bucket: output_bucket,
147 | };
148 | return this._query('POST', `/models/${name}/_start`, params, true);
149 | };
150 |
151 | stopModel = async name => {
152 | const params = {};
153 | return this._query('POST', `/models/${name}/_stop`, params, true);
154 | };
155 |
156 | scheduleJob = async job => {
157 | let params = {
158 | ...job,
159 | };
160 |
161 | window.console.log(params);
162 | params.every.count = parseInt(params.every.count, 10) || 1;
163 |
164 | if (!params.params) {
165 | delete params.params;
166 | } else {
167 | params.params = JSON.parse(params.params);
168 | }
169 |
170 | if (!params.json) {
171 | delete params.json;
172 | } else {
173 | params.json = JSON.parse(params.json);
174 | }
175 |
176 | if (!params.every.at) {
177 | delete params.every.at;
178 | }
179 |
180 | delete params['$$hashKey'];
181 | delete params.ok;
182 | delete params.error;
183 | delete params.status_code;
184 | delete params.last_run_timestamp;
185 |
186 | window.console.log(params);
187 | return this._query('POST', `/scheduled_jobs`, params);
188 | };
189 |
190 | deleteJob = async name => {
191 | return this._query('DELETE', `/scheduled_jobs/${name}`);
192 | };
193 |
194 | convertTimeRange = timeRange => {
195 | const { from, to } = timeRange.raw;
196 | return {
197 | lower: from,
198 | upper: to,
199 | };
200 | };
201 | }
202 |
--------------------------------------------------------------------------------
/src/datasource/module.ts:
--------------------------------------------------------------------------------
1 | import { LoudMLDatasource } from './datasource';
2 | // import { LoudMLQueryCtrl as QueryCtrl } from './query_ctrl';
3 | // import { LoudMLConfigCtrl as ConfigCtrl } from './config_ctrl';
4 | import { LoudMLQueryCtrl } from './query_ctrl';
5 | import { LoudMLConfigCtrl } from './config_ctrl';
6 | import { LoudMLQuery, LoudMLOptions } from './types';
7 |
8 | // export { LoudMLDatasource as Datasource, ConfigCtrl, QueryCtrl };
9 |
10 | export { LoudMLDatasource, LoudMLDatasource as Datasource, LoudMLQueryCtrl as QueryCtrl, LoudMLConfigCtrl as ConfigCtrl };
11 |
--------------------------------------------------------------------------------
/src/datasource/partials/add_job.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
15 |
76 |
77 |
129 |
130 |
131 | Schedule a Job
132 |
133 |
134 |
135 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/src/datasource/partials/add_model.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
112 |
113 |
197 |
198 |
249 |
250 |
251 | Save Model
252 |
253 |
254 |
255 |
256 |
257 |
--------------------------------------------------------------------------------
/src/datasource/partials/config.html:
--------------------------------------------------------------------------------
1 |
86 |
87 |
235 |
--------------------------------------------------------------------------------
/src/datasource/partials/edit_model.html:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
14 |
27 |
28 |
86 |
87 |
145 |
146 |
170 |
171 |
196 |
197 |
198 | Save Model
199 |
200 |
201 |
202 |
203 |
204 |
--------------------------------------------------------------------------------
/src/datasource/partials/query_ctrl.html:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/datasource/partials/query_ctrl.html
--------------------------------------------------------------------------------
/src/datasource/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "datasource",
3 | "name": "Loud ML Datasource",
4 | "id": "loudml-datasource",
5 | "metrics": true,
6 | "info": {
7 | "logos": {
8 | "small": "../loudml-grafana-app/img/logo.png",
9 | "large": "../loudml-grafana-app/img/logo.png"
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/datasource/query_ctrl.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import template from './partials/query_ctrl.html';
3 |
4 | import { QueryCtrl } from 'grafana/app/plugins/sdk';
5 |
6 | export class LoudMLQueryCtrl extends QueryCtrl {
7 | static template = template;
8 |
9 | /** @ngInject */
10 | constructor($scope, $injector) {
11 | super($scope, $injector);
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/datasource/types.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { DataQuery, DataSourceJsonData } from '@grafana/data';
3 |
4 | export interface LoudMLOptions extends DataSourceJsonData {
5 | bucket: string;
6 | }
7 |
8 | export interface LoudMLQuery extends DataQuery {}
9 |
10 | export const DEFAULT_LOUDML_RP = 'autogen';
11 |
12 | export const MODEL_TYPE_LIST = [
13 | { name: 'Donut', type: 'donut', default: true },
14 | // { name: 'DiskUtil', type: 'donut-ns', default: false },
15 | // { name: 'donut multivariate', type: 'donut-mv', default: false },
16 | ];
17 |
18 | export const DEFAULT_MODEL = {
19 | bucket_interval: '20m',
20 | default_bucket: null,
21 | features: [],
22 | interval: '1m',
23 | max_evals: 20,
24 | name: '',
25 | offset: '10s',
26 | span: 10,
27 | type: MODEL_TYPE_LIST[0].type,
28 | };
29 |
30 | export const DEFAULT_JOB = {
31 | name: '',
32 | method: 'get',
33 | relative_url: '',
34 | params: '',
35 | json: '',
36 | every: {
37 | count: 1,
38 | unit: '',
39 | at: '',
40 | },
41 | };
42 |
43 | export const DEFAULT_FEATURE = {
44 | name: '',
45 | measurement: null,
46 | field: null,
47 | metric: 'mean',
48 | default: null,
49 | io: 'io',
50 | anomaly_type: 'low_high',
51 | match_all: [],
52 | };
53 |
54 | export const DEFAULT_START_OPTIONS = {
55 | output_bucket: 'loudml',
56 | save_output_data: true,
57 | flag_abnormal_data: true,
58 | };
59 |
60 | export const MIN_INTERVAL_SECOND = 5;
61 |
62 | export const MIN_INTERVAL_UNIT = `${MIN_INTERVAL_SECOND}s`;
63 |
64 | export const MAX_INTERVAL_SECOND = 60;
65 |
66 | export const MAX_INTERVAL_UNIT = `${MAX_INTERVAL_SECOND}s`;
67 |
68 | export const MIN_SPAN = 10;
69 |
70 | export const MAX_SPAN = 100;
71 |
72 | export const DEFAULT_ANOMALY_TYPE = [
73 | { text: 'low', value: 'low' },
74 | { text: 'high', value: 'high' },
75 | { text: 'low/high', value: 'low_high' },
76 | ];
77 |
78 | export const ANOMALY_HOOK_NAME = 'add_annotation';
79 |
80 | export const ANOMALY_HOOK = {
81 | type: 'annotations',
82 | name: ANOMALY_HOOK_NAME,
83 | config: {
84 | id: null,
85 | type: 'loudml',
86 | bucket: null,
87 | },
88 | };
89 |
--------------------------------------------------------------------------------
/src/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/img/logo.png
--------------------------------------------------------------------------------
/src/img/loudml_grafana_datasource.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/img/loudml_grafana_datasource.png
--------------------------------------------------------------------------------
/src/img/loudml_grafana_panel.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/img/loudml_grafana_panel.png
--------------------------------------------------------------------------------
/src/module.ts:
--------------------------------------------------------------------------------
1 | import { ConfigCtrl } from './config/config_ctrl';
2 |
3 | export { ConfigCtrl };
4 |
--------------------------------------------------------------------------------
/src/module_raw.d.ts:
--------------------------------------------------------------------------------
1 | declare module '*.html' {
2 | const contents: string;
3 | export = contents;
4 | }
5 |
--------------------------------------------------------------------------------
/src/panel/Graph2.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // Libraries
3 | import _ from 'lodash';
4 | import $ from 'jquery';
5 | import tinycolor from 'tinycolor2';
6 | import React, { PureComponent } from 'react';
7 | import uniqBy from 'lodash/uniqBy';
8 | import flattenDeep from 'lodash/flattenDeep';
9 | import cloneDeep from 'lodash/cloneDeep';
10 |
11 | import appEvents from 'grafana/app/core/app_events';
12 | import { getDataSourceSrv, getBackendSrv } from '@grafana/runtime';
13 | import { getValueFormat, formattedValueToString, AnnotationEvent, DataSourceApi, AppEvents } from '@grafana/data';
14 | import { Graph, OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA } from '@grafana/ui';
15 |
16 | export class Graph2 extends Graph {
17 | dashboard: any;
18 | datasourcePromises: any;
19 | globalAnnotationsPromise: any;
20 | annotations: AnnotationEvent[];
21 |
22 | constructor(props) {
23 | super(props);
24 |
25 | const promises = [];
26 | const dsPromises = [];
27 | const range = props.timeRange;
28 | this.annotations = [];
29 |
30 | if (props.panelChrome) {
31 | this.dashboard = props.panelChrome.props.dashboard;
32 |
33 | Promise.all([this.getGlobalAnnotations(range)])
34 | .then(results => {
35 | this.annotations = flattenDeep(results[0]);
36 | // filter out annotations that do not belong to requesting panel
37 | this.annotations = this.annotations.filter(item => {
38 | // if event has panel id and query is of type dashboard then panel and requesting panel id must match
39 | if (item.panelId && item.source.type === 'dashboard') {
40 | return item.panelId === props.panelChrome.props.panel.id;
41 | }
42 | return true;
43 | });
44 |
45 | this.annotations = this.dedupAnnotations(this.annotations);
46 | this.draw();
47 | })
48 | .catch(err => {
49 | if (!err.message && err.data && err.data.message) {
50 | err.message = err.data.message;
51 | }
52 | console.log('AnnotationSrv.query error', err);
53 | appEvents.emit(AppEvents.alertError, ['Annotation Query Failed', err.message || err]);
54 | return [];
55 | });
56 | }
57 | }
58 |
59 | getGlobalAnnotations(range: any) {
60 | const promises = [];
61 | const dsPromises = [];
62 |
63 | for (const annotation of this.dashboard.annotations.list) {
64 | if (!annotation.enable) {
65 | continue;
66 | }
67 |
68 | if (annotation.snapshotData) {
69 | return this.translateQueryResult(annotation, annotation.snapshotData);
70 | }
71 |
72 | const datasourcePromise = getDataSourceSrv().get(annotation.datasource);
73 | dsPromises.push(datasourcePromise);
74 | promises.push(
75 | datasourcePromise
76 | .then((datasource: DataSourceApi) => {
77 | // issue query against data source
78 | return datasource.annotationQuery({
79 | range,
80 | rangeRaw: range.raw,
81 | annotation: annotation,
82 | dashboard: this.dashboard,
83 | });
84 | })
85 | .then(results => {
86 | // store response in annotation object if this is a snapshot call
87 | if (this.dashboard.snapshot) {
88 | annotation.snapshotData = cloneDeep(results);
89 | }
90 | // translate result
91 | return this.translateQueryResult(annotation, results);
92 | })
93 | );
94 | }
95 |
96 | this.datasourcePromises = Promise.all(dsPromises);
97 | this.globalAnnotationsPromise = Promise.all(promises);
98 | return this.globalAnnotationsPromise;
99 | }
100 |
101 | dedupAnnotations(annotations: any) {
102 | let dedup = [];
103 |
104 | // Split events by annotationId property existence
105 | const events = _.partition(annotations, 'id');
106 |
107 | const eventsById = _.groupBy(events[0], 'id');
108 | dedup = _.map(eventsById, eventGroup => {
109 | if (eventGroup.length > 1 && !_.every(eventGroup, this.isPanelAlert)) {
110 | // Get first non-panel alert
111 | return _.find(eventGroup, event => {
112 | return event.eventType !== 'panel-alert';
113 | });
114 | } else {
115 | return _.head(eventGroup);
116 | }
117 | });
118 |
119 | dedup = _.concat(dedup, events[1]);
120 | return dedup;
121 | }
122 |
123 | isPanelAlert(event: { eventType: string }) {
124 | return event.eventType === 'panel-alert';
125 | }
126 |
127 | translateQueryResult(annotation: any, results: any) {
128 | // if annotation has snapshotData
129 | // make clone and remove it
130 | if (annotation.snapshotData) {
131 | annotation = cloneDeep(annotation);
132 | delete annotation.snapshotData;
133 | }
134 |
135 | for (const item of results) {
136 | item.source = annotation;
137 | item.isRegion = item.timeEnd && item.time !== item.timeEnd;
138 | }
139 |
140 | return results;
141 | }
142 |
143 | tickFormatter(val, axis) {
144 | const formatter = getValueFormat('short');
145 |
146 | if (!formatter) {
147 | throw new Error(`Unit '${format}' is not supported`);
148 | }
149 | return formattedValueToString(formatter(val, axis.tickDecimals, axis.scaledDecimals));
150 | }
151 |
152 | getYAxes(series: GraphSeriesXY[]) {
153 | if (series.length === 0) {
154 | return [{ show: true, min: -1, max: 1 }];
155 | }
156 | return uniqBy(
157 | series.map(s => {
158 | const index = s.yAxis ? s.yAxis.index : 1;
159 | const min = s.yAxis && !isNaN(s.yAxis.min as number) ? s.yAxis.min : null;
160 | const tickDecimals = s.yAxis && !isNaN(s.yAxis.tickDecimals as number) ? s.yAxis.tickDecimals : null;
161 | return {
162 | show: true,
163 | index,
164 | position: index === 1 ? 'left' : 'right',
165 | min,
166 | tickDecimals,
167 | tickFormatter: this.tickFormatter,
168 | };
169 | }),
170 | yAxisConfig => yAxisConfig.index
171 | );
172 | }
173 |
174 | getFillGradient(amount: number) {
175 | if (!amount) {
176 | return null;
177 | }
178 |
179 | return {
180 | colors: [{ opacity: 0.0 }, { opacity: amount / 10 }],
181 | };
182 | }
183 |
184 | translateFillOption(fill: number) {
185 | if (this.props.stack) {
186 | return fill === 0 ? 0.001 : fill / 10;
187 | } else {
188 | return fill / 10;
189 | }
190 | }
191 |
192 | draw() {
193 | if (this.element === null) {
194 | return;
195 | }
196 |
197 | const {
198 | width,
199 | series,
200 | timeRange,
201 | showLines,
202 | showBars,
203 | showPoints,
204 | isStacked,
205 | lineWidth,
206 | fill,
207 | fillGradient,
208 | timeZone,
209 | onHorizontalRegionSelected,
210 | } = this.props;
211 |
212 | if (!width) {
213 | return;
214 | }
215 |
216 | const ticks = width / 100;
217 | const min = timeRange.from.valueOf();
218 | const max = timeRange.to.valueOf();
219 | const yaxes = this.getYAxes(series);
220 |
221 | const flotOptions: any = {
222 | legend: {
223 | show: false,
224 | },
225 | series: {
226 | stack: isStacked,
227 | lines: {
228 | show: showLines,
229 | fill: this.translateFillOption(fill),
230 | fillColor: this.getFillGradient(fillGradient),
231 | lineWidth: lineWidth,
232 | zero: false,
233 | },
234 | points: {
235 | show: showPoints,
236 | fill: 1,
237 | fillColor: false,
238 | radius: 2,
239 | },
240 | bars: {
241 | show: showBars,
242 | fill: 1,
243 | // Dividig the width by 1.5 to make the bars not touch each other
244 | barWidth: showBars ? this.getBarWidth() / 1.5 : 1,
245 | zero: false,
246 | lineWidth: lineWidth,
247 | },
248 | shadowSize: 0,
249 | },
250 | xaxis: {
251 | show: true,
252 | mode: 'time',
253 | min: min,
254 | max: max,
255 | label: 'Datetime',
256 | ticks: ticks,
257 | timeformat: timeFormat(ticks, min, max),
258 | timezone: timeZone ?? DefaultTimeZone,
259 | },
260 | yaxes,
261 | grid: {
262 | minBorderMargin: 0,
263 | markings: [],
264 | backgroundColor: null,
265 | borderWidth: 0,
266 | hoverable: true,
267 | clickable: true,
268 | color: '#a1a1a1',
269 | margin: { left: 0, right: 0 },
270 | labelMarginX: 0,
271 | mouseActiveRadius: 30,
272 | },
273 | selection: {
274 | mode: onHorizontalRegionSelected ? 'x' : null,
275 | color: '#666',
276 | },
277 | crosshair: {
278 | mode: 'x',
279 | },
280 | };
281 |
282 | this.addFlotEvents(this.annotations, flotOptions);
283 |
284 | try {
285 | $.plot(
286 | this.element,
287 | series.filter(s => s.isVisible),
288 | flotOptions
289 | );
290 | } catch (err) {
291 | console.log('Graph rendering error', err, flotOptions, series);
292 | throw new Error('Error rendering panel');
293 | }
294 | }
295 |
296 | render() {
297 | const { height, width, series } = this.props;
298 | const noDataToBeDisplayed = series.length === 0;
299 | const tooltip = this.renderTooltip();
300 | const context = this.renderContextMenu();
301 | return (
302 |
303 |
(this.element = e)}
306 | style={{ height, width }}
307 | onMouseLeave={() => {
308 | this.setState({ isTooltipVisible: false });
309 | }}
310 | />
311 | {noDataToBeDisplayed &&
No data
}
312 | {tooltip}
313 | {context}
314 |
315 | );
316 | }
317 |
318 | addFlotEvents(annotations: any, flotOptions: any) {
319 | if (!annotations) {
320 | return;
321 | }
322 |
323 | const types: any = {
324 | $__alerting: {
325 | color: ALERTING_COLOR,
326 | position: 'BOTTOM',
327 | markerSize: 5,
328 | },
329 | $__ok: {
330 | color: OK_COLOR,
331 | position: 'BOTTOM',
332 | markerSize: 5,
333 | },
334 | $__no_data: {
335 | color: NO_DATA_COLOR,
336 | position: 'BOTTOM',
337 | markerSize: 5,
338 | },
339 | $__pending: {
340 | color: PENDING_COLOR,
341 | position: 'BOTTOM',
342 | markerSize: 5,
343 | },
344 | $__editing: {
345 | color: DEFAULT_ANNOTATION_COLOR,
346 | position: 'BOTTOM',
347 | markerSize: 5,
348 | },
349 | };
350 |
351 | // annotations from query
352 | annotations.map(item => {
353 | // add properties used by jquery flot events
354 | item.min = item.time;
355 | item.max = item.time;
356 | item.eventType = item.source.name;
357 |
358 | if (item.newState) {
359 | item.eventType = '$__' + item.newState;
360 | } else {
361 | if (!types[item.source.name]) {
362 | types[item.source.name] = {
363 | color: item.source.iconColor,
364 | position: 'BOTTOM',
365 | markerSize: 5,
366 | };
367 | }
368 | }
369 | });
370 |
371 | const regions = getRegions(annotations);
372 | addRegionMarking(regions, flotOptions);
373 |
374 | const eventSectionHeight = 20;
375 | const eventSectionMargin = 7;
376 | flotOptions.grid.eventSectionHeight = eventSectionMargin;
377 | flotOptions.xaxis.eventSectionHeight = eventSectionHeight;
378 |
379 | flotOptions.events = {
380 | levels: _.keys(types).length + 1,
381 | data: annotations,
382 | types: types,
383 | manager: this,
384 | };
385 | }
386 | }
387 |
388 | function getRegions(events: AnnotationEvent[]) {
389 | return _.filter(events, 'isRegion');
390 | }
391 |
392 | function addRegionMarking(regions: any[], flotOptions: { grid: { markings: any } }) {
393 | const markings = flotOptions.grid.markings;
394 | const defaultColor = DEFAULT_ANNOTATION_COLOR;
395 | let fillColor;
396 |
397 | _.each(regions, region => {
398 | if (region.source) {
399 | fillColor = region.source.iconColor || defaultColor;
400 | } else {
401 | fillColor = defaultColor;
402 | }
403 |
404 | fillColor = addAlphaToRGB(fillColor, REGION_FILL_ALPHA);
405 | markings.push({
406 | xaxis: { from: region.min, to: region.timeEnd },
407 | color: fillColor,
408 | });
409 | });
410 | }
411 |
412 | function addAlphaToRGB(colorString: string, alpha: number): string {
413 | const color = tinycolor(colorString);
414 | if (color.isValid()) {
415 | color.setAlpha(alpha);
416 | return color.toRgbString();
417 | } else {
418 | return colorString;
419 | }
420 | }
421 |
422 | // Copied from graph.ts
423 | function timeFormat(ticks: number, min: number, max: number): string {
424 | if (min && max && ticks) {
425 | const range = max - min;
426 | const secPerTick = range / ticks / 1000;
427 | const oneDay = 86400000;
428 | const oneYear = 31536000000;
429 |
430 | if (secPerTick <= 45) {
431 | return '%H:%M:%S';
432 | }
433 | if (secPerTick <= 7200 || range <= oneDay) {
434 | return '%H:%M';
435 | }
436 | if (secPerTick <= 80000) {
437 | return '%m/%d %H:%M';
438 | }
439 | if (secPerTick <= 2419200 || range <= oneYear) {
440 | return '%m/%d';
441 | }
442 | return '%Y-%m';
443 | }
444 |
445 | return '%H:%M';
446 | }
447 |
448 | export default Graph2;
449 |
--------------------------------------------------------------------------------
/src/panel/GraphLegendEditor.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import React from 'react';
3 | import { LegendOptions, PanelOptionsGroup, Switch, Input, StatsPicker } from '@grafana/ui';
4 |
5 | export interface GraphLegendEditorLegendOptions extends LegendOptions {
6 | stats?: string[];
7 | decimals?: number;
8 | sortBy?: string;
9 | sortDesc?: boolean;
10 | }
11 |
12 | interface GraphLegendEditorProps {
13 | options: GraphLegendEditorLegendOptions;
14 | onChange: (options: GraphLegendEditorLegendOptions) => void;
15 | }
16 |
17 | export const GraphLegendEditor: React.FunctionComponent
= props => {
18 | const { options, onChange } = props;
19 |
20 | const onStatsChanged = (stats: string[]) => {
21 | onChange({
22 | ...options,
23 | stats,
24 | });
25 | };
26 |
27 | const onOptionToggle = (option: keyof LegendOptions) => (event?: React.ChangeEvent) => {
28 | const newOption: Partial = {};
29 | if (!event) {
30 | return;
31 | }
32 |
33 | if (option === 'placement') {
34 | newOption[option] = event.target.checked ? 'right' : 'under';
35 | } else {
36 | newOption[option] = event.target.checked;
37 | }
38 |
39 | onChange({
40 | ...options,
41 | ...newOption,
42 | });
43 | };
44 |
45 | const labelWidth = 8;
46 | return (
47 |
48 |
49 |
Options
50 |
51 |
52 |
58 |
59 |
60 |
61 |
Show
62 |
63 |
64 |
65 |
66 |
67 |
Decimals
68 |
{
74 | onChange({
75 | ...options,
76 | decimals: parseInt(event.target.value, 10),
77 | });
78 | }}
79 | />
80 |
81 |
82 |
83 |
84 |
Hidden series
85 | {/* */}
86 |
87 |
88 |
89 | );
90 | };
91 |
--------------------------------------------------------------------------------
/src/panel/GraphPanel.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import React from 'react';
3 | import { Chart, LegendDisplayMode } from '@grafana/ui';
4 | import { GraphWithLegend2 } from './GraphWithLegend2';
5 | import { PanelProps } from '@grafana/data';
6 | import { Options } from './types';
7 | import { GraphPanelController, CreateBaselineButton, MLModelController } from './GraphPanelController';
8 |
9 | interface GraphPanelProps extends PanelProps {}
10 |
11 | export const GraphPanel: React.FunctionComponent = ({
12 | data,
13 | timeRange,
14 | timeZone,
15 | width,
16 | height,
17 | options,
18 | fieldConfig,
19 | onOptionsChange,
20 | onChangeTimeRange,
21 | }) => {
22 | if (!data) {
23 | return (
24 |
25 |
No data found in response
26 |
27 | );
28 | }
29 |
30 | const {
31 | graph: { showLines, showBars, showPoints, isStacked, lineWidth, fill, fillGradient },
32 | legend: legendOptions,
33 | tooltipOptions,
34 | } = options;
35 |
36 | const graphProps = {
37 | showBars,
38 | showLines,
39 | showPoints,
40 | isStacked,
41 | lineWidth,
42 | fill,
43 | fillGradient,
44 | tooltipOptions,
45 | };
46 | const { asTable, isVisible, ...legendProps } = legendOptions;
47 |
48 | return (
49 | <>
50 |
51 |
52 |
53 |
54 |
55 | {({ onSeriesToggle, onHorizontalRegionSelected, ...controllerApi }) => {
56 | return (
57 |
73 |
74 |
75 | );
76 | }}
77 |
78 | >
79 | );
80 | };
81 |
--------------------------------------------------------------------------------
/src/panel/GraphPanelEditor.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // Libraries
3 | import _ from 'lodash';
4 | import React, { PureComponent } from 'react';
5 |
6 | // Services
7 | import { getDataSourceSrv } from '@grafana/runtime';
8 |
9 | // Types
10 | import { PanelEditorProps, FieldConfig, DataSourceSelectItem } from '@grafana/data';
11 | import { Switch, LegendOptions, GraphTooltipOptions, PanelOptionsGrid, PanelOptionsGroup, FieldPropertiesEditor, Select, Input } from '@grafana/ui';
12 |
13 | import { Options, GraphOptions, GraphDatasourceOptions } from './types';
14 | import { GraphLegendEditor } from './GraphLegendEditor';
15 |
16 | export class GraphPanelEditor extends PureComponent> {
17 | datasources: DataSourceSelectItem[] = getDataSourceSrv().getMetricSources();
18 |
19 | constructor(props) {
20 | super(props);
21 | }
22 |
23 | datasourcesList = function() {
24 | var res = new Array({ label: 'Not selected', value: '' });
25 |
26 | this.datasources.forEach(function(val) {
27 | if (val.meta.id === 'loudml-datasource') {
28 | res.push({ label: val.name, value: val.value });
29 | }
30 | });
31 |
32 | return res;
33 | };
34 |
35 | onChangeDataSource = (value: any) => {
36 | this.props.options.datasourceOptions.datasource = value.value;
37 | this.setState({ value: value.value });
38 | };
39 |
40 | onChangeInputBucket = (event: any) => {
41 | this.props.options.datasourceOptions.input_bucket = event.target.value;
42 | this.setState({ value: event.target.value });
43 | };
44 |
45 | onBlurInputBucket = () => {
46 | // window.console.log("onBlurInputBucket", this.state);
47 | };
48 |
49 | onChangeOutputBucket = (event: any) => {
50 | this.props.options.datasourceOptions.output_bucket = event.target.value;
51 | this.setState({ value: event.target.value });
52 | };
53 |
54 | onBlurOutputBucket = () => {
55 | // window.console.log("onBlurOutputBucket", this.state);
56 | };
57 |
58 | onGraphOptionsChange = (options: Partial) => {
59 | this.props.onOptionsChange({
60 | ...this.props.options,
61 | graph: {
62 | ...this.props.options.graph,
63 | ...options,
64 | },
65 | });
66 | };
67 |
68 | onLegendOptionsChange = (options: LegendOptions) => {
69 | this.props.onOptionsChange({ ...this.props.options, legend: options });
70 | };
71 |
72 | onTooltipOptionsChange = (options: GraphTooltipOptions) => {
73 | this.props.onOptionsChange({ ...this.props.options, tooltipOptions: options });
74 | };
75 |
76 | onToggleLines = () => {
77 | this.onGraphOptionsChange({ showLines: !this.props.options.graph.showLines });
78 | };
79 |
80 | onToggleBars = () => {
81 | this.onGraphOptionsChange({ showBars: !this.props.options.graph.showBars });
82 | };
83 |
84 | onTogglePoints = () => {
85 | this.onGraphOptionsChange({ showPoints: !this.props.options.graph.showPoints });
86 | };
87 |
88 | onToggleisStacked = () => {
89 | this.onGraphOptionsChange({ isStacked: !this.props.options.graph.isStacked });
90 | };
91 |
92 | onChangeFill = (value: any) => {
93 | this.onGraphOptionsChange({ fill: value.value });
94 | this.setState({ value: value.value });
95 | };
96 |
97 | onChangeFillGradient = (value: any) => {
98 | this.onGraphOptionsChange({ fillGradient: value.value });
99 | this.setState({ value: value.value });
100 | };
101 |
102 | onChangeLineWidth = (value: any) => {
103 | this.onGraphOptionsChange({ lineWidth: value.value });
104 | this.setState({ value: value.value });
105 | };
106 |
107 | onDefaultsChange = (field: FieldConfig) => {
108 | this.props.onOptionsChange({
109 | ...this.props.options,
110 | fieldOptions: {
111 | ...this.props.options.fieldOptions,
112 | defaults: field,
113 | },
114 | });
115 | };
116 |
117 | render() {
118 | const {
119 | graph: { showBars, showPoints, showLines, isStacked, lineWidth, fill, fillGradient },
120 | tooltipOptions: { mode },
121 | datasourceOptions: { datasource, input_bucket, output_bucket },
122 | } = this.props.options;
123 |
124 | return (
125 | <>
126 |
127 |
128 | Loud ML Server
129 | {
132 | this.onChangeDataSource({ value: value.value as any });
133 | }}
134 | options={this.datasourcesList()}
135 | />
136 |
137 |
138 | Input Bucket
139 |
148 |
149 | Bucket to get data for ML Model (Equal to Datasource used on Query tab; it should be in Loud ML YAML config)
150 |
151 |
152 | Output Bucket
153 |
162 |
163 | Specify a bucket to store ML training results (It should be in Loud ML YAML config)
164 |
165 |
166 |
167 |
168 |
Draw Modes
169 |
170 |
171 |
172 |
173 |
174 |
Mode Options
175 |
176 | Fill
177 | {
181 | this.onChangeFill({ value: value.value as any });
182 | }}
183 | options={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t }))}
184 | />
185 |
186 |
187 | Fill Gradient
188 | {
192 | this.onChangeFillGradient({ value: value.value as any });
193 | }}
194 | options={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t }))}
195 | />
196 |
197 |
198 |
199 | Line Width
200 | {
204 | this.onChangeLineWidth({ value: value.value as any });
205 | }}
206 | options={[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t }))}
207 | />
208 |
209 |
210 |
211 |
Stacking & Null value
212 |
213 |
214 |
215 |
216 |
217 |
218 |
219 |
220 |
221 |
222 |
223 | {
226 | this.onTooltipOptionsChange({ mode: value.value as any });
227 | }}
228 | options={[
229 | { label: 'All series', value: 'multi' },
230 | { label: 'Single', value: 'single' },
231 | ]}
232 | />
233 |
234 |
235 |
236 | >
237 | );
238 | }
239 | }
240 |
--------------------------------------------------------------------------------
/src/panel/GraphWithLegend2.tsx:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | // Libraries
3 | import React from 'react';
4 | import { css } from 'emotion';
5 | import { GraphSeriesValue } from '@grafana/data';
6 |
7 | import { Graph2 } from './Graph2';
8 | import { LegendRenderOptions, VizLegendItem, LegendDisplayMode } from '@grafana/ui';
9 | import { Graph, GraphProps, VizLegend } from '@grafana/ui';
10 | import { CustomScrollbar } from '@grafana/ui';
11 | import { stylesFactory } from '@grafana/ui';
12 |
13 | // import { GraphCtrl } from 'grafana/app/plugins/panel/graph';
14 |
15 | export type SeriesOptionChangeHandler = (label: string, option: TOption) => void;
16 | export type SeriesColorChangeHandler = SeriesOptionChangeHandler;
17 | export type SeriesAxisToggleHandler = SeriesOptionChangeHandler;
18 |
19 | export interface GraphWithLegendProps extends GraphProps, LegendRenderOptions {
20 | isLegendVisible: boolean;
21 | legendDisplayMode: LegendDisplayMode;
22 | sortLegendBy?: string;
23 | sortLegendDesc?: boolean;
24 | onSeriesColorChange?: SeriesColorChangeHandler;
25 | onSeriesAxisToggle?: SeriesAxisToggleHandler;
26 | onSeriesToggle?: (label: string, event: React.MouseEvent) => void;
27 | onToggleSort: (sortBy: string) => void;
28 | }
29 |
30 | const getGraphWithLegendStyles = stylesFactory(({ placement }: GraphWithLegendProps) => ({
31 | wrapper: css`
32 | display: flex;
33 | flex-direction: ${placement === 'under' ? 'column' : 'row'};
34 | height: 100%;
35 | `,
36 | graphContainer: css`
37 | min-height: 65%;
38 | flex-grow: 1;
39 | `,
40 | legendContainer: css`
41 | padding: 25px 0;
42 | max-height: ${placement === 'under' ? '35%' : 'none'};
43 | `,
44 | }));
45 |
46 | const shouldHideLegendItem = (data: GraphSeriesValue[][], hideEmpty = false, hideZero = false) => {
47 | const isZeroOnlySeries = data.reduce((acc, current) => acc + (current[1] || 0), 0) === 0;
48 | const isNullOnlySeries = !data.reduce((acc, current) => acc && current[1] !== null, true);
49 |
50 | return (hideEmpty && isNullOnlySeries) || (hideZero && isZeroOnlySeries);
51 | };
52 |
53 | export const GraphWithLegend2: React.FunctionComponent = (props: GraphWithLegendProps) => {
54 | const {
55 | series,
56 | timeRange,
57 | width,
58 | height,
59 | showBars,
60 | showLines,
61 | showPoints,
62 | sortLegendBy,
63 | sortLegendDesc,
64 | isLegendVisible,
65 | legendDisplayMode,
66 | placement,
67 | onSeriesAxisToggle,
68 | onSeriesColorChange,
69 | onSeriesToggle,
70 | onToggleSort,
71 | hideEmpty,
72 | hideZero,
73 | isStacked,
74 | lineWidth,
75 | fill,
76 | fillGradient,
77 | onHorizontalRegionSelected,
78 | timeZone,
79 | children,
80 | panelChrome,
81 | } = props;
82 | const { graphContainer, wrapper, legendContainer } = getGraphWithLegendStyles(props);
83 |
84 | const legendItems = series.reduce((acc, s) => {
85 | return shouldHideLegendItem(s.data, hideEmpty, hideZero)
86 | ? acc
87 | : acc.concat([
88 | {
89 | label: s.label,
90 | color: s.color || '',
91 | disabled: !s.isVisible,
92 | yAxis: s.yAxis.index,
93 | getDisplayValues: () => s.info || [],
94 | },
95 | ]);
96 | }, []);
97 |
98 | return (
99 |
100 |
101 |
118 | {children}
119 |
120 |
121 |
122 |
123 | {isLegendVisible && (
124 |
125 | {
132 | if (onSeriesToggle) {
133 | onSeriesToggle(item.label, event);
134 | }
135 | }}
136 | onSeriesColorChange={onSeriesColorChange}
137 | onToggleSort={onToggleSort}
138 | />
139 |
140 | )}
141 |
142 |
143 | );
144 | };
145 |
--------------------------------------------------------------------------------
/src/panel/extractors.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | /*eslint-disable */
3 | // Data extractor functions
4 | // to parse selected data/queries into features, groupBy and params for Loud ML Model
5 |
6 | import {
7 | DEFAULT_MODEL,
8 | } from '../datasource/types';
9 |
10 | export function extract_tooltip_feature(target: any): any {
11 | if (target.select) {
12 | // InfluxDB or PostgreSQL or so
13 | if (target.measurement) {
14 | // InfluxDB
15 | return target.measurement + ": " + target.select
16 | .map(o => _formatFeature(o))
17 | .join(', ');
18 | // return target.measurement + ": " + _formatFeature(target.select[0])
19 | }
20 |
21 | if (target.table) {
22 | // PostgreSQL
23 | return target.table + ": " + target.select[0]
24 | .filter(o => o.type==='column')
25 | .map(o => o.params.join(', ')).join('; ')
26 | }
27 | }
28 |
29 | if (target.metric) {
30 | // OpenTSDB or so
31 | return target.metric
32 | }
33 |
34 | if (target.expr) {
35 | // Prometheus or so
36 | return target.expr
37 | }
38 |
39 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) {
40 | // Elasticsearch or so
41 | return target.metrics[0].type + ": " + target.metrics[0].field
42 | }
43 | }
44 |
45 | export function extract_group_by(target: any): any {
46 | if (target.groupBy) {
47 | // InfluxDB or so
48 | let res = _formatGroupBy(target.groupBy);
49 | if (res == "time: $__interval") {
50 | return "time: " + DEFAULT_MODEL.interval;
51 | }
52 | return res;
53 | }
54 |
55 | if (target.group) {
56 | // PostgreSQL or so
57 | return target.group
58 | .filter(o => o.type==='time')
59 | .map(o => [o.type, o.params[0]].join(': ')).join(', ')
60 | }
61 |
62 | if (target.aggregator && target.downsampleInterval && target.downsampleAggregator) {
63 | // OpenTSDB or so
64 | return target.aggregator + ":" + target.downsampleInterval + "-" + target.downsampleAggregator + "-nan"
65 | }
66 |
67 | if (target.expr) {
68 | // Prometheus or so
69 | return target.interval || "auto"
70 | }
71 |
72 | if (target.query && target.bucketAggs && target.bucketAggs.length>0) {
73 | // Elasticsearch or so
74 | if (target.bucketAggs[0].settings) {
75 | return target.bucketAggs[0].settings.interval;
76 | }
77 | return "auto"
78 | }
79 | }
80 |
81 | export function extract_fill_value(target: any) {
82 | if (target.groupBy) {
83 | // InfluxDB or so
84 | const f = _formatFillValue(target.groupBy)
85 |
86 | if (f == "null") {
87 | return null;
88 | }
89 | if (f == "none") {
90 | return null;
91 | }
92 |
93 | return f;
94 | }
95 |
96 | if (target.group) {
97 | // PostgreSQL or so
98 | return target.group
99 | .filter(o => o.type==='time')
100 | .map(o => o.params[1]).join(', ')
101 | }
102 |
103 | // OpenTSDB or Prometheus or so
104 | return null;
105 | }
106 |
107 | export function extract_format_tags(target: any) {
108 | if (target.tags && target.tags.map) {
109 | // InfluxDB or so
110 | return target.tags.map(o => [o.key, o.operator, o.value].join(' ')).join(', ')
111 | }
112 |
113 | // OpenTSDB or so
114 | // 1 tag selected
115 | if (target.currentTagKey && target.currentTagValue) {
116 | return target.currentTagKey + " = " + target.currentTagValue
117 | }
118 | // 2 and more tags
119 | if (target.tags && typeof target.tags === 'object') {
120 | let res = [];
121 | for (const key in target.tags) {
122 | if (target.tags.hasOwnProperty(key)) {
123 | res.push(key + " = " + target.tags[key]);
124 | }
125 | }
126 | return res.join(', ');
127 | }
128 |
129 | if (target.where) {
130 | // PostgreSQL or so
131 | return target.where
132 | .filter(o => o.type==='expression')
133 | .map(o => o.params.join(' ')).join(', ')
134 | }
135 |
136 | if (target.query) {
137 | // Elasticsearch or so
138 | return target.query
139 | }
140 | }
141 |
142 | export function extract_is_valid(target: any) {
143 | if (target.select) {
144 | // InfluxDB or so
145 | return true
146 | }
147 |
148 | if (target.metric) {
149 | // OpenTSDB or so
150 | return true
151 | }
152 |
153 | if (target.expr) {
154 | // Prometheus or so
155 | return true
156 | }
157 |
158 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) {
159 | // Elasticsearch or so
160 | return true
161 | }
162 | }
163 |
164 | export function extract_model_database(datasource: any) {
165 | // console.log(datasource);
166 | if (datasource.database) {
167 | // InfluxDB
168 | return datasource.database;
169 | }
170 |
171 | if (datasource.index) {
172 | // Elasticsearch
173 | return datasource.index;
174 | }
175 |
176 | return datasource.name.toLowerCase().replace(/-/g, "_");
177 | }
178 |
179 | export function extract_model_measurement(target: any) {
180 | if (target.measurement) {
181 | // InfluxDB or so
182 | return target.measurement;
183 | }
184 |
185 | if (target.metric) {
186 | // OpenTSDB or so
187 | return target.metric;
188 | }
189 |
190 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) {
191 | // Elasticsearch or so
192 | return target.metrics[0].type;
193 | }
194 |
195 | return "auto";
196 | }
197 |
198 | export function extract_model_select(target: any, field: any) {
199 | if (target.select) {
200 | // InfluxDB or so
201 | return _formatSelect(field);
202 | }
203 |
204 | if (target.metric && target.aggregator) {
205 | // OpenTSDB or so
206 | return target.aggregator + "_" + target.metric.replace(/\./g, "_");
207 | }
208 |
209 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) {
210 | // Elasticsearch or so
211 | return target.metrics[0].type + "_" + target.metrics[0].field.replace(/\./g, "_");
212 | }
213 |
214 | if (target.expr) {
215 | // Prometheus or so
216 | return string_to_slug(target.expr);
217 | }
218 | }
219 |
220 | export function extract_model_feature(target: any, field: any) {
221 | if (target.select) {
222 | // InfluxDB or so
223 | return _get_feature(field)
224 | }
225 |
226 | if (target.metric) {
227 | // OpenTSDB or so
228 | return target.metric
229 | }
230 |
231 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) {
232 | // Elasticsearch or so
233 | return target.metrics[0].field
234 | }
235 |
236 | if (target.expr) {
237 | // Prometheus or so
238 | return target.expr
239 | }
240 | }
241 |
242 | export function extract_model_func(target: any, field: any) {
243 | if (target.select) {
244 | // InfluxDB or so
245 | return _get_func(field)
246 | }
247 |
248 | if (target.aggregator) {
249 | // OpenTSDB or so
250 | return target.aggregator
251 | }
252 |
253 | if (target.query && target.bucketAggs && target.metrics && target.metrics.length > 0) {
254 | // Elasticsearch or so
255 | return target.metrics[0].type
256 | }
257 |
258 | return "avg"
259 | }
260 |
261 | export function extract_model_fill(target: any) {
262 | if (target.groupBy) {
263 | // InfluxDB or so
264 | const f = _get_fill(target.groupBy);
265 | if (f == "0") {
266 | return 0;
267 | }
268 | if (f == "null") {
269 | return null;
270 | }
271 | if (f == "none") {
272 | return null;
273 | }
274 | return f;
275 | }
276 | // OpenTSDB or so
277 | return null; // TODO
278 | }
279 |
280 | export function extract_model_time_format(target: any) {
281 | if (target.groupBy) {
282 | // InfluxDB or so
283 | let res = _formatTime(target.groupBy);
284 | // console.log(res);
285 | if (res == "time_$__interval" ) {
286 | return DEFAULT_MODEL.interval;
287 | }
288 | return res;
289 | } else {
290 | // OpenTSDB or Prometheus or so
291 | return target.downsampleInterval || target.interval || "auto"
292 | }
293 | }
294 |
295 | export function extract_model_time(target: any) {
296 | if (target.groupBy) {
297 | // InfluxDB or so
298 | let res = _get_time(target.groupBy);
299 | // console.log(res);
300 | if (res == "$__interval" ) {
301 | return DEFAULT_MODEL.interval;
302 | }
303 | return res;
304 | }
305 |
306 | if (target.query && target.bucketAggs && target.bucketAggs.length > 0 && target.bucketAggs[0].settings) {
307 | // Elasticsearch or so
308 | return target.bucketAggs[0].settings.interval
309 | }
310 |
311 | // else - OpenTSDB or Prometheus or so
312 | return target.downsampleInterval || target.interval || "20m"
313 | }
314 |
315 | export function extract_model_tags(target: any) {
316 | if (target.tags && target.tags.map) {
317 | // InfluxDB or so
318 | return _formatTags(target.tags)
319 | }
320 |
321 | // OpenTSDB or so
322 | // 1 tag selected
323 | if (target.currentTagKey && target.currentTagValue) {
324 | return target.currentTagKey + "_" + target.currentTagValue
325 | }
326 | // 2 and more tags
327 | if (target.tags && typeof target.tags === 'object') {
328 | let res = [];
329 | for (const key in target.tags) {
330 | if (target.tags.hasOwnProperty(key)) {
331 | res.push(key + "_" + target.tags[key]);
332 | }
333 | }
334 | return res.join('_')
335 | }
336 |
337 | return ""
338 | }
339 |
340 | export function extract_model_tags_map(target: any) {
341 | if (target.tags && target.tags.map) {
342 | // InfluxDB or so
343 | return target.tags.map(
344 | (tag) => ({
345 | tag: tag.key,
346 | value: tag.value,
347 | })
348 | )
349 | }
350 |
351 | // OpenTSDB or so
352 | // 1 tag selected
353 | if (target.currentTagKey && target.currentTagValue) {
354 | return {
355 | tag: target.currentTagKey,
356 | value: target.currentTagValue
357 | }
358 | }
359 | // 2 and more tags
360 | if (target.tags && typeof target.tags === 'object') {
361 | let res = [];
362 | for (const key in target.tags) {
363 | if (target.tags.hasOwnProperty(key)) {
364 | res.push({tag: key, value: target.tags[key]});
365 | }
366 | }
367 | return res
368 | }
369 |
370 | return []
371 | }
372 |
373 | // Internal parser functions -------------------------------------------------
374 |
375 | function _formatFeature(value) {
376 | // window.console.log('Feature Value', value);
377 | const selectField = value.filter(o => o.type==='field');
378 | return selectField.map(o => o.params.join(', ')).join('; ')
379 | }
380 |
381 | function _formatGroupBy(value: any) {
382 | // window.console.log('Group By Value', value);
383 | const groupBy = value.filter(o => o.type==='time');
384 | return groupBy.map(o => [o.type, o.params].join(': ')).join(', ')
385 | }
386 |
387 | function _formatFillValue(value: any) {
388 | // window.console.log('Fill Value', value);
389 | const fill = value.filter(o => o.type==='fill');
390 | return fill.map(o => [o.type, o.params].join(': ')).join(', ')
391 | }
392 |
393 | function _formatSelect(value: any) {
394 | const selectFunc = value.filter(o => o.type!=='field');
395 | const selectField = value.filter(o => o.type==='field');
396 |
397 | return selectFunc.map(o => o.type).join('_') + '_' + selectField.map(o => o.params.join('_')).join('_')
398 | }
399 |
400 | function _formatTime(value: any) {
401 | const groupBy = value.filter(o => o.type==='time');
402 | return groupBy.map(o => [o.type, o.params].join('_')).join('_')
403 | }
404 |
405 | function _get_feature(value: any) {
406 | const field = value.filter(o => o.type==='field');
407 | if (field.length === 0) {
408 | // TODO: check how we ended up with empty field and allowed user to click ML Button
409 | return "";
410 | }
411 | return field[0].params[0];
412 | }
413 |
414 | function _get_func(value: any) {
415 | const func = value.filter(o => o.type!=='field');
416 | if (func.length === 0) {
417 | return "";
418 | }
419 | return func[0].type;
420 | }
421 |
422 | function _get_fill(value: any) {
423 | const fill = value.filter(o => o.type==='fill');
424 | if (fill.length === 0) {
425 | return "null";
426 | }
427 | return fill[0].params[0];
428 | }
429 |
430 | function _get_time(value: any) {
431 | const time = value.filter(o => o.type==='time');
432 | if (time.length !== 1) {
433 | return DEFAULT_MODEL.interval;
434 | }
435 | return time[0].params[0];
436 | }
437 |
438 | function _formatTags(value: any) {
439 | return value.map(o => [o.key, o.value].join('_')).join('_')
440 | }
441 |
442 | function string_to_slug(str: any) {
443 | str = str.replace(/^\s+|\s+$/g, '');
444 | str = str.toLowerCase();
445 |
446 | // remove accents, swap ñ for n, etc
447 | var from = "àáäâèéëêìíïîòóöôùúüûñç·/_,:;";
448 | var to = "aaaaeeeeiiiioooouuuunc------";
449 | for (var i=0, l=from.length ; i {
34 | const graphs: GraphSeriesXY[] = [];
35 |
36 | const displayProcessor = getDisplayProcessor({
37 | field: {
38 | config: {
39 | unit: fieldOptions?.defaults?.unit,
40 | decimals: legendOptions.decimals,
41 | },
42 | },
43 | });
44 |
45 | let fieldColumnIndex = -1;
46 | for (const series of dataFrames) {
47 | const { timeField } = getTimeField(series);
48 | if (!timeField) {
49 | continue;
50 | }
51 |
52 | for (const field of series.fields) {
53 | if (field.type !== FieldType.number) {
54 | continue;
55 | }
56 | // Storing index of series field for future inspection
57 | fieldColumnIndex++;
58 |
59 | // Use external calculator just to make sure it works :)
60 | const points = getFlotPairs({
61 | xField: timeField,
62 | yField: field,
63 | nullValueMode: NullValueMode.Null,
64 | });
65 |
66 | if (points.length > 0) {
67 | const seriesStats = reduceField({ field, reducers: legendOptions.stats });
68 | let statsDisplayValues: DisplayValue[];
69 |
70 | if (legendOptions.stats) {
71 | statsDisplayValues = legendOptions.stats.map(stat => {
72 | const statDisplayValue = displayProcessor(seriesStats[stat]);
73 |
74 | return {
75 | ...statDisplayValue,
76 | title: stat,
77 | };
78 | });
79 | }
80 |
81 | let color: FieldColor;
82 | if (seriesOptions[field.name] && seriesOptions[field.name].color) {
83 | // Case when panel has settings provided via SeriesOptions, i.e. graph panel
84 | color = {
85 | mode: FieldColorMode.Fixed,
86 | fixedColor: seriesOptions[field.name].color,
87 | };
88 | } else if (field.config && field.config.color) {
89 | // Case when color settings are set on field, i.e. Explore logs histogram (see makeSeriesForLogs)
90 | color = field.config.color;
91 | } else {
92 | color = {
93 | mode: FieldColorMode.Fixed,
94 | fixedColor: colors[graphs.length % colors.length],
95 | };
96 | }
97 |
98 | field.config = fieldOptions
99 | ? {
100 | ...field.config,
101 | unit: fieldOptions.defaults.unit,
102 | decimals: fieldOptions.defaults.decimals,
103 | color,
104 | }
105 | : { ...field.config, color };
106 |
107 | field.display = getDisplayProcessor({ field });
108 |
109 | // Time step is used to determine bars width when graph is rendered as bar chart
110 | const timeStep = getSeriesTimeStep(timeField);
111 | const useMsDateFormat = hasMsResolution(timeField);
112 |
113 | timeField.display = getDisplayProcessor({
114 | timeZone,
115 | field: {
116 | ...timeField,
117 | type: timeField.type,
118 | config: {
119 | unit: `time:${useMsDateFormat ? MS_DATE_TIME_FORMAT : DEFAULT_DATE_TIME_FORMAT}`,
120 | },
121 | },
122 | });
123 |
124 | graphs.push({
125 | label: field.name,
126 | data: points,
127 | color: field.config.color?.fixedColor,
128 | info: statsDisplayValues,
129 | isVisible: true,
130 | yAxis: {
131 | index: (seriesOptions[field.name] && seriesOptions[field.name].yAxis) || 1,
132 | },
133 | // This index is used later on to retrieve appropriate series/time for X and Y axes
134 | seriesIndex: fieldColumnIndex,
135 | timeField: { ...timeField },
136 | valueField: { ...field },
137 | timeStep,
138 | });
139 | }
140 | }
141 | }
142 |
143 | return graphs;
144 | };
145 |
--------------------------------------------------------------------------------
/src/panel/module.test.ts:
--------------------------------------------------------------------------------
1 | // Just a stub test
2 | describe('placeholder test', () => {
3 | it('should return true', () => {
4 | expect(true).toBeTruthy();
5 | });
6 | });
7 |
--------------------------------------------------------------------------------
/src/panel/module.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { FieldConfigProperty, PanelPlugin } from '@grafana/data';
3 | import { getDataSourceSrv, GrafanaBootConfig } from '@grafana/runtime';
4 |
5 | import { GraphPanelEditor } from './GraphPanelEditor';
6 | import { GraphPanel } from './GraphPanel';
7 | import { Options, defaults } from './types';
8 |
9 | // function grafanaVersion(): string | null {
10 | // if(_.has(window, 'grafanaBootData.settings.buildInfo.version')) {
11 | // return window.grafanaBootData.settings.buildInfo.version;
12 | // }
13 | // return null;
14 | // }
15 | // console.log(GrafanaBootConfig);
16 |
17 | // export const plugin = new PanelPlugin(GraphPanel).setDefaults(defaults).setEditor(GraphPanelEditor);
18 |
19 | function datasourcesList() {
20 | var res = new Array({ label: 'Not selected', value: '' });
21 | var datasources = getDataSourceSrv().getMetricSources();
22 |
23 | datasources.forEach(function(val) {
24 | if (val.meta.id === 'loudml-datasource') {
25 | res.push({ label: val.name, value: val.value });
26 | }
27 | });
28 |
29 | return res;
30 | }
31 |
32 | export const plugin = new PanelPlugin(GraphPanel)
33 | .useFieldConfig({ standardOptions: [FieldConfigProperty.Unit, FieldConfigProperty.Decimals] })
34 | .setPanelOptions(builder => {
35 | builder
36 | .addSelect({
37 | path: 'datasourceOptions.datasource',
38 | name: 'Loud ML Server',
39 | description: '',
40 | settings: {
41 | options: datasourcesList(),
42 | },
43 | defaultValue: '',
44 | })
45 | .addTextInput({
46 | path: 'datasourceOptions.input_bucket',
47 | name: 'Input Bucket',
48 | description: 'Datasource/Database used in Query',
49 | defaultValue: '',
50 | })
51 | .addTextInput({
52 | path: 'datasourceOptions.output_bucket',
53 | name: 'Output Bucket',
54 | description: 'Database to store ML Model training results',
55 | defaultValue: 'loudml',
56 | })
57 | .addBooleanSwitch({
58 | path: 'graph.showBars',
59 | name: 'Bars',
60 | description: '',
61 | defaultValue: false,
62 | })
63 | .addBooleanSwitch({
64 | path: 'graph.showLines',
65 | name: 'Lines',
66 | description: '',
67 | defaultValue: true,
68 | })
69 | .addSelect({
70 | path: 'graph.lineWidth',
71 | name: 'Line Width',
72 | description: '',
73 | settings: {
74 | options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t })),
75 | },
76 | defaultValue: 1,
77 | })
78 | .addSelect({
79 | path: 'graph.fill',
80 | name: 'Area fill',
81 | description: '',
82 | settings: {
83 | options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t })),
84 | },
85 | defaultValue: 1,
86 | })
87 | .addSelect({
88 | path: 'graph.fillGradient',
89 | name: 'Fill Gradient',
90 | description: '',
91 | settings: {
92 | options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(t => ({ value: t, label: t })),
93 | },
94 | defaultValue: 0,
95 | })
96 | .addBooleanSwitch({
97 | path: 'graph.showPoints',
98 | name: 'Poins',
99 | description: '',
100 | defaultValue: false,
101 | })
102 | .addBooleanSwitch({
103 | path: 'graph.isStacked',
104 | name: 'Stack',
105 | description: '',
106 | defaultValue: false,
107 | })
108 | .addBooleanSwitch({
109 | path: 'legend.isVisible',
110 | name: 'Show legend',
111 | description: '',
112 | defaultValue: true,
113 | })
114 | .addBooleanSwitch({
115 | path: 'legend.asTable',
116 | name: 'Display legend as table',
117 | description: '',
118 | defaultValue: false,
119 | })
120 | .addRadio({
121 | path: 'legend.placement',
122 | name: 'Legend placement',
123 | description: '',
124 | defaultValue: 'under',
125 | settings: {
126 | options: [
127 | { value: 'under', label: 'Below graph' },
128 | { value: 'right', label: 'Right to the graph' },
129 | ],
130 | },
131 | })
132 | .addRadio({
133 | path: 'tooltipOptions.mode',
134 | name: 'Tooltip mode',
135 | description: '',
136 | defaultValue: 'single',
137 | settings: {
138 | options: [
139 | { value: 'single', label: 'Single series' },
140 | { value: 'multi', label: 'All series' },
141 | ],
142 | },
143 | });
144 | });
145 |
--------------------------------------------------------------------------------
/src/panel/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "panel",
3 | "name": "Loud ML Graph",
4 | "id": "loudml-graph-panel",
5 | "info": {
6 | "logos": {
7 | "small": "../loudml-grafana-app/img/logo.png",
8 | "large": "../loudml-grafana-app/img/logo.png"
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/panel/types.ts:
--------------------------------------------------------------------------------
1 | // @ts-nocheck
2 | import { LegendOptions, GraphTooltipOptions } from '@grafana/ui';
3 | import { GraphLegendEditorLegendOptions } from './GraphLegendEditor';
4 | import { YAxis, ReducerID, FieldDisplayOptions, ThresholdsMode, DataSourceSelectItem } from '@grafana/data';
5 |
6 | export const standardFieldDisplayOptions: FieldDisplayOptions = {
7 | values: false,
8 | calcs: [ReducerID.mean],
9 | defaults: {
10 | thresholds: {
11 | mode: ThresholdsMode.Absolute,
12 | steps: [
13 | { value: -Infinity, color: 'green' },
14 | { value: 80, color: 'red' }, // 80%
15 | ],
16 | },
17 | mappings: [],
18 | },
19 | overrides: [],
20 | };
21 |
22 | export interface SeriesOptions {
23 | color?: string;
24 | yAxis?: YAxis;
25 | [key: string]: any;
26 | }
27 | export interface GraphOptions {
28 | showBars: boolean;
29 | showLines: boolean;
30 | showPoints: boolean;
31 | isStacked: boolean;
32 | lineWidth: number;
33 | fill: number;
34 | fillGradient: number;
35 | }
36 |
37 | export interface GraphDatasourceOptions {
38 | datasource?: string;
39 | input_bucket?: string;
40 | output_bucket?: string;
41 | }
42 |
43 | export interface Options {
44 | graph: GraphOptions;
45 | legend: LegendOptions & GraphLegendEditorLegendOptions;
46 | series: {
47 | [alias: string]: SeriesOptions;
48 | };
49 | fieldOptions: FieldDisplayOptions;
50 | tooltipOptions: GraphTooltipOptions;
51 | datasourceOptions: GraphDatasourceOptions;
52 | modelName?: string;
53 | }
54 |
55 | export const defaults: Options = {
56 | graph: {
57 | showBars: false,
58 | showLines: true,
59 | showPoints: false,
60 | isStacked: false,
61 | lineWidth: 1,
62 | fill: 1,
63 | fillGradient: 0,
64 | },
65 | legend: {
66 | asTable: false,
67 | isVisible: true,
68 | placement: 'under',
69 | },
70 | series: {},
71 | fieldOptions: { ...standardFieldDisplayOptions },
72 | tooltipOptions: { mode: 'single' },
73 | datasourceOptions: {
74 | datasource: '',
75 | input_bucket: '',
76 | output_bucket: 'loudml',
77 | },
78 | modelName: '',
79 | };
80 |
--------------------------------------------------------------------------------
/src/plugin.json:
--------------------------------------------------------------------------------
1 | {
2 | "type": "app",
3 | "name": "Loud ML app",
4 | "id": "loudml-grafana-app",
5 |
6 | "info": {
7 | "description": "Loud ML app: Visualization panel and datasource for Grafana to connect with Loud ML AI",
8 | "author": {
9 | "name": "Volodymyr Sergeyev"
10 | },
11 | "keywords": ["LoudML", "ML", "AI"],
12 | "logos": {
13 | "small": "img/logo.png",
14 | "large": "img/logo.png"
15 | },
16 | "links": [
17 | {"name": "Website", "url": "https://github.com/vsergeyev/loudml-grafana-app"},
18 | {"name": "License", "url": "https://github.com/vsergeyev/loudml-grafana-app/blob/master/LICENSE"}
19 | ],
20 | "screenshots": [
21 | {"name": "LoudML graph panel", "path": "img/loudml_grafana_panel.png"},
22 | {"name": "LoudML datasource", "path": "img/loudml_grafana_datasource.png"}
23 | ],
24 | "version": "1.7.2",
25 | "updated": "%TODAY%"
26 | },
27 |
28 | "includes": [
29 | { "type": "panel", "name": "Loud ML Graph Panel" },
30 | { "type": "datasource", "name": "Loud ML Datasource" }
31 | ],
32 |
33 | "dependencies": {
34 | "grafanaDependency": ">=7.0.0",
35 | "plugins": []
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vsergeyev/loudml-grafana-app/86f39400a63c5b2dc900f43199300443913a20c5/src/utils.ts
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@grafana/toolkit/src/config/tsconfig.plugin.json",
3 | "include": ["src", "types"],
4 | "compilerOptions": {
5 | "alwaysStrict": false,
6 | "baseUrl": "./src",
7 | "typeRoots": ["./node_modules/@types"],
8 | "skipLibCheck": true,
9 | "suppressImplicitAnyIndexErrors": true,
10 | "noImplicitReturns": false,
11 | "noImplicitThis": false,
12 | "noImplicitUseStrict": false,
13 | "noImplicitAny": false,
14 | "noUnusedLocals": false,
15 | "allowUnreachableCode": true,
16 | "module": "commonjs",
17 | "target": "es6",
18 | "jsx": "react",
19 | "strict": false,
20 | "paths": {
21 | "@": ["."]
22 | }
23 | },
24 | "linterOptions": {
25 | "exclude": ["./dist/**", "./node_modules/**", "grafana/**"]
26 | },
27 | "exclude": ["node_modules", "grafana"],
28 | }
29 |
--------------------------------------------------------------------------------