ruby-grafana-reporter 0.4.2 → 0.5.0
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 +87 -3
- data/lib/grafana/panel.rb +1 -1
- data/lib/grafana/prometheus_datasource.rb +11 -2
- 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 +65 -39
- 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 +470 -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 +5 -1
- data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +0 -5
- data/lib/grafana_reporter/configuration.rb +27 -0
- 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 +9 -7
- 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
- metadata +9 -3
- 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: '06912db0a0635560dca75c00c01c047deac42741c7c6ba2ee717cbccdf26cb19'
|
4
|
+
data.tar.gz: 8d09e509266737bf1b73c3f6ad5c03255de461c116a6e5ec7b45bfaef56520b7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 66e81686e253f8e98101f97b0b08db97f955cd1c274f2ed683f597b675c228adf198cd2c66f9ce3df05506a760564643e43b94c40fa952b13748122ecc8d6890
|
7
|
+
data.tar.gz: 916ec07231acf19c20b0f01d7bfe9a47d5825a8e0c3a6c67a9fa615e2b993343d1506fb129cc72b66e2be9b02102c15e338a6e8b5746286494de4164a9678c38
|
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,19 @@ 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
|
+
# replace grafana variables $__interval and $__interval_ms in query
|
27
|
+
query = query.gsub(/\$(?:__)?interval(?=\W|$)/, "#{((query_description[:to].to_i - query_description[:from].to_i) / 1000 / 1000).to_i}s")
|
28
|
+
query = query.gsub(/\$(?:__)?interval_ms(?=\W|$)/, "#{((query_description[:to].to_i - query_description[:from].to_i) / 1000).to_i}")
|
29
|
+
|
30
|
+
url = "/api/datasources/proxy/#{id}/query?db=#{@model['database']}&q=#{ERB::Util.url_encode(query)}&epoch=ms"
|
19
31
|
|
20
32
|
webrequest = query_description[:prepared_request]
|
21
33
|
webrequest.relative_url = url
|
@@ -29,8 +41,8 @@ module Grafana
|
|
29
41
|
def raw_query_from_panel_model(panel_query_target)
|
30
42
|
return panel_query_target['query'] if panel_query_target['rawQuery']
|
31
43
|
|
32
|
-
#
|
33
|
-
|
44
|
+
# build composed queries
|
45
|
+
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
46
|
end
|
35
47
|
|
36
48
|
# @see AbstractDatasource#default_variable_format
|
@@ -40,10 +52,82 @@ module Grafana
|
|
40
52
|
|
41
53
|
private
|
42
54
|
|
55
|
+
def build_group_by(stmt)
|
56
|
+
groups = []
|
57
|
+
fill = ""
|
58
|
+
|
59
|
+
stmt.each do |group|
|
60
|
+
case group['type']
|
61
|
+
when 'tag'
|
62
|
+
groups << "\"#{group['params'].first}\""
|
63
|
+
|
64
|
+
when 'fill'
|
65
|
+
fill = " fill(#{group['params'].first})"
|
66
|
+
|
67
|
+
else
|
68
|
+
groups << "#{group['type']}(#{group['params'].join(', ')})"
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
" GROUP BY #{groups.join(', ')}#{fill}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_where(stmt)
|
77
|
+
custom_where = []
|
78
|
+
|
79
|
+
stmt.each do |where|
|
80
|
+
value = where['operator'] =~ /^[=!]~$/ ? where['value'] : "'#{where['value']}'"
|
81
|
+
custom_where << "\"#{where['key']}\" #{where['operator']} #{value}"
|
82
|
+
end
|
83
|
+
|
84
|
+
" WHERE #{"(#{custom_where.join(' AND ')}) AND " unless custom_where.empty?}$timeFilter"
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_from(stmt)
|
88
|
+
" FROM \"#{"stmt['policy']." unless stmt['policy'] == 'default'}#{stmt['measurement']}\""
|
89
|
+
end
|
90
|
+
|
91
|
+
def build_select(stmt)
|
92
|
+
res = "SELECT"
|
93
|
+
parts = []
|
94
|
+
|
95
|
+
stmt.each do |value|
|
96
|
+
part = ""
|
97
|
+
|
98
|
+
value.each do |item|
|
99
|
+
case item['type']
|
100
|
+
when 'field'
|
101
|
+
# frame field parameter as string
|
102
|
+
part = "\"#{item['params'].first}\""
|
103
|
+
|
104
|
+
when 'alias'
|
105
|
+
# append AS with parameter as string
|
106
|
+
part = "#{part} AS \"#{item['params'].first}\""
|
107
|
+
|
108
|
+
|
109
|
+
when 'math'
|
110
|
+
# append parameter as raw value for calculation
|
111
|
+
part = "#{part} #{item['params'].first}"
|
112
|
+
|
113
|
+
|
114
|
+
else
|
115
|
+
# frame current part by brackets and call by item function including parameters
|
116
|
+
part = "#{item['type']}(#{part}#{", #{item['params'].join(', ')}" unless item['params'].empty?})"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
parts << part
|
121
|
+
end
|
122
|
+
|
123
|
+
"#{res} #{parts.join(', ')}"
|
124
|
+
end
|
125
|
+
|
43
126
|
# @see AbstractDatasource#preformat_response
|
44
127
|
def preformat_response(response_body)
|
45
128
|
# TODO: how to handle multiple query results?
|
46
129
|
json = JSON.parse(response_body)['results'].first['series']
|
130
|
+
return {} if json.nil?
|
47
131
|
|
48
132
|
header = ['time']
|
49
133
|
content = {}
|
data/lib/grafana/panel.rb
CHANGED
@@ -14,7 +14,11 @@ module Grafana
|
|
14
14
|
def request(query_description)
|
15
15
|
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
16
16
|
|
17
|
-
|
17
|
+
# TODO: properly allow endpoint to be set - also check raw_query method
|
18
|
+
end_point = @endpoint ? @endpoint : "query_range"
|
19
|
+
|
20
|
+
# TODO: set query option 'step' on request
|
21
|
+
url = "/api/datasources/proxy/#{id}/api/v1/#{end_point}?"\
|
18
22
|
"start=#{query_description[:from]}&end=#{query_description[:to]}"\
|
19
23
|
"&query=#{replace_variables(query_description[:raw_query], query_description[:variables])}"
|
20
24
|
|
@@ -28,6 +32,7 @@ module Grafana
|
|
28
32
|
|
29
33
|
# @see AbstractDatasource#raw_query_from_panel_model
|
30
34
|
def raw_query_from_panel_model(panel_query_target)
|
35
|
+
@endpoint = panel_query_target['format'] == 'time_series' && (panel_query_target['instant'] == false || !panel_query_target['instant']) ? 'query_range' : 'query'
|
31
36
|
panel_query_target['expr']
|
32
37
|
end
|
33
38
|
|
@@ -47,7 +52,11 @@ module Grafana
|
|
47
52
|
|
48
53
|
# keep sorting, if json has only one target item, otherwise merge results and return
|
49
54
|
# as a time sorted array
|
50
|
-
|
55
|
+
# TODO properly set headlines
|
56
|
+
if json.length == 1
|
57
|
+
return { header: headers << json.first['metric'].to_s, content: [[json.first['value'][1], json.first['value'][0]]] } if json.first.has_key?('value') # this happens for the special case of calls to '/query' endpoint
|
58
|
+
return { header: headers << json.first['metric']['mode'], content: json.first['values'] }
|
59
|
+
end
|
51
60
|
|
52
61
|
# TODO: show warning if results may be sorted different
|
53
62
|
json.each_index do |i|
|
@@ -19,7 +19,7 @@ module Grafana
|
|
19
19
|
body: {
|
20
20
|
from: query_description[:from],
|
21
21
|
to: query_description[:to],
|
22
|
-
queries: [rawSql:
|
22
|
+
queries: [rawSql: sql, datasourceId: id, format: 'table']
|
23
23
|
}.to_json,
|
24
24
|
request: Net::HTTP::Post
|
25
25
|
}
|
@@ -49,6 +49,8 @@ module Grafana
|
|
49
49
|
def preformat_response(response_body)
|
50
50
|
results = {}
|
51
51
|
results.default = []
|
52
|
+
results[:header] = []
|
53
|
+
results[:content] = []
|
52
54
|
|
53
55
|
JSON.parse(response_body)['results'].each_value do |query_result|
|
54
56
|
if query_result.key?('error')
|
@@ -66,13 +68,5 @@ module Grafana
|
|
66
68
|
|
67
69
|
results
|
68
70
|
end
|
69
|
-
|
70
|
-
def prepare_sql(sql)
|
71
|
-
# remove comments in query
|
72
|
-
sql.gsub!(/--[^\r\n]*(?:[\r\n]+|$)/, ' ')
|
73
|
-
sql.gsub!(/\r\n/, ' ')
|
74
|
-
sql.gsub!(/\n/, ' ')
|
75
|
-
sql
|
76
|
-
end
|
77
71
|
end
|
78
72
|
end
|