ruby-grafana-reporter 0.4.1 → 0.4.5

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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +336 -185
  3. data/lib/VERSION.rb +2 -2
  4. data/lib/grafana/abstract_datasource.rb +30 -17
  5. data/lib/grafana/errors.rb +4 -4
  6. data/lib/grafana/grafana.rb +2 -0
  7. data/lib/grafana/grafana_property_datasource.rb +12 -0
  8. data/lib/grafana/graphite_datasource.rb +27 -5
  9. data/lib/grafana/influxdb_datasource.rb +156 -0
  10. data/lib/grafana/panel.rb +1 -1
  11. data/lib/grafana/prometheus_datasource.rb +37 -6
  12. data/lib/grafana/sql_datasource.rb +10 -11
  13. data/lib/grafana/variable.rb +29 -22
  14. data/lib/grafana_reporter/abstract_query.rb +150 -31
  15. data/lib/grafana_reporter/abstract_report.rb +37 -5
  16. data/lib/grafana_reporter/abstract_table_format_strategy.rb +34 -0
  17. data/lib/grafana_reporter/alerts_table_query.rb +5 -6
  18. data/lib/grafana_reporter/annotations_table_query.rb +5 -6
  19. data/lib/grafana_reporter/application/application.rb +7 -2
  20. data/lib/grafana_reporter/application/webservice.rb +35 -30
  21. data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +25 -0
  22. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +7 -5
  23. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +7 -5
  24. data/lib/grafana_reporter/asciidoctor/help.rb +458 -0
  25. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +5 -4
  26. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +5 -4
  27. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +5 -4
  28. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +6 -6
  29. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +4 -4
  30. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +21 -35
  31. data/lib/grafana_reporter/asciidoctor/report.rb +16 -26
  32. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +1 -1
  33. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +6 -4
  34. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +5 -3
  35. data/lib/grafana_reporter/console_configuration_wizard.rb +2 -2
  36. data/lib/grafana_reporter/csv_table_format_strategy.rb +23 -0
  37. data/lib/grafana_reporter/demo_report_wizard.rb +5 -2
  38. data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
  39. data/lib/grafana_reporter/erb/report.rb +14 -21
  40. data/lib/grafana_reporter/erb/report_jail.rb +21 -0
  41. data/lib/grafana_reporter/errors.rb +19 -3
  42. data/lib/grafana_reporter/panel_image_query.rb +2 -5
  43. data/lib/grafana_reporter/query_value_query.rb +1 -19
  44. data/lib/grafana_reporter/report_webhook.rb +12 -8
  45. data/lib/ruby_grafana_reporter.rb +10 -11
  46. metadata +9 -3
  47. data/lib/grafana_reporter/help.rb +0 -443
@@ -33,6 +33,7 @@ module Grafana
33
33
  if config_or_value.is_a? Hash
34
34
  @config = config_or_value
35
35
  @name = @config['name']
36
+ # TODO: if a variable uses type 'query' which is never updated, the selected values are stored in 'options'
36
37
  unless @config['current'].nil?
37
38
  @raw_value = @config['current']['value']
38
39
  @text = @config['current']['text']
@@ -62,93 +63,98 @@ module Grafana
62
63
  def value_formatted(format = '')
63
64
  value = @raw_value
64
65
 
65
- # handle value 'All' properly
66
- # TODO: fix check for selection of All properly
67
- if (value == 'All') || (@text == 'All')
66
+ # if 'All' is selected for this template variable, capture all values properly
67
+ # (from grafana config or query) and format the results afterwards accordingly
68
+ if value == '$__all'
68
69
  if !@config['options'].empty?
70
+ # 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
69
72
  value = @config['options'].map { |item| item['value'] }
70
- elsif !@config['query'].empty?
71
- # TODO: replace variables in this query, too
73
+
74
+ 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
72
78
  return @config['query']
73
- # TODO: handle 'All' value properly for query attributes
79
+
74
80
  else
75
- # TODO: how to handle All selection properly at this point?
81
+ # TODO: add support for variable type: 'datasource' and 'adhoc'
76
82
  end
77
83
  end
78
84
 
79
85
  case format
80
86
  when 'csv'
81
- return value.join(',').to_s if multi?
87
+ return value.join(',').to_s if multi? && value.is_a?(Array)
82
88
 
83
89
  value.to_s
84
90
 
85
91
  when 'distributed'
86
- return value.join(",#{name}=") if multi?
92
+ return value.join(",#{name}=") if multi? && value.is_a?(Array)
87
93
 
88
94
  value
89
95
  when 'doublequote'
90
- if multi?
96
+ if multi? && value.is_a?(Array)
91
97
  value = value.map { |item| "\"#{item.gsub(/\\/, '\\\\').gsub(/"/, '\\"')}\"" }
92
98
  return value.join(',')
93
99
  end
94
100
  "\"#{value.gsub(/"/, '\\"')}\""
95
101
 
96
102
  when 'json'
97
- if multi?
103
+ if multi? && value.is_a?(Array)
98
104
  value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\\0')}\"" }
99
105
  return "[#{value.join(',')}]"
100
106
  end
101
107
  "\"#{value.gsub(/"/, '\\"')}\""
102
108
 
103
109
  when 'percentencode'
104
- value = "{#{value.join(',')}}" if multi?
110
+ value = "{#{value.join(',')}}" if multi? && value.is_a?(Array)
105
111
  ERB::Util.url_encode(value)
106
112
 
107
113
  when 'pipe'
108
- return value.join('|') if multi?
114
+ return value.join('|') if multi? && value.is_a?(Array)
109
115
 
110
116
  value
111
117
 
112
118
  when 'raw'
113
- return "{#{value.join(',')}}" if multi?
119
+ return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
114
120
 
115
121
  value
116
122
 
117
123
  when 'regex'
118
- if multi?
124
+ if multi? && value.is_a?(Array)
119
125
  value = value.map { |item| item.gsub(%r{[/$.|\\]}, '\\\\\0') }
120
126
  return "(#{value.join('|')})"
121
127
  end
122
128
  value.gsub(%r{[/$.|\\]}, '\\\\\0')
123
129
 
124
130
  when 'singlequote'
125
- if multi?
131
+ if multi? && value.is_a?(Array)
126
132
  value = value.map { |item| "'#{item.gsub(/'/, '\\\\\0')}'" }
127
133
  return value.join(',')
128
134
  end
129
135
  "'#{value.gsub(/'/, '\\\\\0')}'"
130
136
 
131
137
  when 'sqlstring'
132
- if multi?
138
+ if multi? && value.is_a?(Array)
133
139
  value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
134
140
  return value.join(',')
135
141
  end
136
142
  "'#{value.gsub(/'/, "''")}'"
137
143
 
138
144
  when 'lucene'
139
- if multi?
145
+ if multi? && value.is_a?(Array)
140
146
  value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\\0')}\"" }
141
147
  return "(#{value.join(' OR ')})"
142
148
  end
143
149
  value.gsub(%r{[" |=/\\]}, '\\\\\0')
144
150
 
145
151
  when /^date(?::(?<format>.*))?$/
146
- # TODO: validate how grafana handles multivariables with date format
152
+ # TODO: grafana does not seem to allow multiselection of variables with date format - raise an error if this happens anyway
147
153
  get_date_formatted(value, Regexp.last_match(1))
148
154
 
149
155
  when ''
150
156
  # default
151
- if multi?
157
+ if multi? && value.is_a?(Array)
152
158
  value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
153
159
  return value.join(',')
154
160
  end
@@ -156,14 +162,13 @@ module Grafana
156
162
 
157
163
  else
158
164
  # glob and all unknown
159
- # TODO add check for array value properly for all cases
160
165
  return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
161
166
 
162
167
  value
163
168
  end
164
169
  end
165
170
 
166
- # @return [Boolean] true, if the value can contain multiple selections, i.e. is an Array
171
+ # @return [Boolean] true, if the value can contain multiple selections, i.e. can contain an Array
167
172
  def multi?
168
173
  return @config['multi'] unless @config['multi'].nil?
169
174
 
@@ -197,6 +202,8 @@ module Grafana
197
202
  until work_string.empty?
198
203
  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}|
199
204
  h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
205
+
206
+ # TODO: add test for sub! and switch to non-modifying frozen string action
200
207
  if tmp.empty?
201
208
  matches << work_string[0]
202
209
  work_string.sub!(/^#{work_string[0]}/, '')
@@ -1,23 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'abstract_table_format_strategy'
4
+ require_relative 'csv_table_format_strategy'
5
+
3
6
  module GrafanaReporter
4
7
  # @abstract Override {#pre_process} and {#post_process} in subclass.
5
8
  #
6
9
  # Superclass containing everything for all queries towards grafana.
7
10
  class AbstractQuery
8
- attr_accessor :datasource, :timeout, :from, :to
11
+ attr_accessor :datasource
9
12
  attr_writer :raw_query
10
- attr_reader :variables, :result, :panel
13
+ attr_reader :variables, :result, :panel, :dashboard
14
+
15
+ def timeout
16
+ # TODO: PRIO check where value priorities should be evaluated
17
+ return @variables['timeout'].raw_value if @variables['timeout']
18
+ return @variables['grafana_default_timeout'].raw_value if @variables['grafana_default_timeout']
19
+
20
+ nil
21
+ end
22
+
23
+ # @param grafana_obj [Object] {Grafana::Grafana}, {Grafana::Dashboard} or {Grafana::Panel} object for which the query is executed
24
+ # @param opts [Hash] hash options, which may consist of:
25
+ # @option opts [Hash] :variables hash of variables, which shall be used to replace variable references in the query
26
+ # @option opts [Boolean] :ignore_dashboard_defaults True if {#assign_dashboard_defaults} should not be called
27
+ # @option opts [Boolean] :do_not_use_translated_times True if given from and to times should used as is, without being resolved to reporter times - using this parameter can lead to inconsistent report contents
28
+ def initialize(grafana_obj, opts = {})
29
+ if grafana_obj.is_a?(Grafana::Panel)
30
+ @panel = grafana_obj
31
+ @dashboard = @panel.dashboard
32
+ @grafana = @dashboard.grafana
33
+
34
+ elsif grafana_obj.is_a?(Grafana::Dashboard)
35
+ @dashboard = grafana_obj
36
+ @grafana = @dashboard.grafana
37
+
38
+ elsif grafana_obj.is_a?(Grafana::Grafana)
39
+ @grafana = grafana_obj
40
+
41
+ elsif !grafana_obj
42
+ # nil given
11
43
 
12
- # @param grafana_or_panel [Object] {Grafana::Grafana} or {Grafana::Panel} object for which the query is executed
13
- def initialize(grafana_or_panel)
14
- if grafana_or_panel.is_a?(Grafana::Panel)
15
- @panel = grafana_or_panel
16
- @grafana = @panel.dashboard.grafana
17
44
  else
18
- @grafana = grafana_or_panel
45
+ raise GrafanaReporterError, "Internal error in AbstractQuery: given object is of type #{grafana_obj.class.name}, which is not supported"
19
46
  end
20
47
  @variables = {}
48
+ @variables['from'] = Grafana::Variable.new(nil)
49
+ @variables['to'] = Grafana::Variable.new(nil)
50
+
51
+ assign_dashboard_defaults unless opts[:ignore_dashboard_defaults]
52
+ opts[:variables].each { |k, v| assign_variable(k, v) } if opts[:variables].is_a?(Hash)
53
+
54
+ @translate_times = true
55
+ @translate_times = false if opts[:do_not_use_translated_times]
21
56
  end
22
57
 
23
58
  # @abstract
@@ -31,11 +66,33 @@ module GrafanaReporter
31
66
  def execute
32
67
  return @result unless @result.nil?
33
68
 
69
+ from = @variables['from'].raw_value
70
+ to = @variables['to'].raw_value
71
+ if @translate_times
72
+ from = translate_date(@variables['from'], @variables['grafana_report_timestamp'], false, @variables['from_timezone'] ||
73
+ @variables['grafana_default_from_timezone'])
74
+ to = translate_date(@variables['to'], @variables['grafana_report_timestamp'], true, @variables['to_timezone'] ||
75
+ @variables['grafana_default_to_timezone'])
76
+ end
77
+
34
78
  pre_process
35
79
  raise DatasourceNotSupportedError.new(@datasource, self) if @datasource.is_a?(Grafana::UnsupportedDatasource)
36
80
 
37
- @result = @datasource.request(from: @from, to: @to, raw_query: raw_query, variables: grafana_variables,
38
- prepared_request: @grafana.prepare_request, timeout: timeout)
81
+ begin
82
+ @result = @datasource.request(from: from, to: to, raw_query: raw_query, variables: grafana_variables,
83
+ prepared_request: @grafana.prepare_request, timeout: timeout)
84
+ rescue ::Grafana::GrafanaError
85
+ # grafana errors will be directly passed through
86
+ raise
87
+ rescue GrafanaReporterError
88
+ # grafana errors will be directly passed through
89
+ raise
90
+ rescue StandardError => e
91
+ raise DatasourceRequestInternalError.new(@datasource, e.message)
92
+ end
93
+
94
+ raise DatasourceRequestInvalidReturnValueError.new(@datasource, @result) unless datasource_response_valid?
95
+
39
96
  post_process
40
97
  @result
41
98
  end
@@ -66,17 +123,6 @@ module GrafanaReporter
66
123
  raise NotImplementedError
67
124
  end
68
125
 
69
- # Used to specify variables to be used for this query. This method ensures, that only the values of the
70
- # {Grafana::Variable} stored in the +variables+ Array are overwritten.
71
- # @param name [String] name of the variable to set
72
- # @param variable [Grafana::Variable] variable from which the {Grafana::Variable#raw_value} will be assigned to the query variables
73
- def assign_variable(name, variable)
74
- raise GrafanaReporterError, "Provided variable is not of type Grafana::Variable (name: '#{name}', value: '#{value}')" unless variable.is_a?(Grafana::Variable)
75
-
76
- @variables[name] ||= variable
77
- @variables[name].raw_value = variable.raw_value
78
- end
79
-
80
126
  # Transposes the given result.
81
127
  #
82
128
  # NOTE: Only the +:content+ of the given result hash is transposed. The +:header+ is ignored.
@@ -105,10 +151,10 @@ module GrafanaReporter
105
151
 
106
152
  filter_columns = filter_columns_variable.raw_value
107
153
  filter_columns.split(',').each do |filter_column|
108
- pos = result[:header][0].index(filter_column)
154
+ pos = result[:header].index(filter_column)
109
155
 
110
156
  unless pos.nil?
111
- result[:header][0].delete_at(pos)
157
+ result[:header].delete_at(pos)
112
158
  result[:content].each { |row| row.delete_at(pos) }
113
159
  end
114
160
  end
@@ -174,7 +220,6 @@ module GrafanaReporter
174
220
  # @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#request}.
175
221
  # @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
176
222
  # @return [Hash] query result with replaced values
177
- # TODO: make sure that caught errors are also visible in logger
178
223
  def replace_values(result, configs)
179
224
  return result if configs.empty?
180
225
 
@@ -189,8 +234,11 @@ module GrafanaReporter
189
234
 
190
235
  k = arr[0]
191
236
  v = arr[1]
192
- k.gsub!(/\\([:,])/, '\1')
193
- v.gsub!(/\\([:,])/, '\1')
237
+
238
+ # allow keys and values to contain escaped colons or commas
239
+ k = k.gsub(/\\([:,])/, '\1')
240
+ v = v.gsub(/\\([:,])/, '\1')
241
+
194
242
  result[:content].map do |row|
195
243
  (row.length - 1).downto 0 do |i|
196
244
  if cols.include?(i + 1) || cols.empty?
@@ -200,6 +248,7 @@ module GrafanaReporter
200
248
  begin
201
249
  row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
202
250
  rescue StandardError => e
251
+ @grafana.logger.error(e.message)
203
252
  row[i] = e.message
204
253
  end
205
254
 
@@ -224,6 +273,7 @@ module GrafanaReporter
224
273
  end
225
274
  end
226
275
  rescue StandardError => e
276
+ @grafana.logger.error(e.message)
227
277
  row[i] = e.message
228
278
  end
229
279
  end
@@ -241,6 +291,34 @@ module GrafanaReporter
241
291
  result
242
292
  end
243
293
 
294
+ # Used to build a table output in a custom format.
295
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
296
+ # @param opts [Hash] options for the formatting:
297
+ # @option opts [Grafana::Variable] :row_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated`
298
+ # @option opts [Grafana::Variable] :column_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated`
299
+ # @option opts [Grafana::Variable] :include_headline specifies if table should contain headline, defaults to false
300
+ # @option opts [Grafana::Variable] :table_formatter specifies which formatter shall be used, defaults to 'csv'
301
+ # @return [String] table in custom output format
302
+ def format_table_output(result, opts)
303
+ opts = { include_headline: Grafana::Variable.new('false'),
304
+ table_formatter: Grafana::Variable.new('csv'),
305
+ row_divider: Grafana::Variable.new('| '),
306
+ column_divider: Grafana::Variable.new(' | ') }.merge(opts.delete_if {|_k, v| v.nil? })
307
+
308
+ 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.")
312
+ return result[:content].map do |row|
313
+ opts[:row_divider].raw_value + row.map do |item|
314
+ item.to_s.gsub('|', '\\|')
315
+ end.join(opts[:column_divider].raw_value)
316
+ end.join("\n")
317
+ end
318
+
319
+ AbstractTableFormatStrategy.get(opts[:table_formatter].raw_value).format(result, opts[:include_headline].raw_value.downcase == 'true')
320
+ end
321
+
244
322
  # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
245
323
  # correct timestamp. Reason is that grafana does this in the frontend, which we have
246
324
  # to emulate here for the reporter.
@@ -256,8 +334,10 @@ module GrafanaReporter
256
334
  # @param timezone [Grafana::Variable] timezone to use, if not system timezone
257
335
  # @return [String] translated date as timestamp string
258
336
  def translate_date(orig_date, report_time, is_to_time, timezone = nil)
259
- # TODO: add test case for creation of variable, if not given, maybe also print a warning
337
+ @grafana.logger.warn("#translate_date has been called without 'report_time' - using current time as fallback.") unless report_time
260
338
  report_time ||= ::Grafana::Variable.new(Time.now.to_s)
339
+ orig_date = orig_date.raw_value if orig_date.is_a?(Grafana::Variable)
340
+
261
341
  return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
262
342
  return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
263
343
  return orig_date if orig_date =~ /^\d+$/
@@ -265,24 +345,24 @@ module GrafanaReporter
265
345
  # check if a relative date is mentioned
266
346
  date_spec = orig_date.clone
267
347
 
268
- date_spec.slice!(/^now/)
348
+ date_spec = date_spec.gsub(/^now/, '')
269
349
  raise TimeRangeUnknownError, orig_date unless date_spec
270
350
 
271
351
  date = DateTime.parse(report_time.raw_value)
272
- # TODO: allow from_translated or similar in ADOC template
352
+ # TODO: PRIO allow from_translated or similar in ADOC template
273
353
  date = date.new_offset(timezone.raw_value) if timezone
274
354
 
275
355
  until date_spec.empty?
276
356
  fit_match = date_spec.match(%r{^/(?<fit>[smhdwMy])})
277
357
  if fit_match
278
358
  date = fit_date(date, fit_match[:fit], is_to_time)
279
- date_spec.slice!(%r{^/#{fit_match[:fit]}})
359
+ date_spec = date_spec.gsub(%r{^/#{fit_match[:fit]}}, '')
280
360
  end
281
361
 
282
362
  delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
283
363
  if delta_match
284
364
  date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
285
- date_spec.slice!(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/)
365
+ date_spec = date_spec.gsub(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/, '')
286
366
  end
287
367
 
288
368
  raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
@@ -296,6 +376,45 @@ module GrafanaReporter
296
376
 
297
377
  private
298
378
 
379
+ # Used to specify variables to be used for this query. This method ensures, that only the values of the
380
+ # {Grafana::Variable} stored in the +variables+ Array are overwritten.
381
+ # @param name [String] name of the variable to set
382
+ # @param variable [Grafana::Variable] variable from which the {Grafana::Variable#raw_value} will be assigned to the query variables
383
+ def assign_variable(name, variable)
384
+ variable = Grafana::Variable.new(variable) unless variable.is_a?(Grafana::Variable)
385
+
386
+ @variables[name] ||= variable
387
+ @variables[name].raw_value = variable.raw_value
388
+ end
389
+
390
+ # Sets default configurations from the given {Grafana::Dashboard} and store them as settings in the
391
+ # {AbstractQuery}.
392
+ #
393
+ # Following data is extracted:
394
+ # - +from+, by {Grafana::Dashboard#from_time}
395
+ # - +to+, by {Grafana::Dashboard#to_time}
396
+ # - and all variables as {Grafana::Variable}, prefixed with +var-+, as grafana also does it
397
+ def assign_dashboard_defaults
398
+ return unless @dashboard
399
+
400
+ assign_variable('from', @dashboard.from_time)
401
+ assign_variable('to', @dashboard.to_time)
402
+ @dashboard.variables.each { |item| assign_variable("var-#{item.name}", item) }
403
+ end
404
+
405
+ def datasource_response_valid?
406
+ return false if @result.nil?
407
+ return false unless @result.is_a?(Hash)
408
+ # TODO: compare how empty valid responses look like in grafana
409
+ return true if @result.empty?
410
+ return false unless @result.key?(:header)
411
+ return false unless @result.key?(:content)
412
+ return false unless @result[:header].is_a?(Array)
413
+ return false unless @result[:content].is_a?(Array)
414
+
415
+ true
416
+ end
417
+
299
418
  # @return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name
300
419
  # is prefixed with +var-+
301
420
  def grafana_variables
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GrafanaReporter
4
- # @abstract Override {#create_report} and {#progress}.
4
+ # @abstract Override {#build} and {#progress}.
5
5
  #
6
6
  # This class is used to build a report on basis of a given configuration and
7
7
  # template.
@@ -124,8 +124,9 @@ module GrafanaReporter
124
124
  logger.internal_messages
125
125
  end
126
126
 
127
- # Is being called to start the report generation.
128
- # @param template [String] path to the template to be used, trailing +.adoc+ extension may be omitted
127
+ # Is being called to start the report generation. To execute the specific report generation, this function
128
+ # calls the abstract {#build} method with the given parameters.
129
+ # @param template [String] path to the template to be used, trailing extension may be omitted, whereas {#default_template_extension} will be appended
129
130
  # @param destination_file_or_path [String or File] path to the destination report or file object to use
130
131
  # @param custom_attributes [Hash] custom attributes, which shall be merged with priority over the configuration
131
132
  # @return [void]
@@ -135,13 +136,31 @@ module GrafanaReporter
135
136
  @destination_file_or_path = destination_file_or_path
136
137
  @custom_attributes = custom_attributes
137
138
 
138
- # automatically add extension, if a file with adoc extension exists
139
- @template = "#{@template}.adoc" if File.file?("#{@template}.adoc") && !File.file?(@template.to_s)
139
+ # automatically add extension, if a file with default template extension exists
140
+ @template = "#{@template}.#{self.class.default_template_extension}" if File.file?("#{@template}.#{self.class.default_template_extension}") && !File.file?(@template.to_s)
140
141
  raise MissingTemplateError, @template.to_s unless File.file?(@template.to_s)
141
142
 
142
143
  notify(:on_before_create)
143
144
  @start_time = Time.new
144
145
  logger.info("Report started at #{@start_time}")
146
+ build
147
+ rescue MissingTemplateError => e
148
+ @logger.error(e.message)
149
+ @error = [e.message]
150
+ done!
151
+ raise e
152
+ rescue StandardError => e
153
+ # catch all errors during execution
154
+ died_with_error(e)
155
+ raise e
156
+ ensure
157
+ done!
158
+ end
159
+
160
+ # @abstract
161
+ # Needs to be overridden by the report implementation.
162
+ def build(template, destination_file_or_path, custom_attributes)
163
+ raise NotImplementedError
145
164
  end
146
165
 
147
166
  # Used to calculate the progress of a report. By default expects +@total_steps+ to contain the total
@@ -167,6 +186,18 @@ module GrafanaReporter
167
186
  raise NotImplementedError
168
187
  end
169
188
 
189
+ # @abstract
190
+ # @return [String] specifying the default extension of a template file
191
+ def self.default_template_extension
192
+ raise NotImplementedError
193
+ end
194
+
195
+ # @abstract
196
+ # @return [String] specifying the default extension of a rendered result file
197
+ def self.default_result_extension
198
+ raise NotImplementedError
199
+ end
200
+
170
201
  private
171
202
 
172
203
  # Called, if the report generation has died with an error.
@@ -188,6 +219,7 @@ module GrafanaReporter
188
219
  def done!
189
220
  return if @done
190
221
 
222
+ @destination_file_or_path.close if @destination_file_or_path.is_a?(File)
191
223
  @done = true
192
224
  @end_time = Time.new
193
225
  @start_time ||= @end_time