ruby-grafana-reporter 0.4.3 → 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +144 -11
- data/lib/VERSION.rb +2 -2
- data/lib/grafana/abstract_datasource.rb +9 -3
- data/lib/grafana/dashboard.rb +6 -1
- data/lib/grafana/errors.rb +4 -11
- data/lib/grafana/grafana.rb +25 -1
- data/lib/grafana/grafana_environment_datasource.rb +56 -0
- data/lib/grafana/grafana_property_datasource.rb +8 -1
- data/lib/grafana/image_rendering_datasource.rb +5 -1
- data/lib/grafana/influxdb_datasource.rb +91 -3
- data/lib/grafana/panel.rb +1 -1
- data/lib/grafana/prometheus_datasource.rb +46 -6
- data/lib/grafana/sql_datasource.rb +3 -9
- data/lib/grafana/variable.rb +67 -36
- data/lib/grafana/webrequest.rb +1 -0
- data/lib/grafana_reporter/abstract_query.rb +62 -36
- data/lib/grafana_reporter/abstract_report.rb +19 -4
- data/lib/grafana_reporter/abstract_table_format_strategy.rb +74 -0
- data/lib/grafana_reporter/alerts_table_query.rb +6 -1
- data/lib/grafana_reporter/annotations_table_query.rb +6 -1
- data/lib/grafana_reporter/application/application.rb +7 -2
- data/lib/grafana_reporter/application/webservice.rb +41 -32
- data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +27 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +3 -2
- data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +3 -2
- data/lib/grafana_reporter/asciidoctor/help.rb +497 -0
- data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +7 -5
- data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +7 -5
- data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +5 -1
- data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +6 -2
- data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +3 -0
- data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +3 -2
- data/lib/grafana_reporter/asciidoctor/report.rb +15 -13
- data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +37 -6
- data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +1 -1
- data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +6 -2
- data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +16 -3
- data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +0 -5
- data/lib/grafana_reporter/configuration.rb +53 -22
- data/lib/grafana_reporter/console_configuration_wizard.rb +5 -3
- data/lib/grafana_reporter/csv_table_format_strategy.rb +25 -0
- data/lib/grafana_reporter/demo_report_wizard.rb +3 -7
- data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
- data/lib/grafana_reporter/erb/report.rb +13 -7
- data/lib/grafana_reporter/errors.rb +10 -8
- data/lib/grafana_reporter/panel_image_query.rb +1 -1
- data/lib/grafana_reporter/query_value_query.rb +7 -1
- data/lib/grafana_reporter/report_webhook.rb +12 -8
- data/lib/grafana_reporter/reporter_environment_datasource.rb +24 -0
- data/lib/ruby_grafana_reporter.rb +0 -0
- metadata +13 -8
- data/lib/grafana_reporter/help.rb +0 -443
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7dd2bc839c1b488d0c4e13d2837686a9c274845ec4f1a2ef46e95708aa4e96d
|
4
|
+
data.tar.gz: 353185c2bbe335ab78b0b2dca7e4edc561ccdfcc1fa72e12d139b8b93da57698
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '079bfb28f6365ecd10612076212447015839c1c31bbb976c99c29dbfd4a5af9e9d45d7d542b6e30d019beee4c409c3a86f0ab84aa6ae5db02352b985d04d2841'
|
7
|
+
data.tar.gz: b4d008a5bd31f825a88ec428b3b9e3b117f60156bdfd1609fea3b06d64ff9037a177a7dcdbee5490d79c177e41abb51e7e345b5b82f056448d7509712dcc6d07
|
data/README.md
CHANGED
@@ -12,8 +12,13 @@ Reporting Service for Grafana
|
|
12
12
|
* [Features](#features)
|
13
13
|
* [Supported datasources](#supported-datasources)
|
14
14
|
* [Quick Start](#quick-start)
|
15
|
-
* [
|
16
|
-
* [
|
15
|
+
* [Setup](#setup)
|
16
|
+
* [Grafana integration](#grafana-integration)
|
17
|
+
* [Advanced information](#advanced-information)
|
18
|
+
* [Webservice](#webservice)
|
19
|
+
* [Using ERB templates](#using-erb-templates)
|
20
|
+
* [Using webhooks](#using-webhooks)
|
21
|
+
* [Developing your own plugin](#developing-your-own-plugin)
|
17
22
|
* [Roadmap](#roadmap)
|
18
23
|
* [Donations](#donations)
|
19
24
|
|
@@ -25,8 +30,8 @@ professional reporting functionality. And this is, where the ruby grafana report
|
|
25
30
|
steps in.
|
26
31
|
|
27
32
|
The key functionality of the reporter is to capture data and images from grafana
|
28
|
-
dashboards and to use it in your custom
|
29
|
-
HTML, or any other format.
|
33
|
+
dashboards and to use it in your custom templates to finally create reports in PDF
|
34
|
+
(default), HTML, or any other format.
|
30
35
|
|
31
36
|
By default (an extended version of) Asciidoctor is enabled as template language.
|
32
37
|
|
@@ -60,7 +65,7 @@ Database | Image rendering | Raw queries | Composed queries
|
|
60
65
|
------------------------- | :-------------: | :-----------: | :------------:
|
61
66
|
all SQL based datasources | supported | supported | supported
|
62
67
|
Graphite | supported | supported | supported
|
63
|
-
InfluxDB | supported | supported |
|
68
|
+
InfluxDB | supported | supported | supported
|
64
69
|
Prometheus | supported | supported | n/a in grafana
|
65
70
|
other datasources | supported | not supported | not supported
|
66
71
|
|
@@ -73,6 +78,9 @@ specification to a raw query, which then in fact is sent to the database.
|
|
73
78
|
|
74
79
|
## Quick Start
|
75
80
|
|
81
|
+
|
82
|
+
### Setup
|
83
|
+
|
76
84
|
You don't have a grafana setup runnning already? No worries, just configure
|
77
85
|
`https://play.grafana.org` in the configuration wizard and see the magic
|
78
86
|
happen!
|
@@ -126,7 +134,7 @@ asciidoctor:
|
|
126
134
|
```
|
127
135
|
* start/restart the asciidoctor docker container
|
128
136
|
|
129
|
-
|
137
|
+
### Grafana integration
|
130
138
|
|
131
139
|
For using the reporter directly from grafana, you need to simply add a link to your
|
132
140
|
grafana dashboard:
|
@@ -158,12 +166,14 @@ you should change the link of the `Demo Report` link to
|
|
158
166
|
hitting the new link in the dashboard, grafana will add the selected template as
|
159
167
|
a variable and forward it to the reporter.
|
160
168
|
|
161
|
-
##
|
169
|
+
## Advanced information
|
170
|
+
|
171
|
+
### Webservice
|
162
172
|
|
163
173
|
Running the reporter as a webservice provides the following URLs
|
164
174
|
|
165
175
|
/overview - for all running or retained renderings
|
166
|
-
/render - for rendering a template, 'var-template' is the only mandatory GET parameter
|
176
|
+
/render - for rendering a template, 'var-template' is the only mandatory GET parameter, all parameters will be passed to the report templates as attributes
|
167
177
|
/view_report - for viewing the status or receving the result of a specific rendering, is automatically called after a successfull /render call
|
168
178
|
/cancel_report - for cancelling the rendering of a specific report, normally not called manually, but on user interaction in the /view_report or /overview URL
|
169
179
|
|
@@ -171,11 +181,135 @@ The main endpoint to call for report generation is configured in the previous ch
|
|
171
181
|
|
172
182
|
However, if you would like to see, currently running report generations and previously generated reports, you may want to call the endpoint `/overview`.
|
173
183
|
|
184
|
+
### Using ERB templates
|
185
|
+
|
186
|
+
By default the configuration wizard will setup the reporter with the asciidoctor
|
187
|
+
template language enabled. For several reasons, you may want to take advantage of
|
188
|
+
the ruby included
|
189
|
+
[ERB template language](https://docs.ruby-lang.org/en/master/ERB.html).
|
190
|
+
|
191
|
+
Anyway you should consider, that ERB templates can include harmful code. So make
|
192
|
+
sure, that you will only use ERB templates in a safe environment.
|
193
|
+
|
194
|
+
To enable the ERB template language, you need to modify your configuration file
|
195
|
+
in the section `grafana-reporter`:
|
196
|
+
|
197
|
+
````
|
198
|
+
grafana-reporter:
|
199
|
+
report-class: GrafanaReporter::ERB::Report
|
200
|
+
````
|
201
|
+
|
202
|
+
Restart the grafana reporter instance, if running as webservice. That's all.
|
203
|
+
|
204
|
+
In ERB templates, you have access to the variables `report`, which is a reference
|
205
|
+
to the currently executed
|
206
|
+
[ERB Report object](https://rubydoc.info/gems/ruby-grafana-reporter/GrafanaReporter/ERB/Report)
|
207
|
+
and `attributes`, which contains a hash
|
208
|
+
of variables, which have been handed over to the report generations, e.g. from
|
209
|
+
a webservice call.
|
210
|
+
|
211
|
+
To test the configuration, you may want to run the configuration wizard again,
|
212
|
+
which will create an ERB template for you.
|
213
|
+
|
214
|
+
### Using webhooks
|
215
|
+
|
216
|
+
Webhooks provide an easy way to get automatically informed about the progress
|
217
|
+
of a report. The nice thing is, that this is completely independent from
|
218
|
+
running the reporter as webservice, i.e. these callbacks are also called if you
|
219
|
+
run the reporter standalone.
|
220
|
+
|
221
|
+
To use webhooks, you have to specify, in which progress states of a report you
|
222
|
+
are interested. Therefore you have to configure it in the `grafana-reporter`
|
223
|
+
section of your configuration file, e.g.
|
224
|
+
|
225
|
+
````
|
226
|
+
grafana-reporter:
|
227
|
+
callbacks:
|
228
|
+
all:
|
229
|
+
- http://<<your_callback_url>>
|
230
|
+
````
|
231
|
+
|
232
|
+
Remember to restart the reporter, if it is running as a webservice.
|
233
|
+
|
234
|
+
After having done so, your callback url will be called for each event with
|
235
|
+
a JSON body including all necessary information of the report. For details see
|
236
|
+
[callback](https://rubydoc.info/gems/ruby-grafana-reporter/GrafanaReporter/ReportWebhook#callback-instance_method).
|
237
|
+
|
238
|
+
### Developing your own plugin
|
239
|
+
|
240
|
+
The reporter is designed to allow easy integration of your own plugins,
|
241
|
+
without having to modify the reporter base source on github (or anywhere
|
242
|
+
else). This section shows how to implement and load a custom datasource.
|
243
|
+
|
244
|
+
Implementing a custom datasource is needed, if you use a custom datasource
|
245
|
+
grafana plugin, which is not yet supported by the reporter. In that case you
|
246
|
+
can build your own custom datasource for the reporter and load it on demand
|
247
|
+
with a command line parameter, without having to build your own fork of this
|
248
|
+
project.
|
249
|
+
|
250
|
+
This documentation will provide a simple, but mocked implementation of an
|
251
|
+
imagined grafana datasource.
|
252
|
+
|
253
|
+
First of all, let's create a new text file, e.g. `my_datasource.rb` with the
|
254
|
+
following content:
|
255
|
+
|
256
|
+
````
|
257
|
+
class MyDatasource < ::Grafana::AbstractDatasource
|
258
|
+
def self.handles?(model)
|
259
|
+
tmp = new(model)
|
260
|
+
tmp.type == 'my_datasource'
|
261
|
+
end
|
262
|
+
|
263
|
+
def request(query_description)
|
264
|
+
# see https://rubydoc.info/gems/ruby-grafana-reporter/Grafana/AbstractDatasource#request-instance_method
|
265
|
+
# for detailed information of given parameters and expected return format
|
266
|
+
|
267
|
+
# TODO: call your datasource, e.g. via REST call
|
268
|
+
# TODO: return the value in the needed format
|
269
|
+
end
|
270
|
+
|
271
|
+
def raw_query_from_panel_model(panel_query_target)
|
272
|
+
# TODO: extract or build the query from the given grafana panel query target hash
|
273
|
+
end
|
274
|
+
|
275
|
+
def default_variable_format
|
276
|
+
# TODO, specify the default variable format
|
277
|
+
# see https://rubydoc.info/gems/ruby-grafana-reporter/Grafana/Variable#value_formatted-instance_method
|
278
|
+
# for detailed information.
|
279
|
+
end
|
280
|
+
end
|
281
|
+
````
|
282
|
+
|
283
|
+
The only thing left to do now, is to make this datasource known to the
|
284
|
+
reporter. This can be done with the `-r` command line flag, e.g.
|
285
|
+
|
286
|
+
````
|
287
|
+
ruby-grafana-reporter -r my_datasource.rb
|
288
|
+
````
|
289
|
+
|
290
|
+
The reporter implemented some magic, to automatically register datasource
|
291
|
+
implementations on load, if they inherit from `::Grafana::AbstractDatasource`.
|
292
|
+
This means, that you don't have to do anything else here.
|
293
|
+
|
294
|
+
Now the reporter knows about your datasource implementation and will use it,
|
295
|
+
if you request information from a panel, which is linked to the type
|
296
|
+
`my_datasource` as specified in the `handles?` method above. If any errors
|
297
|
+
occur during execution, the reporter will catch them and show them in the error
|
298
|
+
log.
|
299
|
+
|
300
|
+
Registering a custom ruby file is independent from running the reporter as a
|
301
|
+
webservice or as a standalone executable. In any case the reporter will apply
|
302
|
+
the file.
|
303
|
+
|
304
|
+
Technically, loading your own plugin will call require for your ruby file,
|
305
|
+
_after_ all reporter files have been loaded and _before_ the execution of the
|
306
|
+
webservice or a rendering process starts.
|
307
|
+
|
174
308
|
## Roadmap
|
175
309
|
|
176
310
|
This is just a collection of things, I am heading for in future, without a schedule.
|
177
311
|
|
178
|
-
* Support
|
312
|
+
* Support grafana internal datasources
|
179
313
|
* Solve code TODOs
|
180
314
|
* Become [rubocop](https://rubocop.org/) ready
|
181
315
|
|
@@ -197,7 +331,6 @@ Inspired by [Izak Marai's grafana reporter](https://github.com/IzakMarais/report
|
|
197
331
|
|
198
332
|
## Donations
|
199
333
|
|
200
|
-
If this project
|
201
|
-
support my work, feel free donate. :)
|
334
|
+
If you like this project and you would like to support my work, feel free to donate. :)
|
202
335
|
|
203
336
|
[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate?hosted_button_id=35LH6JNLPHPHQ)
|
data/lib/VERSION.rb
CHANGED
@@ -42,12 +42,12 @@ module Grafana
|
|
42
42
|
@model = model
|
43
43
|
end
|
44
44
|
|
45
|
-
# @return [String] category of the datasource, e.g.
|
45
|
+
# @return [String] category of the datasource, e.g. +tsdb+ or +sql+
|
46
46
|
def category
|
47
47
|
@model['meta']['category']
|
48
48
|
end
|
49
49
|
|
50
|
-
# @return [String] type of the datasource, e.g.
|
50
|
+
# @return [String] type of the datasource, e.g. +mysql+
|
51
51
|
def type
|
52
52
|
@model['type'] || @model['meta']['id']
|
53
53
|
end
|
@@ -126,10 +126,16 @@ module Grafana
|
|
126
126
|
while repeat && (repeat_count < 3)
|
127
127
|
repeat = false
|
128
128
|
repeat_count += 1
|
129
|
+
|
129
130
|
variables.each do |name, variable|
|
131
|
+
# do not replace with non grafana variables
|
132
|
+
next unless name =~ /^var-/
|
133
|
+
|
130
134
|
# only set ticks if value is string
|
131
135
|
var_name = name.gsub(/^var-/, '')
|
132
|
-
|
136
|
+
next unless var_name =~ /^\w+$/
|
137
|
+
|
138
|
+
res = res.gsub(/(?:\$\{#{var_name}(?::(?<format>\w+))?\}|\$#{var_name}(?!\w))/) do
|
133
139
|
format = default_variable_format
|
134
140
|
if $LAST_MATCH_INFO
|
135
141
|
format = $LAST_MATCH_INFO[:format] if $LAST_MATCH_INFO[:format]
|
data/lib/grafana/dashboard.rb
CHANGED
@@ -35,6 +35,11 @@ module Grafana
|
|
35
35
|
@model['uid']
|
36
36
|
end
|
37
37
|
|
38
|
+
# @return [String] dashboard title
|
39
|
+
def title
|
40
|
+
@model['title']
|
41
|
+
end
|
42
|
+
|
38
43
|
# @return [Panel] panel for the specified ID
|
39
44
|
def panel(id)
|
40
45
|
panels = @panels.select { |item| item.field('id') == id.to_i }
|
@@ -53,7 +58,7 @@ module Grafana
|
|
53
58
|
list = @model['templating']['list']
|
54
59
|
return unless list.is_a? Array
|
55
60
|
|
56
|
-
list.each { |item| @variables << Variable.new(item) }
|
61
|
+
list.each { |item| @variables << Variable.new(item, self) }
|
57
62
|
end
|
58
63
|
|
59
64
|
# read panels
|
data/lib/grafana/errors.rb
CHANGED
@@ -37,9 +37,9 @@ module Grafana
|
|
37
37
|
|
38
38
|
# Raised if a given datasource does not exist in a specific {Grafana} instance.
|
39
39
|
class DatasourceDoesNotExistError < GrafanaError
|
40
|
-
# @param field [String] specifies, how the datasource has been searched, e.g.
|
40
|
+
# @param field [String] specifies, how the datasource has been searched, e.g. +id+ or +name+
|
41
41
|
# @param datasource_identifier [String] identifier of the datasource, which could not be found,
|
42
|
-
# e.g. the
|
42
|
+
# e.g. the specified id or name
|
43
43
|
def initialize(field, datasource_identifier)
|
44
44
|
super("Datasource with #{field} '#{datasource_identifier}' does not exist.")
|
45
45
|
end
|
@@ -53,7 +53,8 @@ module Grafana
|
|
53
53
|
class ImageCouldNotBeRenderedError < GrafanaError
|
54
54
|
def initialize(panel)
|
55
55
|
super("The specified panel '#{panel.id}' from dashboard '#{panel.dashboard.id}' could not be "\
|
56
|
-
'rendered to an image.'
|
56
|
+
'rendered to an image. Check if rendering is possible manually by selecting "Share" and then '\
|
57
|
+
'"Direct link rendered image" from a panel\'s options menu.')
|
57
58
|
end
|
58
59
|
end
|
59
60
|
|
@@ -70,12 +71,4 @@ module Grafana
|
|
70
71
|
super("The datasource query provided, does not look like a grafana datasource target (received: #{query}).")
|
71
72
|
end
|
72
73
|
end
|
73
|
-
|
74
|
-
# Raised if a datasource implementation cannot handle a query, which is composed
|
75
|
-
# in the grafana visual editor.
|
76
|
-
class ComposedQueryNotSupportedError < GrafanaError
|
77
|
-
def initialize(class_obj)
|
78
|
-
super("Composed queries are not yet supported for datasource '#{class_obj}'.")
|
79
|
-
end
|
80
|
-
end
|
81
74
|
end
|
data/lib/grafana/grafana.rb
CHANGED
@@ -25,6 +25,30 @@ module Grafana
|
|
25
25
|
initialize_datasources unless @base_uri.empty?
|
26
26
|
end
|
27
27
|
|
28
|
+
# @return [Hash] Information about the current organization
|
29
|
+
def organization
|
30
|
+
return @organization if @organization
|
31
|
+
|
32
|
+
response = prepare_request({ relative_url: '/api/org/' }).execute
|
33
|
+
if response.is_a?(Net::HTTPOK)
|
34
|
+
@organization = JSON.parse(response.body)
|
35
|
+
end
|
36
|
+
|
37
|
+
@organization
|
38
|
+
end
|
39
|
+
|
40
|
+
# @return [String] grafana version
|
41
|
+
def version
|
42
|
+
return @version if @version
|
43
|
+
|
44
|
+
response = prepare_request({ relative_url: '/api/health' }).execute
|
45
|
+
if response.is_a?(Net::HTTPOK)
|
46
|
+
@version = JSON.parse(response.body)['version']
|
47
|
+
end
|
48
|
+
|
49
|
+
@version
|
50
|
+
end
|
51
|
+
|
28
52
|
# Used to test a connection to the grafana instance.
|
29
53
|
#
|
30
54
|
# Running this function also determines, if the API configured here has Admin or NON-Admin privileges,
|
@@ -50,7 +74,7 @@ module Grafana
|
|
50
74
|
# @return [Datasource] Datasource for the specified datasource name
|
51
75
|
def datasource_by_name(datasource_name)
|
52
76
|
datasource_name = 'default' if datasource_name.to_s.empty?
|
53
|
-
# TODO: add support for grafana builtin datasource types
|
77
|
+
# TODO: PRIO add support for grafana builtin datasource types
|
54
78
|
return UnsupportedDatasource.new(nil) if datasource_name.to_s =~ /-- (?:Mixed|Dashboard|Grafana) --/
|
55
79
|
raise DatasourceDoesNotExistError.new('name', datasource_name) unless @datasources[datasource_name]
|
56
80
|
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements a datasource to return environment related information about the grafana instance in a tabular format.
|
5
|
+
class GrafanaEnvironmentDatasource < ::Grafana::AbstractDatasource
|
6
|
+
# +:raw_query+ needs to contain a Hash with the following structure:
|
7
|
+
#
|
8
|
+
# {
|
9
|
+
# grafana: {Grafana} object to query
|
10
|
+
# mode: 'general' (default) or 'dashboards' for receiving different environment information
|
11
|
+
# }
|
12
|
+
# @see AbstractDatasource#request
|
13
|
+
def request(query_description)
|
14
|
+
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
15
|
+
raw_query = {mode: 'general'}.merge(query_description[:raw_query])
|
16
|
+
|
17
|
+
return dashboards_data(raw_query[:grafana]) if raw_query[:mode] == 'dashboards'
|
18
|
+
|
19
|
+
general_data(raw_query[:grafana])
|
20
|
+
end
|
21
|
+
|
22
|
+
# @see AbstractDatasource#default_variable_format
|
23
|
+
def default_variable_format
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# @see AbstractDatasource#name
|
28
|
+
def name
|
29
|
+
self.class.to_s
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def general_data(grafana)
|
35
|
+
{
|
36
|
+
header: ['Version', 'Organization Name', 'Organization ID', 'Access permissions'],
|
37
|
+
content: [[grafana.version,
|
38
|
+
grafana.organization['name'],
|
39
|
+
grafana.organization['id'],
|
40
|
+
grafana.test_connection]]
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
def dashboards_data(grafana)
|
45
|
+
content = []
|
46
|
+
grafana.dashboard_ids.each do |id|
|
47
|
+
content << [id, grafana.dashboard(id).title, grafana.dashboard(id).panels.length]
|
48
|
+
end
|
49
|
+
|
50
|
+
{
|
51
|
+
header: ['Dashboard ID', 'Dashboard Name', '# Panels'],
|
52
|
+
content: content
|
53
|
+
}
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -16,6 +16,8 @@ module Grafana
|
|
16
16
|
panel = query_description[:raw_query][:panel]
|
17
17
|
property_name = query_description[:raw_query][:property_name]
|
18
18
|
|
19
|
+
return "Panel property '#{property_name}' does not exist for panel '#{panel.id}'" unless panel.field(property_name)
|
20
|
+
|
19
21
|
{
|
20
22
|
header: [query_description[:raw_query][:property_name]],
|
21
23
|
content: [replace_variables(panel.field(property_name), query_description[:variables])]
|
@@ -24,7 +26,12 @@ module Grafana
|
|
24
26
|
|
25
27
|
# @see AbstractDatasource#default_variable_format
|
26
28
|
def default_variable_format
|
27
|
-
|
29
|
+
'glob'
|
30
|
+
end
|
31
|
+
|
32
|
+
# @see AbstractDatasource#name
|
33
|
+
def name
|
34
|
+
self.class.to_s
|
28
35
|
end
|
29
36
|
end
|
30
37
|
end
|
@@ -10,12 +10,16 @@ module Grafana
|
|
10
10
|
# }
|
11
11
|
# @see AbstractDatasource#request
|
12
12
|
def request(query_description)
|
13
|
+
panel = query_description[:raw_query][:panel]
|
14
|
+
|
13
15
|
webrequest = query_description[:prepared_request]
|
14
|
-
webrequest.relative_url =
|
16
|
+
webrequest.relative_url = panel.render_url + url_params(query_description)
|
15
17
|
webrequest.options.merge!({ accept: 'image/png' })
|
16
18
|
|
17
19
|
result = webrequest.execute
|
18
20
|
|
21
|
+
raise ImageCouldNotBeRenderedError, panel if result.body.include?('<html')
|
22
|
+
|
19
23
|
{ header: ['image'], content: [result.body] }
|
20
24
|
end
|
21
25
|
|
@@ -15,7 +15,23 @@ module Grafana
|
|
15
15
|
def request(query_description)
|
16
16
|
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
17
17
|
|
18
|
-
|
18
|
+
# replace variables
|
19
|
+
query = replace_variables(query_description[:raw_query], query_description[:variables])
|
20
|
+
|
21
|
+
# Unfortunately the grafana internal variables are not replaced in the grafana backend, but in the
|
22
|
+
# frontend, i.e. we have to replace them here manually
|
23
|
+
# replace $timeFilter variable
|
24
|
+
query = query.gsub(/\$timeFilter(?=\W|$)/, "time >= #{query_description[:from]}ms and time <= #{query_description[:to]}ms")
|
25
|
+
|
26
|
+
interval = query_description[:variables].delete('interval') || ((query_description[:to].to_i - query_description[:from].to_i) / 1000).to_i
|
27
|
+
interval = interval.raw_value if interval.is_a?(Variable)
|
28
|
+
|
29
|
+
# replace grafana variables $__interval and $__interval_ms in query
|
30
|
+
# TODO: check where calculation and replacement of interval variable should take place
|
31
|
+
query = query.gsub(/\$(?:__)?interval(?=\W|$)/, "#{interval.is_a?(String) ? interval : "#{(interval / 1000).to_i}s"}")
|
32
|
+
query = query.gsub(/\$(?:__)?interval_ms(?=\W|$)/, "#{interval}")
|
33
|
+
|
34
|
+
url = "/api/datasources/proxy/#{id}/query?db=#{@model['database']}&q=#{ERB::Util.url_encode(query)}&epoch=ms"
|
19
35
|
|
20
36
|
webrequest = query_description[:prepared_request]
|
21
37
|
webrequest.relative_url = url
|
@@ -29,8 +45,8 @@ module Grafana
|
|
29
45
|
def raw_query_from_panel_model(panel_query_target)
|
30
46
|
return panel_query_target['query'] if panel_query_target['rawQuery']
|
31
47
|
|
32
|
-
#
|
33
|
-
|
48
|
+
# build composed queries
|
49
|
+
build_select(panel_query_target['select']) + build_from(panel_query_target) + build_where(panel_query_target['tags']) + build_group_by(panel_query_target['groupBy'])
|
34
50
|
end
|
35
51
|
|
36
52
|
# @see AbstractDatasource#default_variable_format
|
@@ -40,10 +56,82 @@ module Grafana
|
|
40
56
|
|
41
57
|
private
|
42
58
|
|
59
|
+
def build_group_by(stmt)
|
60
|
+
groups = []
|
61
|
+
fill = ""
|
62
|
+
|
63
|
+
stmt.each do |group|
|
64
|
+
case group['type']
|
65
|
+
when 'tag'
|
66
|
+
groups << "\"#{group['params'].first}\""
|
67
|
+
|
68
|
+
when 'fill'
|
69
|
+
fill = " fill(#{group['params'].first})"
|
70
|
+
|
71
|
+
else
|
72
|
+
groups << "#{group['type']}(#{group['params'].join(', ')})"
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
" GROUP BY #{groups.join(', ')}#{fill}"
|
78
|
+
end
|
79
|
+
|
80
|
+
def build_where(stmt)
|
81
|
+
custom_where = []
|
82
|
+
|
83
|
+
stmt.each do |where|
|
84
|
+
value = where['operator'] =~ /^[=!]~$/ ? where['value'] : "'#{where['value']}'"
|
85
|
+
custom_where << "\"#{where['key']}\" #{where['operator']} #{value}"
|
86
|
+
end
|
87
|
+
|
88
|
+
" WHERE #{"(#{custom_where.join(' AND ')}) AND " unless custom_where.empty?}$timeFilter"
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_from(stmt)
|
92
|
+
" FROM \"#{"stmt['policy']." unless stmt['policy'] == 'default'}#{stmt['measurement']}\""
|
93
|
+
end
|
94
|
+
|
95
|
+
def build_select(stmt)
|
96
|
+
res = "SELECT"
|
97
|
+
parts = []
|
98
|
+
|
99
|
+
stmt.each do |value|
|
100
|
+
part = ""
|
101
|
+
|
102
|
+
value.each do |item|
|
103
|
+
case item['type']
|
104
|
+
when 'field'
|
105
|
+
# frame field parameter as string
|
106
|
+
part = "\"#{item['params'].first}\""
|
107
|
+
|
108
|
+
when 'alias'
|
109
|
+
# append AS with parameter as string
|
110
|
+
part = "#{part} AS \"#{item['params'].first}\""
|
111
|
+
|
112
|
+
|
113
|
+
when 'math'
|
114
|
+
# append parameter as raw value for calculation
|
115
|
+
part = "#{part} #{item['params'].first}"
|
116
|
+
|
117
|
+
|
118
|
+
else
|
119
|
+
# frame current part by brackets and call by item function including parameters
|
120
|
+
part = "#{item['type']}(#{part}#{", #{item['params'].join(', ')}" unless item['params'].empty?})"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
parts << part
|
125
|
+
end
|
126
|
+
|
127
|
+
"#{res} #{parts.join(', ')}"
|
128
|
+
end
|
129
|
+
|
43
130
|
# @see AbstractDatasource#preformat_response
|
44
131
|
def preformat_response(response_body)
|
45
132
|
# TODO: how to handle multiple query results?
|
46
133
|
json = JSON.parse(response_body)['results'].first['series']
|
134
|
+
return {} if json.nil?
|
47
135
|
|
48
136
|
header = ['time']
|
49
137
|
content = {}
|
data/lib/grafana/panel.rb
CHANGED
@@ -14,9 +14,25 @@ module Grafana
|
|
14
14
|
def request(query_description)
|
15
15
|
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
16
16
|
|
17
|
-
|
18
|
-
|
19
|
-
|
17
|
+
query_hash = query_description[:raw_query].is_a?(Hash) ? query_description[:raw_query] : {}
|
18
|
+
|
19
|
+
# read instant value and convert instant value to boolean value
|
20
|
+
instant = query_description[:variables].delete('instant') || query_hash[:instant] || false
|
21
|
+
instant = instant.raw_value if instant.is_a?(Variable)
|
22
|
+
instant = instant.to_s.downcase == 'true'
|
23
|
+
interval = query_description[:variables].delete('interval') || query_hash[:interval] || 15
|
24
|
+
interval = interval.raw_value if interval.is_a?(Variable)
|
25
|
+
query = query_hash[:query] || query_description[:raw_query]
|
26
|
+
|
27
|
+
url = if instant
|
28
|
+
"/api/datasources/proxy/#{id}/api/v1/query?time=#{query_description[:to]}&query="\
|
29
|
+
"#{CGI.escape(replace_variables(query, query_description[:variables]))}"
|
30
|
+
else
|
31
|
+
"/api/datasources/proxy/#{id}/api/v1/query_range?start=#{query_description[:from]}"\
|
32
|
+
"&end=#{query_description[:to]}"\
|
33
|
+
"&query=#{CGI.escape(replace_variables(query, query_description[:variables]))}"\
|
34
|
+
"&step=#{interval}"
|
35
|
+
end
|
20
36
|
|
21
37
|
webrequest = query_description[:prepared_request]
|
22
38
|
webrequest.relative_url = url
|
@@ -28,7 +44,8 @@ module Grafana
|
|
28
44
|
|
29
45
|
# @see AbstractDatasource#raw_query_from_panel_model
|
30
46
|
def raw_query_from_panel_model(panel_query_target)
|
31
|
-
panel_query_target['expr']
|
47
|
+
{ query: panel_query_target['expr'], instant: panel_query_target['instant'],
|
48
|
+
interval: panel_query_target['step'] }
|
32
49
|
end
|
33
50
|
|
34
51
|
# @see AbstractDatasource#default_variable_format
|
@@ -40,14 +57,37 @@ module Grafana
|
|
40
57
|
|
41
58
|
# @see AbstractDatasource#preformat_response
|
42
59
|
def preformat_response(response_body)
|
43
|
-
json = JSON.parse(response_body)
|
60
|
+
json = JSON.parse(response_body)
|
61
|
+
|
62
|
+
# handle response with error result
|
63
|
+
unless json['error'].nil?
|
64
|
+
return { header: ['error'], content: [[ json['error'] ]] }
|
65
|
+
end
|
66
|
+
|
67
|
+
result_type = json['data']['resultType']
|
68
|
+
json = json['data']['result']
|
44
69
|
|
45
70
|
headers = ['time']
|
46
71
|
content = {}
|
47
72
|
|
73
|
+
# handle vector queries
|
74
|
+
if result_type == 'vector'
|
75
|
+
return {
|
76
|
+
header: (headers << 'value') + json.first['metric'].keys,
|
77
|
+
content: [ [json.first['value'][0], json.first['value'][1]] + json.first['metric'].values ]
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
# handle scalar queries
|
82
|
+
if result_type =~ /^(?:scalar|string)$/
|
83
|
+
return { header: headers << result_type, content: [[json[0], json[1]]] }
|
84
|
+
end
|
85
|
+
|
48
86
|
# keep sorting, if json has only one target item, otherwise merge results and return
|
49
87
|
# as a time sorted array
|
50
|
-
|
88
|
+
if json.length == 1
|
89
|
+
return { header: headers << json.first['metric']['mode'], content: json.first['values'] }
|
90
|
+
end
|
51
91
|
|
52
92
|
# TODO: show warning if results may be sorted different
|
53
93
|
json.each_index do |i|
|