ruby-grafana-reporter 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
[](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
|