ruby-grafana-reporter 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -71
  3. data/bin/ruby-grafana-reporter +5 -5
  4. data/lib/VERSION.rb +3 -2
  5. data/lib/grafana/abstract_datasource.rb +116 -0
  6. data/lib/grafana/dashboard.rb +1 -3
  7. data/lib/grafana/errors.rb +15 -0
  8. data/lib/grafana/grafana.rb +53 -56
  9. data/lib/grafana/grafana_alerts_datasource.rb +57 -0
  10. data/lib/grafana/grafana_annotations_datasource.rb +56 -0
  11. data/lib/grafana/grafana_property_datasource.rb +25 -0
  12. data/lib/grafana/graphite_datasource.rb +44 -0
  13. data/lib/grafana/image_rendering_datasource.rb +44 -0
  14. data/lib/grafana/panel.rb +9 -3
  15. data/lib/grafana/prometheus_datasource.rb +39 -0
  16. data/lib/grafana/sql_datasource.rb +65 -0
  17. data/lib/grafana/variable.rb +1 -0
  18. data/lib/grafana/webrequest.rb +71 -0
  19. data/lib/grafana_reporter/abstract_query.rb +401 -0
  20. data/lib/grafana_reporter/abstract_report.rb +54 -3
  21. data/lib/grafana_reporter/alerts_table_query.rb +44 -0
  22. data/lib/grafana_reporter/annotations_table_query.rb +43 -0
  23. data/lib/grafana_reporter/application/application.rb +12 -8
  24. data/lib/grafana_reporter/application/webservice.rb +18 -6
  25. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +90 -0
  26. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +89 -0
  27. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +76 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +77 -0
  29. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +72 -0
  30. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +98 -0
  31. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
  32. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +23 -0
  33. data/lib/grafana_reporter/asciidoctor/report.rb +24 -31
  34. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  35. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  36. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +92 -0
  37. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +88 -0
  38. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  39. data/lib/grafana_reporter/configuration.rb +12 -6
  40. data/lib/grafana_reporter/console_configuration_wizard.rb +115 -65
  41. data/lib/grafana_reporter/demo_report_wizard.rb +87 -0
  42. data/lib/grafana_reporter/errors.rb +33 -0
  43. data/lib/grafana_reporter/help.rb +447 -0
  44. data/lib/grafana_reporter/logger/two_way_logger.rb +1 -1
  45. data/lib/grafana_reporter/panel_image_query.rb +29 -0
  46. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  47. data/lib/grafana_reporter/query_value_query.rb +79 -0
  48. data/lib/grafana_reporter/report_webhook.rb +35 -0
  49. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +0 -3
  50. metadata +37 -35
  51. data/lib/grafana/abstract_panel_query.rb +0 -22
  52. data/lib/grafana/abstract_query.rb +0 -132
  53. data/lib/grafana/abstract_sql_query.rb +0 -51
  54. data/lib/grafana/panel_image_query.rb +0 -52
  55. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -101
  56. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
  57. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -40
  58. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -92
  59. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -91
  60. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -69
  61. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -68
  62. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -61
  63. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -78
  64. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -73
  65. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -20
  66. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -43
  67. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -30
  68. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -70
  69. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -66
  70. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -88
  71. data/lib/grafana_reporter/asciidoctor/help.rb +0 -435
  72. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -36
  73. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -28
  74. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -44
  75. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -40
  76. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -312
  77. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -42
  78. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -44
@@ -0,0 +1,401 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ # @abstract Override {#pre_process}, {#post_process} and {#self.build_demo_entry} in subclass.
5
+ #
6
+ # Superclass containing everything for all queries towards grafana.
7
+ class AbstractQuery
8
+ attr_accessor :datasource, :timeout, :from, :to
9
+ attr_writer :raw_query
10
+ attr_reader :variables, :result, :panel
11
+
12
+ # @param grafana_or_panel [Object] {Grafana} or {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
+ else
18
+ @grafana = grafana_or_panel
19
+ end
20
+ @variables = {}
21
+ end
22
+
23
+ # @abstract
24
+ #
25
+ # Runs the whole process to receive values properly from this query:
26
+ # - calls {#pre_process}
27
+ # - executes this query against the {Grafana} instance
28
+ # - calls {#post_process}
29
+ #
30
+ # @return [Hash] result of the query in standardized format
31
+ def execute
32
+ return @result unless @result.nil?
33
+
34
+ pre_process
35
+ @result = @datasource.request(from: @from, to: @to, raw_query: raw_query, variables: grafana_variables,
36
+ prepared_request: @grafana.prepare_request, timeout: timeout)
37
+ post_process
38
+ @result
39
+ end
40
+
41
+ # Sets default configurations from the given {Dashboard} and store them as settings in the query.
42
+ #
43
+ # Following data is extracted:
44
+ # - +from+, by {Dashboard#from_time}
45
+ # - +to+, by {Dashboard#to_time}
46
+ # - and all variables as {Variable}, prefixed with +var-+, as grafana also does it
47
+ # @param dashboard [Dashboard] dashboard from which the defaults are captured
48
+ def set_defaults_from_dashboard(dashboard)
49
+ @from = dashboard.from_time
50
+ @to = dashboard.to_time
51
+ dashboard.variables.each { |item| merge_variables({ "var-#{item.name}": item }) }
52
+ end
53
+
54
+ # Overwrite this function to extract a proper raw query value from this object.
55
+ #
56
+ # If the property +@raw_query+ is not set manually by the calling object, this
57
+ # method may be overwritten to extract the raw query from this object instead.
58
+ def raw_query
59
+ @raw_query
60
+ end
61
+
62
+ # @abstract
63
+ #
64
+ # Overwrite this function to perform all necessary actions, before the query is actually executed.
65
+ # Here you can e.g. set values of variables or similar.
66
+ #
67
+ # Especially for direct queries, it is essential to set the +@datasource+ variable at latest here in the
68
+ # subclass.
69
+ def pre_process
70
+ raise NotImplementedError
71
+ end
72
+
73
+ # @abstract
74
+ #
75
+ # Use this function to format the raw result of the @result variable to conform to the expected return value.
76
+ def post_process
77
+ raise NotImplementedError
78
+ end
79
+
80
+ # Merges the given hashes to the current object by using the {#merge_variables} method.
81
+ # It respects the priorities of the hashes and the object and allows only valid variables to be passed.
82
+ # @param document_hash [Hash] variables from report template level
83
+ # @param item_hash [Hash] variables from item configuration level, i.e. specific call, which may override document
84
+ # @return [void]
85
+ # TODO: rename method
86
+ def merge_hash_variables(document_hash, item_hash)
87
+ sel_doc_items = document_hash.select do |k, _v|
88
+ k =~ /^var-/ || k == 'grafana-report-timestamp' || k =~ /grafana_default_(?:from|to)_timezone/
89
+ end
90
+ merge_variables(sel_doc_items.each_with_object({}) { |(k, v), h| h[k] = ::Grafana::Variable.new(v) })
91
+
92
+ sel_items = item_hash.select do |k, _v|
93
+ # TODO: specify accepted options in each class or check if simply all can be allowed with prefix +var-+
94
+ k =~ /^var-/ || k =~ /^render-/ || k =~ /filter_columns|format|replace_values_.*|transpose|column_divider|
95
+ row_divider|from_timezone|to_timezone|result_type|query/x
96
+ end
97
+ merge_variables(sel_items.each_with_object({}) { |(k, v), h| h[k] = ::Grafana::Variable.new(v) })
98
+
99
+ @timeout = item_hash['timeout'] || document_hash['grafana-default-timeout'] || @timeout
100
+ @from = item_hash['from'] || document_hash['from'] || @from
101
+ @to = item_hash['to'] || document_hash['to'] || @to
102
+ end
103
+
104
+ # Transposes the given result.
105
+ #
106
+ # NOTE: Only the +:content+ of the given result hash is transposed. The +:header+ is ignored.
107
+ #
108
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#preformat_response})
109
+ # @param transpose_variable [Grafana::Variable] true, if the result hash shall be transposed
110
+ # @return [Hash] transposed query result
111
+ def transpose(result, transpose_variable)
112
+ return result unless transpose_variable
113
+ return result unless transpose_variable.raw_value == 'true'
114
+
115
+ result[:content] = result[:content].transpose
116
+
117
+ result
118
+ end
119
+
120
+ # Filters columns out of the query result.
121
+ #
122
+ # Multiple columns may be filtered. Therefore the column titles have to be named in the
123
+ # {Grafana::Variable#raw_value} and have to be separated by +,+ (comma).
124
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#preformat_response})
125
+ # @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result
126
+ # @return [Hash] filtered query result
127
+ def filter_columns(result, filter_columns_variable)
128
+ return result unless filter_columns_variable
129
+
130
+ filter_columns = filter_columns_variable.raw_value
131
+ filter_columns.split(',').each do |filter_column|
132
+ pos = result[:header][0].index(filter_column)
133
+
134
+ unless pos.nil?
135
+ result[:header][0].delete_at(pos)
136
+ result[:content].each { |row| row.delete_at(pos) }
137
+ end
138
+ end
139
+
140
+ result
141
+ end
142
+
143
+ # Uses the Kernel#format method to format values in the query results.
144
+ #
145
+ # The formatting will be applied separately for every column. Therefore the column formats have to be named
146
+ # in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for
147
+ # a column, no change will happen.
148
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#preformat_response})
149
+ # @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result
150
+ # @return [Hash] formatted query result
151
+ # TODO: make sure that caught errors are also visible in logger
152
+ def format_columns(result, formats)
153
+ return result unless formats
154
+
155
+ formats.text.split(',').each_index do |i|
156
+ format = formats.text.split(',')[i]
157
+ next if format.empty?
158
+
159
+ result[:content].map do |row|
160
+ next unless row.length > i
161
+
162
+ begin
163
+ row[i] = format % row[i] if row[i]
164
+ rescue StandardError => e
165
+ row[i] = e.message
166
+ end
167
+ end
168
+ end
169
+ result
170
+ end
171
+
172
+ # Used to replace values in a query result according given configurations.
173
+ #
174
+ # The given variables will be applied to an appropriate column, depending
175
+ # on the naming of the variable. The variable name ending specifies the column,
176
+ # e.g. a variable named +replace_values_2+ will be applied to the second column.
177
+ #
178
+ # The {Grafana::Variable#text} needs to contain the replace specification.
179
+ # Multiple replacements can be specified by separating them with +,+. If a
180
+ # literal comma is needed, it can be escaped with a backslash: +\\,+.
181
+ #
182
+ # The rule will be separated from the replacement text with a colon +:+.
183
+ # If a literal colon is wanted, it can be escaped with a backslash: +\\:+.
184
+ #
185
+ # Examples:
186
+ # - Basic string replacement
187
+ # MyTest:ThisValue
188
+ # will replace all occurences of the text 'MyTest' with 'ThisValue'.
189
+ # - Number comparison
190
+ # <=10:OK
191
+ # will replace all values smaller or equal to 10 with 'OK'.
192
+ # - Regular expression
193
+ # ^[^ ]\\+ (\d+)$:\1 is the answer
194
+ # will replace all values matching the pattern, e.g. 'answerToAllQuestions 42' to
195
+ # '42 is the answer'. Important to know: the regular expressions always have to start
196
+ # with +^+ and end with +$+, i.e. the expression itself always has to match
197
+ # the whole content in one field.
198
+ # @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#preformat_response}.
199
+ # @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
200
+ # @return [Hash] query result with replaced values
201
+ # TODO: make sure that caught errors are also visible in logger
202
+ def replace_values(result, configs)
203
+ return result if configs.empty?
204
+
205
+ configs.each do |key, formats|
206
+ cols = key.split('_')[2..-1].map(&:to_i)
207
+
208
+ formats.text.split(/(?<!\\),/).each_index do |j|
209
+ format = formats.text.split(/(?<!\\),/)[j]
210
+
211
+ arr = format.split(/(?<!\\):/)
212
+ raise MalformedReplaceValuesStatementError, format if arr.length != 2
213
+
214
+ k = arr[0]
215
+ v = arr[1]
216
+ k.gsub!(/\\([:,])/, '\1')
217
+ v.gsub!(/\\([:,])/, '\1')
218
+ result[:content].map do |row|
219
+ (row.length - 1).downto 0 do |i|
220
+ if cols.include?(i + 1) || cols.empty?
221
+
222
+ # handle regular expressions
223
+ if k.start_with?('^') && k.end_with?('$')
224
+ begin
225
+ row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
226
+ rescue StandardError => e
227
+ row[i] = e.message
228
+ end
229
+
230
+ # handle value comparisons
231
+ elsif (match = k.match(/^ *(?<operator>[<>]=?|<>|=) *(?<number>[+-]?\d+(?:\.\d+)?)$/))
232
+ skip = false
233
+ begin
234
+ val = Float(row[i])
235
+ rescue StandardError
236
+ # value cannot be converted to number, simply ignore it as the comparison does not fit here
237
+ skip = true
238
+ end
239
+
240
+ unless skip
241
+ begin
242
+ op = match[:operator].gsub(/^=$/, '==').gsub(/^<>$/, '!=')
243
+ if val.public_send(op.to_sym, Float(match[:number]))
244
+ row[i] = if v.include?('\\1')
245
+ v.gsub(/\\1/, row[i].to_s)
246
+ else
247
+ v
248
+ end
249
+ end
250
+ rescue StandardError => e
251
+ row[i] = e.message
252
+ end
253
+ end
254
+
255
+ # handle as normal comparison
256
+ elsif row[i].to_s == k
257
+ row[i] = v
258
+ end
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+
265
+ result
266
+ end
267
+
268
+ # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
269
+ # correct timestamp. Reason is that grafana does this in the frontend, which we have
270
+ # to emulate here for the reporter.
271
+ #
272
+ # Additionally providing this function the +report_time+ assures that all queries
273
+ # rendered within one report will use _exactly_ the same timestamp in those relative
274
+ # times, i.e. there shouldn't appear any time differences, no matter how long the
275
+ # report is running.
276
+ # @param orig_date [String] time string provided by grafana, usually +from+ or +to+.
277
+ # @param report_time [Grafana::Variable] report start time
278
+ # @param is_to_time [Boolean] true, if the time should be calculated for +to+, false if it shall be
279
+ # calculated for +from+
280
+ # @param timezone [Grafana::Variable] timezone to use, if not system timezone
281
+ # @return [String] translated date as timestamp string
282
+ def translate_date(orig_date, report_time, is_to_time, timezone = nil)
283
+ report_time ||= Variable.new(Time.now.to_s)
284
+ return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
285
+ return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
286
+ return orig_date if orig_date =~ /^\d+$/
287
+
288
+ # check if a relative date is mentioned
289
+ date_spec = orig_date.clone
290
+
291
+ date_spec.slice!(/^now/)
292
+ raise TimeRangeUnknownError, orig_date unless date_spec
293
+
294
+ date = DateTime.parse(report_time.raw_value)
295
+ # TODO: allow from_translated or similar in ADOC template
296
+ date = date.new_offset(timezone.raw_value) if timezone
297
+
298
+ until date_spec.empty?
299
+ fit_match = date_spec.match(%r{^/(?<fit>[smhdwMy])})
300
+ if fit_match
301
+ date = fit_date(date, fit_match[:fit], is_to_time)
302
+ date_spec.slice!(%r{^/#{fit_match[:fit]}})
303
+ end
304
+
305
+ delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
306
+ if delta_match
307
+ date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
308
+ date_spec.slice!(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/)
309
+ end
310
+
311
+ raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
312
+ end
313
+
314
+ # step back one second, if this is the 'to' time
315
+ date = (date.to_time - 1).to_datetime if is_to_time
316
+
317
+ (Time.at(date.to_time.to_i).to_i * 1000).to_s
318
+ end
319
+
320
+ private
321
+
322
+ # @return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name
323
+ # is prefixed with +var-+
324
+ def grafana_variables
325
+ @variables.select { |k, _v| k =~ /^var-.+/ }
326
+ end
327
+
328
+ def delta_date(date, delta_count, time_letter)
329
+ # substract specified time
330
+ case time_letter
331
+ when 's'
332
+ (date.to_time + (delta_count * 1)).to_datetime
333
+ when 'm'
334
+ (date.to_time + (delta_count * 60)).to_datetime
335
+ when 'h'
336
+ (date.to_time + (delta_count * 60 * 60)).to_datetime
337
+ when 'd'
338
+ date.next_day(delta_count)
339
+ when 'w'
340
+ date.next_day(delta_count * 7)
341
+ when 'M'
342
+ date.next_month(delta_count)
343
+ when 'y'
344
+ date.next_year(delta_count)
345
+ end
346
+ end
347
+
348
+ def fit_date(date, fit_letter, is_to_time)
349
+ # fit to specified time frame
350
+ case fit_letter
351
+ when 's'
352
+ date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, date.sec, date.zone)
353
+ date = (date.to_time + 1).to_datetime if is_to_time
354
+ when 'm'
355
+ date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, 0, date.zone)
356
+ date = (date.to_time + 60).to_datetime if is_to_time
357
+ when 'h'
358
+ date = DateTime.new(date.year, date.month, date.day, date.hour, 0, 0, date.zone)
359
+ date = (date.to_time + 60 * 60).to_datetime if is_to_time
360
+ when 'd'
361
+ date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
362
+ date = date.next_day(1) if is_to_time
363
+ when 'w'
364
+ date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
365
+ date = if date.wday.zero?
366
+ date.prev_day(7)
367
+ else
368
+ date.prev_day(date.wday - 1)
369
+ end
370
+ date = date.next_day(7) if is_to_time
371
+ when 'M'
372
+ date = DateTime.new(date.year, date.month, 1, 0, 0, 0, date.zone)
373
+ date = date.next_month if is_to_time
374
+ when 'y'
375
+ date = DateTime.new(date.year, 1, 1, 0, 0, 0, date.zone)
376
+ date = date.next_year if is_to_time
377
+ end
378
+
379
+ date
380
+ end
381
+
382
+ # Merges the given Hash with the stored variables.
383
+ #
384
+ # Can be used to easily set many values at once in the local variables hash.
385
+ #
386
+ # Please note, that the values of the Hash need to be of type {Variable}.
387
+ #
388
+ # @param hash [Hash<String,Variable>] Hash containing variable name as key and {Variable} as value
389
+ # @return [AbstractQuery] this object
390
+ # TODO: test if this method can be removed, or make it private at least
391
+ def merge_variables(hash)
392
+ hash.each do |k, v|
393
+ if @variables[k.to_s].nil?
394
+ @variables[k.to_s] = v
395
+ else
396
+ @variables[k.to_s].raw_value = v.raw_value
397
+ end
398
+ end
399
+ end
400
+ end
401
+ end
@@ -9,6 +9,13 @@ module GrafanaReporter
9
9
  # Objects of this class are also stored in {Application::Application}, unless
10
10
  # the retention time is over.
11
11
  class AbstractReport
12
+ # Array of supported event callback symbols
13
+ EVENT_CALLBACKS = %i[all on_before_create on_after_cancel on_after_finish].freeze
14
+
15
+ # Class variable for storing event listeners
16
+ @@event_listeners = {}
17
+ @@event_listeners.default = []
18
+
12
19
  # @return [String] path to the template
13
20
  attr_reader :template
14
21
 
@@ -45,11 +52,26 @@ module GrafanaReporter
45
52
  raise MissingTemplateError, @template.to_s unless File.exist?(@template.to_s)
46
53
  end
47
54
 
55
+ # Registers a new event listener object.
56
+ # @param event [Symbol] one of EVENT_CALLBACKS
57
+ # @param listener [Object] object responding to #callback(event_symbol, object)
58
+ def self.add_event_listener(event, listener)
59
+ @@event_listeners[event] = [] if @@event_listeners[event] == []
60
+ @@event_listeners[event].push(listener)
61
+ end
62
+
63
+ # Removes all registeres event listener objects
64
+ def self.clear_event_listeners
65
+ @@event_listeners = {}
66
+ @@event_listeners.default = []
67
+ end
68
+
48
69
  # Call to request cancelling the report generation.
49
70
  # @return [void]
50
71
  def cancel!
51
72
  @cancel = true
52
73
  logger.info('Cancelling report generation invoked.')
74
+ notify(:on_after_cancel)
53
75
  end
54
76
 
55
77
  # @return [String] path to the report destination file
@@ -81,9 +103,12 @@ module GrafanaReporter
81
103
  @error || []
82
104
  end
83
105
 
84
- # @return [String] status of the report, one of 'in progress', 'cancelled', 'died' or 'finished'.
106
+ # @return [String] status of the report as string, either 'not started', 'in progress', 'cancelling',
107
+ # 'cancelled', 'died' or 'finished'.
85
108
  def status
109
+ return 'not started' unless @start_time
86
110
  return 'cancelled' if done && cancel
111
+ return 'cancelling' if !done && cancel
87
112
  return 'finished' if done && error.empty?
88
113
  return 'died' if done && !error.empty?
89
114
 
@@ -96,11 +121,12 @@ module GrafanaReporter
96
121
  logger.internal_messages
97
122
  end
98
123
 
99
- # @abstract
100
124
  # Is being called to start the report generation.
101
125
  # @return [void]
102
126
  def create_report
103
- raise NotImplementedError
127
+ notify(:on_before_create)
128
+ @start_time = Time.new
129
+ logger.info("Report started at #{@start_time}")
104
130
  end
105
131
 
106
132
  # @abstract
@@ -108,5 +134,30 @@ module GrafanaReporter
108
134
  def progress
109
135
  raise NotImplementedError
110
136
  end
137
+
138
+ private
139
+
140
+ def done!
141
+ @done = true
142
+ @end_time = Time.new
143
+ logger.info("Report creation ended after #{@end_time - @start_time} seconds with status '#{status}'")
144
+ notify(:on_after_finish)
145
+ end
146
+
147
+ def notify(event)
148
+ (@@event_listeners[:all] + @@event_listeners[event]).each do |listener|
149
+ logger.debug("Informing event listener '#{listener.class}' about event '#{event}' for report '#{object_id}'.")
150
+ begin
151
+ res = listener.callback(event, self)
152
+ logger.debug("Event listener '#{listener.class}' for event '#{event}' and report '#{object_id}' returned "\
153
+ "with result '#{res}'.")
154
+ rescue StandardError => e
155
+ msg = "Event listener '#{listener.class}' for event '#{event}' and report '#{object_id}' returned with "\
156
+ "error: #{e.message} - #{e.backtrace}."
157
+ puts msg
158
+ logger.error(msg)
159
+ end
160
+ end
161
+ end
111
162
  end
112
163
  end