ruby-grafana-reporter 0.4.5 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/lib/VERSION.rb +2 -2
  4. data/lib/grafana/abstract_datasource.rb +3 -0
  5. data/lib/grafana/dashboard.rb +6 -1
  6. data/lib/grafana/errors.rb +2 -1
  7. data/lib/grafana/grafana.rb +24 -0
  8. data/lib/grafana/grafana_environment_datasource.rb +56 -0
  9. data/lib/grafana/image_rendering_datasource.rb +5 -1
  10. data/lib/grafana/influxdb_datasource.rb +0 -2
  11. data/lib/grafana/sql_datasource.rb +2 -0
  12. data/lib/grafana/variable.rb +46 -23
  13. data/lib/grafana/webrequest.rb +1 -0
  14. data/lib/grafana_reporter/abstract_query.rb +31 -23
  15. data/lib/grafana_reporter/abstract_report.rb +2 -0
  16. data/lib/grafana_reporter/abstract_table_format_strategy.rb +44 -4
  17. data/lib/grafana_reporter/alerts_table_query.rb +2 -1
  18. data/lib/grafana_reporter/annotations_table_query.rb +2 -1
  19. data/lib/grafana_reporter/application/webservice.rb +8 -4
  20. data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +11 -9
  21. data/lib/grafana_reporter/asciidoctor/help.rb +24 -12
  22. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +2 -4
  23. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +2 -4
  24. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +37 -6
  25. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +0 -5
  26. data/lib/grafana_reporter/configuration.rb +27 -0
  27. data/lib/grafana_reporter/console_configuration_wizard.rb +3 -1
  28. data/lib/grafana_reporter/csv_table_format_strategy.rb +11 -9
  29. data/lib/grafana_reporter/demo_report_wizard.rb +3 -6
  30. data/lib/grafana_reporter/panel_image_query.rb +0 -1
  31. data/lib/grafana_reporter/query_value_query.rb +7 -1
  32. data/lib/grafana_reporter/reporter_environment_datasource.rb +24 -0
  33. data/lib/ruby_grafana_reporter.rb +0 -11
  34. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd6bdf1a9856b3c0a663060c4b2ecfc3a094ae230635807398a611d38d74ac90
4
- data.tar.gz: 95ac2750e962a67b0d3b71184b5409b88441840f4b08f31284b33f20b0d862a3
3
+ metadata.gz: '06912db0a0635560dca75c00c01c047deac42741c7c6ba2ee717cbccdf26cb19'
4
+ data.tar.gz: 8d09e509266737bf1b73c3f6ad5c03255de461c116a6e5ec7b45bfaef56520b7
5
5
  SHA512:
6
- metadata.gz: 53698b98b33afef1706243cf5c322bd1a9968ec723e18d32ec5d550769117fa9c64c43c9d174752630ac7f8e8015d1394d4f2060fbe6be1b2ce73d7ad8390801
7
- data.tar.gz: 5f39c12e056e689ff442f00c175219e6a560e9e38362a5aec9f67dc738b172ebf0b9d288958e312020f8304603e54ff1c4e21ca44d77182342a3df9fe03825ba
6
+ metadata.gz: 66e81686e253f8e98101f97b0b08db97f955cd1c274f2ed683f597b675c228adf198cd2c66f9ce3df05506a760564643e43b94c40fa952b13748122ecc8d6890
7
+ data.tar.gz: 916ec07231acf19c20b0f01d7bfe9a47d5825a8e0c3a6c67a9fa615e2b993343d1506fb129cc72b66e2be9b02102c15e338a6e8b5746286494de4164a9678c38
data/README.md CHANGED
@@ -30,7 +30,7 @@ professional reporting functionality. And this is, where the ruby grafana report
30
30
  steps in.
31
31
 
32
32
  The key functionality of the reporter is to capture data and images from grafana
33
- dashboards and to use it in your custom templates to finally create reports in PDF
33
+ dashboards and to use it in your custom templates to finally create reports in PDF
34
34
  (default), HTML, or any other format.
35
35
 
36
36
  By default (an extended version of) Asciidoctor is enabled as template language.
@@ -65,7 +65,7 @@ Database | Image rendering | Raw queries | Composed queries
65
65
  ------------------------- | :-------------: | :-----------: | :------------:
66
66
  all SQL based datasources | supported | supported | supported
67
67
  Graphite | supported | supported | supported
68
- InfluxDB | supported | supported | not (yet) supported
68
+ InfluxDB | supported | supported | supported
69
69
  Prometheus | supported | supported | n/a in grafana
70
70
  other datasources | supported | not supported | not supported
71
71
 
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, 5].freeze
4
+ GRAFANA_REPORTER_VERSION = [0, 5, 0].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2021-08-26'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2021-11-05'
@@ -128,6 +128,9 @@ module Grafana
128
128
  repeat_count += 1
129
129
 
130
130
  variables.each do |name, variable|
131
+ # do not replace with non grafana variables
132
+ next unless name =~ /^var-/
133
+
131
134
  # only set ticks if value is string
132
135
  var_name = name.gsub(/^var-/, '')
133
136
  next unless var_name =~ /^\w+$/
@@ -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
@@ -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
 
@@ -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,
@@ -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
@@ -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
 
@@ -24,8 +24,6 @@ module Grafana
24
24
  query = query.gsub(/\$timeFilter(?=\W|$)/, "time >= #{query_description[:from]}ms and time <= #{query_description[:to]}ms")
25
25
 
26
26
  # replace grafana variables $__interval and $__interval_ms in query
27
- # TODO: influx datasource currently uses a fixed values of 1000 width for interval variables specified in a query - it should be possible to calculate this according to grafana
28
- # TODO: check where calculation and replacement of interval variable should take place
29
27
  query = query.gsub(/\$(?:__)?interval(?=\W|$)/, "#{((query_description[:to].to_i - query_description[:from].to_i) / 1000 / 1000).to_i}s")
30
28
  query = query.gsub(/\$(?:__)?interval_ms(?=\W|$)/, "#{((query_description[:to].to_i - query_description[:from].to_i) / 1000).to_i}")
31
29
 
@@ -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')
@@ -29,15 +29,14 @@ module Grafana
29
29
 
30
30
  # @param config_or_value [Hash, Object] configuration hash of a variable out of an {Dashboard} instance
31
31
  # or a value of any kind.
32
- def initialize(config_or_value)
32
+ # @param dashboard [Dashboard] parent dashboard, if applicable; especially needed for query variable
33
+ # evaluation.
34
+ def initialize(config_or_value, dashboard = nil)
33
35
  if config_or_value.is_a? Hash
36
+ @dashboard = dashboard
34
37
  @config = config_or_value
35
38
  @name = @config['name']
36
- # TODO: if a variable uses type 'query' which is never updated, the selected values are stored in 'options'
37
- unless @config['current'].nil?
38
- @raw_value = @config['current']['value']
39
- @text = @config['current']['text']
40
- end
39
+ init_values
41
40
  else
42
41
  @config = {}
43
42
  @raw_value = config_or_value
@@ -68,14 +67,19 @@ module Grafana
68
67
  if value == '$__all'
69
68
  if !@config['options'].empty?
70
69
  # this query contains predefined values, so capture them and format the values accordingly
71
- # this happens either if type='custom' or a query, which is never updated
72
- value = @config['options'].map { |item| item['value'] }
70
+ # this happens either for type='custom' or type 'query', if it is never updated
71
+ value = @config['options'].reject { |item| item['value'] == '$__all' }.map { |item| item['value'] }
73
72
 
74
73
  elsif @config['type'] == 'query' && !@config['query'].empty?
75
- # TODO: replace variables in query, execute it and evaluate the results as if they were normally selected
76
- # "multiFormat": contains variable replacement in "query", e.g. 'regex values' or 'glob'
77
- # "datasource": contains name of datasource
78
- return @config['query']
74
+ # variables in this configuration are not stored in grafana, i.e. if all is selected here,
75
+ # the values have to be fetched from the datasource
76
+ query = ::GrafanaReporter::QueryValueQuery.new(@dashboard)
77
+ query.datasource = @dashboard.grafana.datasource_by_name(@config['datasource'])
78
+ query.variables['result_type'] = Variable.new('object')
79
+ query.raw_query = @config['query']
80
+ result = query.execute
81
+
82
+ value = result[:content].map { |item| item[0].to_s }
79
83
 
80
84
  else
81
85
  # TODO: add support for variable type: 'datasource' and 'adhoc'
@@ -149,8 +153,10 @@ module Grafana
149
153
  value.gsub(%r{[" |=/\\]}, '\\\\\0')
150
154
 
151
155
  when /^date(?::(?<format>.*))?$/
152
- # TODO: grafana does not seem to allow multiselection of variables with date format - raise an error if this happens anyway
153
- get_date_formatted(value, Regexp.last_match(1))
156
+ if multi? && value.is_a?(Array)
157
+ raise GrafanaError, "Date format cannot be specified for a variable containing an array of values"
158
+ end
159
+ Variable.format_as_date(value, Regexp.last_match(1))
154
160
 
155
161
  when ''
156
162
  # default
@@ -168,8 +174,9 @@ module Grafana
168
174
  end
169
175
  end
170
176
 
171
- # @return [Boolean] true, if the value can contain multiple selections, i.e. can contain an Array
177
+ # @return [Boolean] true, if the value can contain multiple selections, i.e. can contain an Array or does contain all
172
178
  def multi?
179
+ return true if @raw_value == '$__all'
173
180
  return @config['multi'] unless @config['multi'].nil?
174
181
 
175
182
  @raw_value.is_a? Array
@@ -187,12 +194,13 @@ module Grafana
187
194
  @text = new_text
188
195
  end
189
196
 
190
- private
191
-
192
- # Realize time formatting according
197
+ # Applies the date format according
193
198
  # {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to}
194
- # and {https://momentjs.com/docs/#/displaying/}.
195
- def get_date_formatted(value, format)
199
+ # and {https://momentjs.com/docs/#/displaying/} to a given value.
200
+ # @param value [String] time as milliseconds to be formatted
201
+ # @param format [String] format string in which the time value shall be returned
202
+ # @return [String] time converted to the specified time format
203
+ def self.format_as_date(value, format)
196
204
  return (Float(value) / 1000).to_i.to_s if format == 'seconds'
197
205
  return Time.at((Float(value) / 1000).to_i).utc.iso8601(3) if !format || (format == 'iso')
198
206
 
@@ -203,13 +211,12 @@ module Grafana
203
211
  tmp = work_string.scan(/^(?:M{1,4}|D{1,4}|d{1,4}|e|E|w{1,2}|W{1,2}|Y{4}|Y{2}|A|a|H{1,2}|
204
212
  h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
205
213
 
206
- # TODO: add test for sub! and switch to non-modifying frozen string action
207
214
  if tmp.empty?
208
215
  matches << work_string[0]
209
- work_string.sub!(/^#{work_string[0]}/, '')
216
+ work_string = work_string.sub(/^#{work_string[0]}/, '')
210
217
  else
211
218
  matches << tmp[0]
212
- work_string.sub!(/^#{tmp[0]}/, '')
219
+ work_string = work_string.sub(/^#{tmp[0]}/, '')
213
220
  end
214
221
  end
215
222
 
@@ -221,5 +228,21 @@ module Grafana
221
228
 
222
229
  Time.at((Float(value) / 1000).to_i).strftime(format_string)
223
230
  end
231
+
232
+ private
233
+
234
+ def init_values
235
+ case @config['type']
236
+ when 'constant'
237
+ self.raw_value = @config['query']
238
+
239
+ else
240
+ if !@config['current'].nil?
241
+ self.raw_value = @config['current']['value']
242
+ else
243
+ raise GrafanaError.new("Grafana variable with type '#{@config['type']}' and name '#{@config['name']}' could not be handled properly. Please raise a ticket.")
244
+ end
245
+ end
246
+ end
224
247
  end
225
248
  end
@@ -50,6 +50,7 @@ module Grafana
50
50
  @logger.debug("Requesting #{uri} with '#{@options[:body]}' and timeout '#{timeout}'")
51
51
  response = @http.request(request)
52
52
  @logger.debug("Received response #{response}")
53
+ @logger.debug("HTTP response body: #{response.body}") unless response.code =~ /^2.*/
53
54
 
54
55
  response
55
56
  end
@@ -44,6 +44,7 @@ module GrafanaReporter
44
44
  else
45
45
  raise GrafanaReporterError, "Internal error in AbstractQuery: given object is of type #{grafana_obj.class.name}, which is not supported"
46
46
  end
47
+ @logger = @grafana ? @grafana.logger : ::Logger.new($stderr, level: :info)
47
48
  @variables = {}
48
49
  @variables['from'] = Grafana::Variable.new(nil)
49
50
  @variables['to'] = Grafana::Variable.new(nil)
@@ -79,7 +80,7 @@ module GrafanaReporter
79
80
  raise DatasourceNotSupportedError.new(@datasource, self) if @datasource.is_a?(Grafana::UnsupportedDatasource)
80
81
 
81
82
  begin
82
- @result = @datasource.request(from: from, to: to, raw_query: raw_query, variables: grafana_variables,
83
+ @result = @datasource.request(from: from, to: to, raw_query: raw_query, variables: @variables,
83
84
  prepared_request: @grafana.prepare_request, timeout: timeout)
84
85
  rescue ::Grafana::GrafanaError
85
86
  # grafana errors will be directly passed through
@@ -143,6 +144,8 @@ module GrafanaReporter
143
144
  #
144
145
  # Multiple columns may be filtered. Therefore the column titles have to be named in the
145
146
  # {Grafana::Variable#raw_value} and have to be separated by +,+ (comma).
147
+ #
148
+ # Commas can be used in a format string, but need to be escaped by using +_,+.
146
149
  # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
147
150
  # @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result
148
151
  # @return [Hash] filtered query result
@@ -150,8 +153,8 @@ module GrafanaReporter
150
153
  return result unless filter_columns_variable
151
154
 
152
155
  filter_columns = filter_columns_variable.raw_value
153
- filter_columns.split(',').each do |filter_column|
154
- pos = result[:header].index(filter_column)
156
+ filter_columns.split(/(?<!_),/).each do |filter_column|
157
+ pos = result[:header].index(filter_column.gsub("_,", ","))
155
158
 
156
159
  unless pos.nil?
157
160
  result[:header].delete_at(pos)
@@ -167,23 +170,33 @@ module GrafanaReporter
167
170
  # The formatting will be applied separately for every column. Therefore the column formats have to be named
168
171
  # in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for
169
172
  # a column, no change will happen.
173
+ #
174
+ # It is also possible to format milliseconds as dates by specifying date formats, e.g. +date:iso+. It is
175
+ # possible to use any date format according
176
+ # {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#from-and-to}
177
+ #
178
+ # Commas can be used in a format string, but need to be escaped by using +_,+.
170
179
  # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
171
180
  # @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result
172
181
  # @return [Hash] formatted query result
173
182
  def format_columns(result, formats)
174
183
  return result unless formats
175
184
 
176
- formats.text.split(',').each_index do |i|
177
- format = formats.text.split(',')[i]
185
+ formats.text.split(/(?<!_),/).each_index do |i|
186
+ format = formats.text.split(/(?<!_),/)[i].gsub("_,", ",")
178
187
  next if format.empty?
179
188
 
180
189
  result[:content].map do |row|
181
190
  next unless row.length > i
182
191
 
183
192
  begin
184
- row[i] = format % row[i] if row[i]
193
+ if format =~ /^date:/
194
+ row[i] = ::Grafana::Variable.format_as_date(row[i], format.sub(/^date:/, '')) if row[i]
195
+ else
196
+ row[i] = format % row[i] if row[i]
197
+ end
185
198
  rescue StandardError => e
186
- @grafana.logger.error(e.message)
199
+ @logger.error(e.message)
187
200
  row[i] = e.message
188
201
  end
189
202
  end
@@ -248,7 +261,7 @@ module GrafanaReporter
248
261
  begin
249
262
  row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
250
263
  rescue StandardError => e
251
- @grafana.logger.error(e.message)
264
+ @logger.error(e.message)
252
265
  row[i] = e.message
253
266
  end
254
267
 
@@ -273,7 +286,7 @@ module GrafanaReporter
273
286
  end
274
287
  end
275
288
  rescue StandardError => e
276
- @grafana.logger.error(e.message)
289
+ @logger.error(e.message)
277
290
  row[i] = e.message
278
291
  end
279
292
  end
@@ -298,17 +311,19 @@ module GrafanaReporter
298
311
  # @option opts [Grafana::Variable] :column_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated`
299
312
  # @option opts [Grafana::Variable] :include_headline specifies if table should contain headline, defaults to false
300
313
  # @option opts [Grafana::Variable] :table_formatter specifies which formatter shall be used, defaults to 'csv'
314
+ # @option opts [Grafana::Variable] :transposed specifies whether the result table is transposed
301
315
  # @return [String] table in custom output format
302
316
  def format_table_output(result, opts)
303
317
  opts = { include_headline: Grafana::Variable.new('false'),
304
318
  table_formatter: Grafana::Variable.new('csv'),
305
319
  row_divider: Grafana::Variable.new('| '),
306
- column_divider: Grafana::Variable.new(' | ') }.merge(opts.delete_if {|_k, v| v.nil? })
320
+ column_divider: Grafana::Variable.new(' | '),
321
+ transpose: Grafana::Variable.new('false') }.merge(opts.delete_if {|_k, v| v.nil? })
307
322
 
308
323
  if opts[:table_formatter].raw_value == 'adoc_deprecated'
309
- @grafana.logger.warn("You are using deprecated 'table_formatter' named 'adoc_deprecated', which will be "\
310
- "removed in a future version. Start using 'adoc_plain' or register your own "\
311
- "implementation of AbstractTableFormatStrategy.")
324
+ @logger.warn("You are using deprecated 'table_formatter' named 'adoc_deprecated', which will be "\
325
+ "removed in a future version. Start using 'adoc_plain' or register your own "\
326
+ "implementation of AbstractTableFormatStrategy.")
312
327
  return result[:content].map do |row|
313
328
  opts[:row_divider].raw_value + row.map do |item|
314
329
  item.to_s.gsub('|', '\\|')
@@ -316,7 +331,7 @@ module GrafanaReporter
316
331
  end.join("\n")
317
332
  end
318
333
 
319
- AbstractTableFormatStrategy.get(opts[:table_formatter].raw_value).format(result, opts[:include_headline].raw_value.downcase == 'true')
334
+ AbstractTableFormatStrategy.get(opts[:table_formatter].raw_value).format(result, opts[:include_headline].raw_value.downcase == 'true', opts[:transpose].raw_value.downcase == 'true')
320
335
  end
321
336
 
322
337
  # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
@@ -334,7 +349,7 @@ module GrafanaReporter
334
349
  # @param timezone [Grafana::Variable] timezone to use, if not system timezone
335
350
  # @return [String] translated date as timestamp string
336
351
  def translate_date(orig_date, report_time, is_to_time, timezone = nil)
337
- @grafana.logger.warn("#translate_date has been called without 'report_time' - using current time as fallback.") unless report_time
352
+ @logger.warn("#translate_date has been called without 'report_time' - using current time as fallback.") unless report_time
338
353
  report_time ||= ::Grafana::Variable.new(Time.now.to_s)
339
354
  orig_date = orig_date.raw_value if orig_date.is_a?(Grafana::Variable)
340
355
 
@@ -405,8 +420,7 @@ module GrafanaReporter
405
420
  def datasource_response_valid?
406
421
  return false if @result.nil?
407
422
  return false unless @result.is_a?(Hash)
408
- # TODO: compare how empty valid responses look like in grafana
409
- return true if @result.empty?
423
+ return false if @result.empty?
410
424
  return false unless @result.key?(:header)
411
425
  return false unless @result.key?(:content)
412
426
  return false unless @result[:header].is_a?(Array)
@@ -415,12 +429,6 @@ module GrafanaReporter
415
429
  true
416
430
  end
417
431
 
418
- # @return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name
419
- # is prefixed with +var-+
420
- def grafana_variables
421
- @variables.select { |k, _v| k =~ /^var-.+/ }
422
- end
423
-
424
432
  def delta_date(date, delta_count, time_letter)
425
433
  # substract specified time
426
434
  case time_letter
@@ -143,6 +143,8 @@ module GrafanaReporter
143
143
  notify(:on_before_create)
144
144
  @start_time = Time.new
145
145
  logger.info("Report started at #{@start_time}")
146
+ logger.info("You are running ruby-grafana-reporter version #{GRAFANA_REPORTER_VERSION.join('.')}.")
147
+ logger.info("A newer version is released. Check out https://github.com/divinity666/ruby-grafana-reporter/releases/latest") unless @config.latest_version_check_ok?
146
148
  build
147
149
  rescue MissingTemplateError => e
148
150
  @logger.error(e.message)
@@ -23,12 +23,52 @@ module GrafanaReporter
23
23
  raise NotImplementedError
24
24
  end
25
25
 
26
- # @abstract
27
- # @param column [Array] datasource table result
26
+ # Used to format a given content array to the desired output format. The default
27
+ # implementation applies the {#format_rules} to create a custom string export. If
28
+ # this is not sufficient for a desired table format, you may simply overwrite this
29
+ # function to have full freedom about the desired output.
30
+ # @param content [Hash] datasource table result
28
31
  # @param include_headline [Boolean] true, if headline should be included in result
32
+ # @param transposed [Boolean] true, if result array is in transposed format
29
33
  # @return [String] formatted in table format
30
- def format(content, include_headline)
31
- raise NotImplementedError
34
+ def format(content, include_headline, transposed)
35
+ result = content[:content]
36
+
37
+ # add the headline at the correct position to the content array
38
+ if include_headline
39
+ if transposed
40
+ result.each_index do |i|
41
+ result[i] = [content[:header][i]] + result[i]
42
+ end
43
+ else
44
+ result = result.unshift(content[:header])
45
+ end
46
+ end
47
+
48
+ # translate the content to a table
49
+ result.map do |row|
50
+ format_rules[:row_start] + row.map do |item|
51
+ value = item.to_s
52
+ if format_rules[:replace_string_or_regex]
53
+ value = value.gsub(format_rules[:replace_string_or_regex], format_rules[:replacement])
54
+ end
55
+
56
+ format_rules[:cell_start] + value + format_rules[:cell_end]
57
+ end.join(format_rules[:between_cells])
58
+ end.join(format_rules[:row_end])
59
+ end
60
+
61
+ # Formatting rules, which are applied to build the table output format.
62
+ def format_rules
63
+ {
64
+ row_start: '',
65
+ row_end: '',
66
+ cell_start: '',
67
+ between_cells: '',
68
+ cell_end: '',
69
+ replace_string_or_regex: nil,
70
+ replacement: ''
71
+ }
32
72
  end
33
73
  end
34
74
  end
@@ -37,7 +37,8 @@ module GrafanaReporter
37
37
  row_divider: @variables['row_divider'],
38
38
  column_divider: @variables['column_divider'],
39
39
  table_formatter: @variables['table_formatter'],
40
- include_headline: @variables['include_headline'])
40
+ include_headline: @variables['include_headline'],
41
+ transpose: @variables['transpose'])
41
42
  end
42
43
  end
43
44
  end
@@ -36,7 +36,8 @@ module GrafanaReporter
36
36
  row_divider: @variables['row_divider'],
37
37
  column_divider: @variables['column_divider'],
38
38
  table_formatter: @variables['table_formatter'],
39
- include_headline: @variables['include_headline'])
39
+ include_headline: @variables['include_headline'],
40
+ transpose: @variables['transpose'])
40
41
  end
41
42
  end
42
43
  end
@@ -26,7 +26,13 @@ module GrafanaReporter
26
26
  @progress_reporter = Thread.new {}
27
27
 
28
28
  @status = :running
29
- accept_requests_loop
29
+ begin
30
+ accept_requests_loop
31
+ rescue SystemExit, Interrupt
32
+ @logger.info("Server shutting down.")
33
+ stop!
34
+ retry
35
+ end
30
36
  @status = :stopped
31
37
  end
32
38
 
@@ -57,8 +63,6 @@ module GrafanaReporter
57
63
  # step 1) accept incoming connection
58
64
  socket = @server.accept
59
65
 
60
- # TODO: shutdown properly on SIGINT/SIGHUB
61
-
62
66
  # stop webservice properly, if shall be shutdown
63
67
  if @status == :stopping
64
68
  socket.close
@@ -252,7 +256,7 @@ module GrafanaReporter
252
256
  <% end.join('') %>
253
257
  <tbody>
254
258
  </table>
255
- <p style="font-size: small; color:grey">You are running ruby-grafana-reporter version <%= GRAFANA_REPORTER_VERSION.join('.') %>.</p>
259
+ <p style="font-size: small; color:grey">You are running ruby-grafana-reporter version <%= GRAFANA_REPORTER_VERSION.join('.') %>.<%= @config.latest_version_check_ok? ? '' : ' Check out the latest version <a href="https://github.com/divinity666/ruby-grafana-reporter/releases/latest">here</a>.' %></p>
256
260
  </body>
257
261
  </html>
258
262
  HTML_TEMPLATE
@@ -10,15 +10,17 @@ module GrafanaReporter
10
10
  'adoc_plain'
11
11
  end
12
12
 
13
- # @see AbstractTableFormatStrategy#format
14
- def format(result, include_headline)
15
- headline = '| ' + result[:header].map { |item| item.to_s.gsub(' | ', '\\|') }.join(' | ')
16
-
17
- content = result[:content].map do |row|
18
- '| ' + row.map { |item| item.to_s.gsub(' | ', '\\|') }.join(' | ')
19
- end.join("\n")
20
-
21
- "#{"#{headline}\n" if include_headline}#{content}"
13
+ # @see AbstractTableFormatStrategy#format_rules
14
+ def format_rules
15
+ {
16
+ row_start: '| ',
17
+ row_end: "\n",
18
+ cell_start: '',
19
+ between_cells: ' | ',
20
+ cell_end: '',
21
+ replace_string_or_regex: '|',
22
+ replacement: '\\|'
23
+ }
22
24
  end
23
25
  end
24
26
  end
@@ -82,7 +82,7 @@ Usage: #{opts[:code_begin]}#{v[:call]}#{opts[:code_end]}
82
82
  #{v[:description]}#{"\n\nSee also: #{v[:see]}" if v[:see]}#{unless v[:options].empty?
83
83
  %(
84
84
  #{opts[:table_begin]}| Option | Description#{"\n#{opts[:head_postfix_col] * 2}" if opts[:head_postfix_col]}
85
- #{v[:options].sort.map { |_opt_k, opt_v| "| #{opts[:code_begin]}#{opt_v[:call]}#{opts[:code_end]} | #{opt_v[:description].gsub('|', '\|')}" }.join("\n") }#{opts[:table_end]})
85
+ #{v[:options].sort.map { |_opt_k, opt_v| "| #{opts[:code_begin]}#{opt_v[:call]}#{opts[:code_end]} | #{opt_v[:description].gsub('|', '\|')}#{"\nSee also: #{opt_v[:see]}" if opt_v[:see]}" }.join("\n") }#{opts[:table_end]})
86
86
  end}
87
87
  )
88
88
  end
@@ -109,12 +109,12 @@ end}
109
109
  res_item[:description] = item['description']
110
110
  res_item[:see] = item['see']
111
111
 
112
- opts = ((item['options'] ? item['options'].keys : [])
112
+ opts = ((item['options'] ? item['options'].keys : []) +
113
113
  (item['standard_options'] ? item['standard_options'].keys : [])).sort
114
114
  opts.each do |opt_key|
115
115
  res_item[:options][opt_key] = {}
116
116
 
117
- if item['standard_options'].key?(opt_key)
117
+ if std_opts.key?(opt_key)
118
118
  res_item[:options][opt_key][:call] = std_opts[opt_key]['call']
119
119
  res_item[:options][opt_key][:description] = "#{std_opts[opt_key]['description']} "\
120
120
  "#{item['standard_options'][opt_key]}".chop
@@ -196,8 +196,12 @@ end}
196
196
  format:
197
197
  call: format="<format_col1>,<format_col2>,..."
198
198
  description: >-
199
- Specify format in which the results shall be returned, e.g. `%.2f` for only two digit decimals of a
200
- float. Several columns are separated by `,`. Execution is applied in the following order `format`,
199
+ Specify format in which the results in a specific column shall be returned, e.g. `%.2f` for only
200
+ two digit decimals of a float. Several column formats are separated by `,`, i.e. `%.2f,%.3f` would
201
+ apply `%.2f` to the first column and `%.3f` to the second column. All other columns would not be
202
+ formatted. You may also format time in milliseconds to a time format by specifying e.g. `date:iso`.
203
+ Commas in format strings are supported, but have to be escaped by useing `_,`.
204
+ Execution of related functions is applied in the following order `format`,
201
205
  `replace_values`, `filter_columns`, `transpose`.
202
206
  see: 'https://ruby-doc.org/core/Kernel.html#method-i-sprintf'
203
207
 
@@ -207,21 +211,24 @@ end}
207
211
  Specify result values which shall be replaced, e.g. `2:OK` will replace query values `2` with value `OK`.
208
212
  Replacing several values is possible by separating by `,`. Matches with regular expressions are also
209
213
  supported, but must be full matches, i.e. have to start with `^` and end with `$`, e.g. `^[012]$:OK`.
210
- Number replacements can also be performed, e.g. `<8.2` or `<>3`. Execution is applied in the following order `format`,
214
+ Number replacements can also be performed, e.g. `<8.2` or `<>3`. Execution of related functions is
215
+ applied in the following order `format`,
211
216
  `replace_values`, `filter_columns`, `transpose`.
212
217
  see: https://ruby-doc.org/core/Regexp.html#class-Regexp-label-Character+Classes
213
218
 
214
219
  filter_columns:
215
220
  call: filter_columns="<column_name_1>,<column_name_2>,..."
216
221
  description: >-
217
- Removes specified columns from result. Execution is applied in the following order `format`, `replace_values`,
218
- `filter_columns`, `transpose`.
222
+ Removes specified columns from result. Commas in format strings are supported, but have to be
223
+ escaped by useing `_,`. Execution of related functions is applied in the following order
224
+ `format`, `replace_values`, `filter_columns`, `transpose`.
219
225
 
220
226
  transpose:
221
227
  call: transpose="true"
222
228
  description: >-
223
- Transposes the query result, i.e. columns become rows and rows become columnns. Execution is applied in the
224
- following order `format`, `replace_values`, `filter_columns`, `transpose`.
229
+ Transposes the query result, i.e. columns become rows and rows become columnns. Execution of related
230
+ functions is applied in the following order `format`, `replace_values`, `filter_columns`,
231
+ `transpose`.
225
232
 
226
233
  column_divider:
227
234
  call: column_divider="<divider>"
@@ -234,7 +241,7 @@ end}
234
241
  call: row_divider="<divider>"
235
242
  description: >-
236
243
  Replace the default row divider with another one, when used in conjunction with `table_formatter` set to
237
- `adoc_deprecated`. . Defaults to `| ` for being interpreted as a asciidoctor row. DEPRECATED: switch to
244
+ `adoc_deprecated`. Defaults to `| ` for being interpreted as a asciidoctor row. DEPRECATED: switch to
238
245
  `table_formatter` named `adoc_plain`, or implement a custom table formatter.
239
246
 
240
247
  table_formatter:
@@ -258,8 +265,13 @@ end}
258
265
  call: 'include::grafana_help[]'
259
266
 
260
267
  grafana_environment:
261
- description: Shows all available variables in the rendering context which can be used in the asciidoctor template.
268
+ description: >-
269
+ Shows all available variables in the rendering context which can be used in the asciidoctor template.
270
+ If optional `instance` is specified, additional information about the configured grafana instance will be provided.
271
+ This is especially helpful for debugging.
262
272
  call: 'include::grafana_environment[]'
273
+ standard_options:
274
+ instance:
263
275
 
264
276
  grafana_alerts:
265
277
  description: >-
@@ -7,8 +7,8 @@ module GrafanaReporter
7
7
  # Implements the hook
8
8
  # grafana_panel_image::<panel_id>[<options>]
9
9
  #
10
- # Stores the queried panel as a temporary image file and returns an asciidoctor link
11
- # to be included in the report.
10
+ # Stores the queried panel as a temporary image file and returns a relative asciidoctor link
11
+ # to the storage location, which can then be included in the report.
12
12
  #
13
13
  # == Used document parameters
14
14
  # +grafana_default_instance+ - name of grafana instance, 'default' if not specified
@@ -20,8 +20,6 @@ module GrafanaReporter
20
20
  # +to+ - 'to' time for the sql query
21
21
  #
22
22
  # == Supported options
23
- # +field+ - property to query for, e.g. +description+ or +title+ (*mandatory*)
24
- #
25
23
  # +instance+ - name of grafana instance, 'default' if not specified
26
24
  #
27
25
  # +dashboard+ - uid of grafana dashboard to use
@@ -7,8 +7,8 @@ module GrafanaReporter
7
7
  # Implements the hook
8
8
  # grafana_panel_image:<panel_id>[<options>]
9
9
  #
10
- # Stores the queried panel as a temporary image file and returns an asciidoctor link
11
- # to be included in the report.
10
+ # Stores the queried panel as a temporary image file and returns a relative asciidoctor link
11
+ # to the storage location, which can then be included in the report.
12
12
  #
13
13
  # == Used document parameters
14
14
  # +grafana_default_instance+ - name of grafana instance, 'default' if not specified
@@ -20,8 +20,6 @@ module GrafanaReporter
20
20
  # +to+ - 'to' time for the sql query
21
21
  #
22
22
  # == Supported options
23
- # +field+ - property to query for, e.g. +description+ or +title+ (*mandatory*)
24
- #
25
23
  # +instance+ - name of grafana instance, 'default' if not specified
26
24
  #
27
25
  # +dashboard+ - uid of grafana dashboard to use
@@ -13,6 +13,9 @@ module GrafanaReporter
13
13
  #
14
14
  # == Used document parameters
15
15
  # All, to be listed as the available environment.
16
+ #
17
+ # == Supported options
18
+ # +instance+ - grafana instance name, if extended information about the grafana instance shall be printed
16
19
  class ShowEnvironmentIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor
17
20
  include ProcessorMixin
18
21
 
@@ -22,19 +25,47 @@ module GrafanaReporter
22
25
  end
23
26
 
24
27
  # :nodoc:
25
- def process(doc, reader, _target, _attrs)
28
+ def process(doc, reader, _target, attrs)
26
29
  # return if @report.cancel
27
30
  @report.next_step
31
+ instance = attrs['instance'] || doc.attr('grafana_default_instance') || 'default'
32
+ attrs['result_type'] = 'sql_table'
28
33
  @report.logger.debug('Processing ShowEnvironmentIncludeProcessor')
34
+ grafana = @report.grafana(instance)
35
+
36
+ vars = { 'table_formatter' => 'adoc_plain', 'include_headline' => 'true'}
37
+ vars = vars.merge(build_attribute_hash(doc.attributes, attrs))
38
+
39
+ # query reporter environment
40
+ result = ['== Reporter', '|===']
41
+ query = QueryValueQuery.new(grafana, variables: vars.merge({'transpose' => 'true'}))
42
+ query.datasource = ::GrafanaReporter::ReporterEnvironmentDatasource.new(nil)
43
+ result += query.execute.split("\n")
44
+
45
+ # query grafana environment
46
+ result += ['|===', '',
47
+ '== Grafana Instance', '|===']
48
+ query = QueryValueQuery.new(grafana, variables: vars.merge({'transpose' => 'true'}))
49
+ query.raw_query = {grafana: grafana, mode: 'general'}
50
+ query.datasource = ::Grafana::GrafanaEnvironmentDatasource.new(nil)
51
+ result += query.execute.split("\n")
52
+
53
+ result += ['|===', '',
54
+ '== Accessible Dashboards', '|===']
55
+ query = QueryValueQuery.new(grafana, variables: vars)
56
+ query.raw_query = {grafana: grafana, mode: 'dashboards'}
57
+ query.datasource = Grafana::GrafanaEnvironmentDatasource.new(nil)
58
+ result += query.execute.split("\n")
29
59
 
30
- vars = ['== Accessible Variables',
31
- '|===']
60
+ result += ['|===', '',
61
+ '== Accessible Variables',
62
+ '|===']
32
63
  doc.attributes.sort.each do |k, v|
33
- vars << "| `+{#{k}}+` | #{v}"
64
+ result << "| `+{#{k}}+` | #{v}"
34
65
  end
35
- vars << '|==='
66
+ result << '|==='
36
67
 
37
- reader.unshift_lines vars
68
+ reader.unshift_lines result
38
69
  end
39
70
 
40
71
  # @see ProcessorMixin#build_demo_entry
@@ -58,11 +58,6 @@ module GrafanaReporter
58
58
  return reader
59
59
  end
60
60
 
61
- # TODO: remove dirty hack to allow the document as parameter for other processors
62
- def doc.document
63
- self
64
- end
65
-
66
61
  # TODO: properly show error messages also in document
67
62
  ext = doc.extensions.find_inline_macro_extension(call) if doc.extensions.inline_macros?
68
63
  if !ext
@@ -152,6 +152,32 @@ module GrafanaReporter
152
152
  get_config('default-document-attributes') || {}
153
153
  end
154
154
 
155
+ # Checks if this is the latest ruby-grafana-reporter version. If and how often the check if
156
+ # performed, depends on the configuration setting `check-for-updates`. By default this is
157
+ # 0 (=disabled). If a number >0 is specified, the checks are performed once every n-days on
158
+ # report creation or call of overview webpage.
159
+ # @return [Boolean] true, if is ok, false if a newer version exists
160
+ def latest_version_check_ok?
161
+ return false if @newer_version_exists
162
+
163
+ value = get_config('grafana-reporter:check-for-updates') || 0
164
+ return true if value <= 0
165
+
166
+ # repeat check only every n-th day
167
+ if @last_version_check
168
+ return true if (Time.now - @last_version_check) < (value * 24*60*60)
169
+ end
170
+
171
+ # check for newer version
172
+ @last_version_check = Time.now
173
+ url = 'https://github.com/divinity666/ruby-grafana-reporter/releases/latest'
174
+ response = Grafana::WebRequest.new(url).execute
175
+ return true if response['location'] =~ /.*[\/v]#{GRAFANA_REPORTER_VERSION.join('.')}$/
176
+
177
+ @newer_version_exists = true
178
+ return false
179
+ end
180
+
155
181
  # This function shall be called, before the configuration object is used in the
156
182
  # {Application::Application#run}. It ensures, that everything is setup properly
157
183
  # and all necessary folders exist. Appropriate errors are raised in case of errors.
@@ -304,6 +330,7 @@ module GrafanaReporter
304
330
  [
305
331
  Hash, 1,
306
332
  {
333
+ 'check-for-updates' => [Integer, 0],
307
334
  'debug-level' => [String, 0],
308
335
  'run-mode' => [String, 0],
309
336
  'test-instance' => [String, 0],
@@ -6,7 +6,6 @@ module GrafanaReporter
6
6
  class ConsoleConfigurationWizard
7
7
  # Provides a command line configuration wizard for setting up the necessary configuration
8
8
  # file.
9
- # TODO: refactor class
10
9
  def start_wizard(config_file, console_config)
11
10
  action = overwrite_or_use_config_file(config_file)
12
11
  return if action == 'abort'
@@ -73,6 +72,9 @@ module GrafanaReporter
73
72
  #{grafana}
74
73
 
75
74
  grafana-reporter:
75
+ # Specifies how often the reporter shall check for newer versions [number of days].
76
+ # You may set check-for-updates to 0 to disable
77
+ check-for-updates: 1
76
78
  report-class: GrafanaReporter::Asciidoctor::Report
77
79
  templates-folder: #{templates}
78
80
  reports-folder: #{reports}
@@ -9,15 +9,17 @@ module GrafanaReporter
9
9
  'csv'
10
10
  end
11
11
 
12
- # @see AbstractTableFormatStrategy#format
13
- def format(result, include_headline)
14
- headline = result[:header].map { |item| item.to_s.gsub(',', '\\,') }.join(',')
15
-
16
- content = result[:content].map do |row|
17
- row.map { |item| item.to_s.gsub(',', '\,') }.join(',')
18
- end.join("\n")
19
-
20
- "#{"#{headline}\n" if include_headline}#{content}"
12
+ # @see AbstractTableFormatStrategy#format_rules
13
+ def format_rules
14
+ {
15
+ row_start: '',
16
+ row_end: "\n",
17
+ cell_start: '',
18
+ between_cells: ', ',
19
+ cell_end: '',
20
+ replace_string_or_regex: ',',
21
+ replacement: '\\,'
22
+ }
21
23
  end
22
24
  end
23
25
  end
@@ -55,12 +55,6 @@ module GrafanaReporter
55
55
  results = {}
56
56
 
57
57
  dashboard.panels.shuffle.each do |panel|
58
- begin
59
- next if panel.datasource.is_a?(Grafana::UnsupportedDatasource)
60
- rescue Grafana::DatasourceDoesNotExistError
61
- next
62
- end
63
-
64
58
  query_classes.each do |query_class|
65
59
  unless query_class.public_instance_methods.include?(:build_demo_entry)
66
60
  results[query_class] = "Method 'build_demo_entry' not implemented for #{query_class.name}"
@@ -77,6 +71,9 @@ module GrafanaReporter
77
71
  # currently not allowed
78
72
  rescue StandardError => e
79
73
  puts "#{e.message}\n#{e.backtrace.join("\n")}"
74
+ rescue NotImplementedError
75
+ # Ignore these errors, as it only means, that a class does not implement
76
+ # the demo entry
80
77
  end
81
78
  end
82
79
  end
@@ -15,7 +15,6 @@ module GrafanaReporter
15
15
  # Returns the body of the http query, which contains the raw image.
16
16
  def post_process
17
17
  @result = @result[:content].first
18
- raise ::Grafana::ImageCouldNotBeRenderedError, @panel if @result.include?('<html')
19
18
  end
20
19
 
21
20
  # @see AbstractQuery#raw_query
@@ -19,8 +19,14 @@ module GrafanaReporter
19
19
  modify_results
20
20
 
21
21
  case @variables['result_type'].raw_value
22
+ when 'object'
23
+
22
24
  when /(?:panel_table|sql_table)/
23
- @result = format_table_output(@result, row_divider: @variables['row_divider'], column_divider: @variables['column_divider'], table_formatter: @variables['table_formatter'], include_headline: @variables['include_headline'])
25
+ @result = format_table_output(@result, row_divider: @variables['row_divider'],
26
+ column_divider: @variables['column_divider'],
27
+ table_formatter: @variables['table_formatter'],
28
+ include_headline: @variables['include_headline'],
29
+ transpose: @variables['transpose'])
24
30
 
25
31
  when /(?:panel_value|sql_value)/
26
32
  tmp = @result[:content] || []
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ # Implements a datasource to return environment related information about the reporter in a tabular format.
5
+ class ReporterEnvironmentDatasource < ::Grafana::AbstractDatasource
6
+ # @see AbstractDatasource#request
7
+ def request(query_description)
8
+ {
9
+ header: ['Version', 'Release Date'],
10
+ content: [[GRAFANA_REPORTER_VERSION.join('.'), GRAFANA_REPORTER_RELEASE_DATE]]
11
+ }
12
+ end
13
+
14
+ # @see AbstractDatasource#default_variable_format
15
+ def default_variable_format
16
+ nil
17
+ end
18
+
19
+ # @see AbstractDatasource#name
20
+ def name
21
+ self.class.to_s
22
+ end
23
+ end
24
+ end
@@ -18,17 +18,6 @@ require 'asciidoctor-pdf'
18
18
  require 'zip'
19
19
  require_relative 'VERSION'
20
20
 
21
- # TODO: add test to see if datasource default formats are applied
22
- # TODO: add test for render-height and render-width, as they are not forwarded
23
- # TODO: show convert-backend, template language and all variables in webservice overview
24
- # TODO: add automated test against grafana playground before building a new release
25
- # TODO: allow registration of files to be defined in config file
26
- # TODO: PRIO: check in cloud - variables fetched from queries are replaced with SQL query instead of the resolved values, which can lead to issues, e.g. when using that in a function as SUBSTRING
27
- # TODO: PRIO: allow multiple report classes in configuration file including possibility to decide for the individual class for each rendering call
28
- # TODO: add configuration example to README
29
- # TODO: find a way, how to automatically update test grafana responses with _real_ grafana responses
30
- # TODO: append necessary variables on demo report creation for plain SQL queries, as they are lacking the grafana reference
31
-
32
21
  folders = [
33
22
  %w[grafana],
34
23
  %w[grafana_reporter logger],
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-grafana-reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christian Kohlmeyer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-08-26 00:00:00.000000000 Z
11
+ date: 2021-11-05 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -118,6 +118,7 @@ files:
118
118
  - "./lib/grafana/grafana.rb"
119
119
  - "./lib/grafana/grafana_alerts_datasource.rb"
120
120
  - "./lib/grafana/grafana_annotations_datasource.rb"
121
+ - "./lib/grafana/grafana_environment_datasource.rb"
121
122
  - "./lib/grafana/grafana_property_datasource.rb"
122
123
  - "./lib/grafana/graphite_datasource.rb"
123
124
  - "./lib/grafana/image_rendering_datasource.rb"
@@ -165,6 +166,7 @@ files:
165
166
  - "./lib/grafana_reporter/panel_property_query.rb"
166
167
  - "./lib/grafana_reporter/query_value_query.rb"
167
168
  - "./lib/grafana_reporter/report_webhook.rb"
169
+ - "./lib/grafana_reporter/reporter_environment_datasource.rb"
168
170
  - "./lib/ruby_grafana_extension.rb"
169
171
  - "./lib/ruby_grafana_reporter.rb"
170
172
  - LICENSE