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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +144 -11
  3. data/lib/VERSION.rb +2 -2
  4. data/lib/grafana/abstract_datasource.rb +9 -3
  5. data/lib/grafana/dashboard.rb +6 -1
  6. data/lib/grafana/errors.rb +4 -11
  7. data/lib/grafana/grafana.rb +25 -1
  8. data/lib/grafana/grafana_environment_datasource.rb +56 -0
  9. data/lib/grafana/grafana_property_datasource.rb +8 -1
  10. data/lib/grafana/image_rendering_datasource.rb +5 -1
  11. data/lib/grafana/influxdb_datasource.rb +87 -3
  12. data/lib/grafana/panel.rb +1 -1
  13. data/lib/grafana/prometheus_datasource.rb +11 -2
  14. data/lib/grafana/sql_datasource.rb +3 -9
  15. data/lib/grafana/variable.rb +67 -36
  16. data/lib/grafana/webrequest.rb +1 -0
  17. data/lib/grafana_reporter/abstract_query.rb +65 -39
  18. data/lib/grafana_reporter/abstract_report.rb +19 -4
  19. data/lib/grafana_reporter/abstract_table_format_strategy.rb +74 -0
  20. data/lib/grafana_reporter/alerts_table_query.rb +6 -1
  21. data/lib/grafana_reporter/annotations_table_query.rb +6 -1
  22. data/lib/grafana_reporter/application/application.rb +7 -2
  23. data/lib/grafana_reporter/application/webservice.rb +41 -32
  24. data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +27 -0
  25. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +3 -2
  26. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +3 -2
  27. data/lib/grafana_reporter/asciidoctor/help.rb +470 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +7 -5
  29. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +7 -5
  30. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +5 -1
  31. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +6 -2
  32. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +3 -0
  33. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +3 -2
  34. data/lib/grafana_reporter/asciidoctor/report.rb +15 -13
  35. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +37 -6
  36. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +1 -1
  37. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +6 -2
  38. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +5 -1
  39. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +0 -5
  40. data/lib/grafana_reporter/configuration.rb +27 -0
  41. data/lib/grafana_reporter/console_configuration_wizard.rb +5 -3
  42. data/lib/grafana_reporter/csv_table_format_strategy.rb +25 -0
  43. data/lib/grafana_reporter/demo_report_wizard.rb +3 -7
  44. data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
  45. data/lib/grafana_reporter/erb/report.rb +13 -7
  46. data/lib/grafana_reporter/errors.rb +9 -7
  47. data/lib/grafana_reporter/panel_image_query.rb +1 -1
  48. data/lib/grafana_reporter/query_value_query.rb +7 -1
  49. data/lib/grafana_reporter/report_webhook.rb +12 -8
  50. data/lib/grafana_reporter/reporter_environment_datasource.rb +24 -0
  51. metadata +9 -3
  52. data/lib/grafana_reporter/help.rb +0 -443
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ab1861d0ed6af7383a37bd357399451e38fb44d23489a52716660bae15a4f95
4
- data.tar.gz: c744af8cd37f2fc469cef0d384d03e0610af5d38157a1367346f40986c29366c
3
+ metadata.gz: '06912db0a0635560dca75c00c01c047deac42741c7c6ba2ee717cbccdf26cb19'
4
+ data.tar.gz: 8d09e509266737bf1b73c3f6ad5c03255de461c116a6e5ec7b45bfaef56520b7
5
5
  SHA512:
6
- metadata.gz: d15b706ffae42fb3cdd39fd1e9ffc83823102566a6cda208e6196dbca9f637a740043b2a022e3529c26433c095bd99fd569722a73f61b35ad2808a2508ada7c2
7
- data.tar.gz: 140c0502ef9340136a7ff42614353b024a5ac70d6af65a1c9bae30034783dbedc41c8b066f3a7bca1142c3c00f24f632fcae5d85c84318de67afd36dc5ef6370
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
- * [Grafana integration](#grafana-integration)
16
- * [Webservice overview](#webservice-overview)
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 reports to finally create reports in PDF,
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 | not (yet) 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
- ## Grafana integration
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
- ## Webservice overview
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 all grafana datasources
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 saves you as much time as I hope it does, and if you'd like to
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Version information
4
- GRAFANA_REPORTER_VERSION = [0, 4, 2].freeze
4
+ GRAFANA_REPORTER_VERSION = [0, 5, 0].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2021-06-17'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2021-11-05'
@@ -42,12 +42,12 @@ module Grafana
42
42
  @model = model
43
43
  end
44
44
 
45
- # @return [String] category of the datasource, e.g. `tsdb` or `sql`
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. `mysql`
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
- res = res.gsub(/(?:\$\{#{var_name}(?::(?<format>\w+))?\}|\$#{var_name})/) do
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]
@@ -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
@@ -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. 'id' or 'name'
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 specifiy id or name
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
@@ -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
- 'glob'
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 = query_description[:raw_query][:panel].render_url + url_params(query_description)
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
- url = "/api/datasources/proxy/#{id}/query?db=#{@model['database']}&q=#{URI.encode(query_description[:raw_query])}&epoch=ms"
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
- # TODO: support composed queries
33
- raise ComposedQueryNotSupportedError, self
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
@@ -18,7 +18,7 @@ module Grafana
18
18
  def field(field)
19
19
  return @model[field] if @model.key?(field)
20
20
 
21
- ''
21
+ nil
22
22
  end
23
23
 
24
24
  # @return [String] panel ID
@@ -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
- url = "/api/datasources/proxy/#{id}/api/v1/query_range?"\
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
- return { header: headers << json.first['metric']['mode'], content: json.first['values'] } if json.length == 1
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: prepare_sql(sql), datasourceId: id, format: 'table']
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