ruby-grafana-reporter 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +144 -11
  3. data/lib/VERSION.rb +2 -2
  4. data/lib/grafana/abstract_datasource.rb +9 -3
  5. data/lib/grafana/dashboard.rb +6 -1
  6. data/lib/grafana/errors.rb +4 -11
  7. data/lib/grafana/grafana.rb +25 -1
  8. data/lib/grafana/grafana_environment_datasource.rb +56 -0
  9. data/lib/grafana/grafana_property_datasource.rb +8 -1
  10. data/lib/grafana/image_rendering_datasource.rb +5 -1
  11. data/lib/grafana/influxdb_datasource.rb +87 -3
  12. data/lib/grafana/panel.rb +1 -1
  13. data/lib/grafana/prometheus_datasource.rb +11 -2
  14. data/lib/grafana/sql_datasource.rb +3 -9
  15. data/lib/grafana/variable.rb +67 -36
  16. data/lib/grafana/webrequest.rb +1 -0
  17. data/lib/grafana_reporter/abstract_query.rb +65 -39
  18. data/lib/grafana_reporter/abstract_report.rb +19 -4
  19. data/lib/grafana_reporter/abstract_table_format_strategy.rb +74 -0
  20. data/lib/grafana_reporter/alerts_table_query.rb +6 -1
  21. data/lib/grafana_reporter/annotations_table_query.rb +6 -1
  22. data/lib/grafana_reporter/application/application.rb +7 -2
  23. data/lib/grafana_reporter/application/webservice.rb +41 -32
  24. data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +27 -0
  25. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +3 -2
  26. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +3 -2
  27. data/lib/grafana_reporter/asciidoctor/help.rb +470 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +7 -5
  29. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +7 -5
  30. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +5 -1
  31. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +6 -2
  32. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +3 -0
  33. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +3 -2
  34. data/lib/grafana_reporter/asciidoctor/report.rb +15 -13
  35. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +37 -6
  36. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +1 -1
  37. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +6 -2
  38. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +5 -1
  39. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +0 -5
  40. data/lib/grafana_reporter/configuration.rb +27 -0
  41. data/lib/grafana_reporter/console_configuration_wizard.rb +5 -3
  42. data/lib/grafana_reporter/csv_table_format_strategy.rb +25 -0
  43. data/lib/grafana_reporter/demo_report_wizard.rb +3 -7
  44. data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
  45. data/lib/grafana_reporter/erb/report.rb +13 -7
  46. data/lib/grafana_reporter/errors.rb +9 -7
  47. data/lib/grafana_reporter/panel_image_query.rb +1 -1
  48. data/lib/grafana_reporter/query_value_query.rb +7 -1
  49. data/lib/grafana_reporter/report_webhook.rb +12 -8
  50. data/lib/grafana_reporter/reporter_environment_datasource.rb +24 -0
  51. metadata +9 -3
  52. data/lib/grafana_reporter/help.rb +0 -443
@@ -29,14 +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
- unless @config['current'].nil?
37
- @raw_value = @config['current']['value']
38
- @text = @config['current']['text']
39
- end
39
+ init_values
40
40
  else
41
41
  @config = {}
42
42
  @raw_value = config_or_value
@@ -62,93 +62,105 @@ module Grafana
62
62
  def value_formatted(format = '')
63
63
  value = @raw_value
64
64
 
65
- # handle value 'All' properly
66
- # TODO: fix check for selection of All properly
67
- if (value == 'All') || (@text == 'All')
65
+ # if 'All' is selected for this template variable, capture all values properly
66
+ # (from grafana config or query) and format the results afterwards accordingly
67
+ if value == '$__all'
68
68
  if !@config['options'].empty?
69
- value = @config['options'].map { |item| item['value'] }
70
- elsif !@config['query'].empty?
71
- # TODO: replace variables in this query, too
72
- return @config['query']
73
- # TODO: handle 'All' value properly for query attributes
69
+ # this query contains predefined values, so capture them and format the values accordingly
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'] }
72
+
73
+ elsif @config['type'] == 'query' && !@config['query'].empty?
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 }
83
+
74
84
  else
75
- # TODO: how to handle All selection properly at this point?
85
+ # TODO: add support for variable type: 'datasource' and 'adhoc'
76
86
  end
77
87
  end
78
88
 
79
89
  case format
80
90
  when 'csv'
81
- return value.join(',').to_s if multi?
91
+ return value.join(',').to_s if multi? && value.is_a?(Array)
82
92
 
83
93
  value.to_s
84
94
 
85
95
  when 'distributed'
86
- return value.join(",#{name}=") if multi?
96
+ return value.join(",#{name}=") if multi? && value.is_a?(Array)
87
97
 
88
98
  value
89
99
  when 'doublequote'
90
- if multi?
100
+ if multi? && value.is_a?(Array)
91
101
  value = value.map { |item| "\"#{item.gsub(/\\/, '\\\\').gsub(/"/, '\\"')}\"" }
92
102
  return value.join(',')
93
103
  end
94
104
  "\"#{value.gsub(/"/, '\\"')}\""
95
105
 
96
106
  when 'json'
97
- if multi?
107
+ if multi? && value.is_a?(Array)
98
108
  value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\\0')}\"" }
99
109
  return "[#{value.join(',')}]"
100
110
  end
101
111
  "\"#{value.gsub(/"/, '\\"')}\""
102
112
 
103
113
  when 'percentencode'
104
- value = "{#{value.join(',')}}" if multi?
114
+ value = "{#{value.join(',')}}" if multi? && value.is_a?(Array)
105
115
  ERB::Util.url_encode(value)
106
116
 
107
117
  when 'pipe'
108
- return value.join('|') if multi?
118
+ return value.join('|') if multi? && value.is_a?(Array)
109
119
 
110
120
  value
111
121
 
112
122
  when 'raw'
113
- return "{#{value.join(',')}}" if multi?
123
+ return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
114
124
 
115
125
  value
116
126
 
117
127
  when 'regex'
118
- if multi?
128
+ if multi? && value.is_a?(Array)
119
129
  value = value.map { |item| item.gsub(%r{[/$.|\\]}, '\\\\\0') }
120
130
  return "(#{value.join('|')})"
121
131
  end
122
132
  value.gsub(%r{[/$.|\\]}, '\\\\\0')
123
133
 
124
134
  when 'singlequote'
125
- if multi?
135
+ if multi? && value.is_a?(Array)
126
136
  value = value.map { |item| "'#{item.gsub(/'/, '\\\\\0')}'" }
127
137
  return value.join(',')
128
138
  end
129
139
  "'#{value.gsub(/'/, '\\\\\0')}'"
130
140
 
131
141
  when 'sqlstring'
132
- if multi?
142
+ if multi? && value.is_a?(Array)
133
143
  value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
134
144
  return value.join(',')
135
145
  end
136
146
  "'#{value.gsub(/'/, "''")}'"
137
147
 
138
148
  when 'lucene'
139
- if multi?
149
+ if multi? && value.is_a?(Array)
140
150
  value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\\0')}\"" }
141
151
  return "(#{value.join(' OR ')})"
142
152
  end
143
153
  value.gsub(%r{[" |=/\\]}, '\\\\\0')
144
154
 
145
155
  when /^date(?::(?<format>.*))?$/
146
- # TODO: validate how grafana handles multivariables with date format
147
- 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))
148
160
 
149
161
  when ''
150
162
  # default
151
- if multi?
163
+ if multi? && value.is_a?(Array)
152
164
  value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
153
165
  return value.join(',')
154
166
  end
@@ -162,8 +174,9 @@ module Grafana
162
174
  end
163
175
  end
164
176
 
165
- # @return [Boolean] true, if the value can contain multiple selections, i.e. is an Array
177
+ # @return [Boolean] true, if the value can contain multiple selections, i.e. can contain an Array or does contain all
166
178
  def multi?
179
+ return true if @raw_value == '$__all'
167
180
  return @config['multi'] unless @config['multi'].nil?
168
181
 
169
182
  @raw_value.is_a? Array
@@ -181,12 +194,13 @@ module Grafana
181
194
  @text = new_text
182
195
  end
183
196
 
184
- private
185
-
186
- # Realize time formatting according
197
+ # Applies the date format according
187
198
  # {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to}
188
- # and {https://momentjs.com/docs/#/displaying/}.
189
- 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)
190
204
  return (Float(value) / 1000).to_i.to_s if format == 'seconds'
191
205
  return Time.at((Float(value) / 1000).to_i).utc.iso8601(3) if !format || (format == 'iso')
192
206
 
@@ -196,12 +210,13 @@ module Grafana
196
210
  until work_string.empty?
197
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}|
198
212
  h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
213
+
199
214
  if tmp.empty?
200
215
  matches << work_string[0]
201
- work_string.sub!(/^#{work_string[0]}/, '')
216
+ work_string = work_string.sub(/^#{work_string[0]}/, '')
202
217
  else
203
218
  matches << tmp[0]
204
- work_string.sub!(/^#{tmp[0]}/, '')
219
+ work_string = work_string.sub(/^#{tmp[0]}/, '')
205
220
  end
206
221
  end
207
222
 
@@ -213,5 +228,21 @@ module Grafana
213
228
 
214
229
  Time.at((Float(value) / 1000).to_i).strftime(format_string)
215
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
216
247
  end
217
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
@@ -1,5 +1,8 @@
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
  #
@@ -10,7 +13,7 @@ module GrafanaReporter
10
13
  attr_reader :variables, :result, :panel, :dashboard
11
14
 
12
15
  def timeout
13
- # TODO: check where value priorities should be evaluated
16
+ # TODO: PRIO check where value priorities should be evaluated
14
17
  return @variables['timeout'].raw_value if @variables['timeout']
15
18
  return @variables['grafana_default_timeout'].raw_value if @variables['grafana_default_timeout']
16
19
 
@@ -41,6 +44,7 @@ module GrafanaReporter
41
44
  else
42
45
  raise GrafanaReporterError, "Internal error in AbstractQuery: given object is of type #{grafana_obj.class.name}, which is not supported"
43
46
  end
47
+ @logger = @grafana ? @grafana.logger : ::Logger.new($stderr, level: :info)
44
48
  @variables = {}
45
49
  @variables['from'] = Grafana::Variable.new(nil)
46
50
  @variables['to'] = Grafana::Variable.new(nil)
@@ -76,7 +80,7 @@ module GrafanaReporter
76
80
  raise DatasourceNotSupportedError.new(@datasource, self) if @datasource.is_a?(Grafana::UnsupportedDatasource)
77
81
 
78
82
  begin
79
- @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,
80
84
  prepared_request: @grafana.prepare_request, timeout: timeout)
81
85
  rescue ::Grafana::GrafanaError
82
86
  # grafana errors will be directly passed through
@@ -89,6 +93,7 @@ module GrafanaReporter
89
93
  end
90
94
 
91
95
  raise DatasourceRequestInvalidReturnValueError.new(@datasource, @result) unless datasource_response_valid?
96
+
92
97
  post_process
93
98
  @result
94
99
  end
@@ -139,6 +144,8 @@ module GrafanaReporter
139
144
  #
140
145
  # Multiple columns may be filtered. Therefore the column titles have to be named in the
141
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 +_,+.
142
149
  # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
143
150
  # @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result
144
151
  # @return [Hash] filtered query result
@@ -146,8 +153,8 @@ module GrafanaReporter
146
153
  return result unless filter_columns_variable
147
154
 
148
155
  filter_columns = filter_columns_variable.raw_value
149
- filter_columns.split(',').each do |filter_column|
150
- pos = result[:header].index(filter_column)
156
+ filter_columns.split(/(?<!_),/).each do |filter_column|
157
+ pos = result[:header].index(filter_column.gsub("_,", ","))
151
158
 
152
159
  unless pos.nil?
153
160
  result[:header].delete_at(pos)
@@ -163,23 +170,33 @@ module GrafanaReporter
163
170
  # The formatting will be applied separately for every column. Therefore the column formats have to be named
164
171
  # in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for
165
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 +_,+.
166
179
  # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
167
180
  # @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result
168
181
  # @return [Hash] formatted query result
169
182
  def format_columns(result, formats)
170
183
  return result unless formats
171
184
 
172
- formats.text.split(',').each_index do |i|
173
- format = formats.text.split(',')[i]
185
+ formats.text.split(/(?<!_),/).each_index do |i|
186
+ format = formats.text.split(/(?<!_),/)[i].gsub("_,", ",")
174
187
  next if format.empty?
175
188
 
176
189
  result[:content].map do |row|
177
190
  next unless row.length > i
178
191
 
179
192
  begin
180
- 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
181
198
  rescue StandardError => e
182
- @grafana.logger.error(e.message)
199
+ @logger.error(e.message)
183
200
  row[i] = e.message
184
201
  end
185
202
  end
@@ -216,7 +233,6 @@ module GrafanaReporter
216
233
  # @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#request}.
217
234
  # @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
218
235
  # @return [Hash] query result with replaced values
219
- # TODO: make sure that caught errors are also visible in logger
220
236
  def replace_values(result, configs)
221
237
  return result if configs.empty?
222
238
 
@@ -231,8 +247,11 @@ module GrafanaReporter
231
247
 
232
248
  k = arr[0]
233
249
  v = arr[1]
234
- k.gsub!(/\\([:,])/, '\1')
235
- v.gsub!(/\\([:,])/, '\1')
250
+
251
+ # allow keys and values to contain escaped colons or commas
252
+ k = k.gsub(/\\([:,])/, '\1')
253
+ v = v.gsub(/\\([:,])/, '\1')
254
+
236
255
  result[:content].map do |row|
237
256
  (row.length - 1).downto 0 do |i|
238
257
  if cols.include?(i + 1) || cols.empty?
@@ -242,7 +261,7 @@ module GrafanaReporter
242
261
  begin
243
262
  row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
244
263
  rescue StandardError => e
245
- @grafana.logger.error(e.message)
264
+ @logger.error(e.message)
246
265
  row[i] = e.message
247
266
  end
248
267
 
@@ -267,6 +286,7 @@ module GrafanaReporter
267
286
  end
268
287
  end
269
288
  rescue StandardError => e
289
+ @logger.error(e.message)
270
290
  row[i] = e.message
271
291
  end
272
292
  end
@@ -284,22 +304,34 @@ module GrafanaReporter
284
304
  result
285
305
  end
286
306
 
287
- # Used to build a output format matching the requested report format.
307
+ # Used to build a table output in a custom format.
288
308
  # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
289
309
  # @param opts [Hash] options for the formatting:
290
- # @option opts [Grafana::Variable] :row_divider requested row divider for the result table
291
- # @option opts [Grafana::Variable] :column_divider requested row divider for the result table
292
- # @option opts [Regex or String] :escape_regex regular expression which specifies a part of a cell content, which has to be escaped
293
- # @option opts [String] :escape_replacement specifies how the found :escape_regex shall be replaced
294
- # @return [String] formatted table result in requested output format
310
+ # @option opts [Grafana::Variable] :row_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated`
311
+ # @option opts [Grafana::Variable] :column_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated`
312
+ # @option opts [Grafana::Variable] :include_headline specifies if table should contain headline, defaults to false
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
315
+ # @return [String] table in custom output format
295
316
  def format_table_output(result, opts)
296
- opts = { escape_regex: '|', escape_replacement: '\\|', row_divider: Grafana::Variable.new('| '), column_divider: Grafana::Variable.new(' | ') }.merge(opts.delete_if {|_k, v| v.nil? })
297
-
298
- result[:content].map do |row|
299
- opts[:row_divider].raw_value + row.map do |item|
300
- item.to_s.gsub(opts[:escape_regex], opts[:escape_replacement])
301
- end.join(opts[:column_divider].raw_value)
317
+ opts = { include_headline: Grafana::Variable.new('false'),
318
+ table_formatter: Grafana::Variable.new('csv'),
319
+ row_divider: Grafana::Variable.new('| '),
320
+ column_divider: Grafana::Variable.new(' | '),
321
+ transpose: Grafana::Variable.new('false') }.merge(opts.delete_if {|_k, v| v.nil? })
322
+
323
+ if opts[:table_formatter].raw_value == 'adoc_deprecated'
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.")
327
+ return result[:content].map do |row|
328
+ opts[:row_divider].raw_value + row.map do |item|
329
+ item.to_s.gsub('|', '\\|')
330
+ end.join(opts[:column_divider].raw_value)
331
+ end.join("\n")
302
332
  end
333
+
334
+ AbstractTableFormatStrategy.get(opts[:table_formatter].raw_value).format(result, opts[:include_headline].raw_value.downcase == 'true', opts[:transpose].raw_value.downcase == 'true')
303
335
  end
304
336
 
305
337
  # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
@@ -317,9 +349,10 @@ module GrafanaReporter
317
349
  # @param timezone [Grafana::Variable] timezone to use, if not system timezone
318
350
  # @return [String] translated date as timestamp string
319
351
  def translate_date(orig_date, report_time, is_to_time, timezone = nil)
320
- # TODO: add test case for creation of variable, if not given, maybe also print a warning
352
+ @logger.warn("#translate_date has been called without 'report_time' - using current time as fallback.") unless report_time
321
353
  report_time ||= ::Grafana::Variable.new(Time.now.to_s)
322
354
  orig_date = orig_date.raw_value if orig_date.is_a?(Grafana::Variable)
355
+
323
356
  return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
324
357
  return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
325
358
  return orig_date if orig_date =~ /^\d+$/
@@ -327,24 +360,24 @@ module GrafanaReporter
327
360
  # check if a relative date is mentioned
328
361
  date_spec = orig_date.clone
329
362
 
330
- date_spec.slice!(/^now/)
363
+ date_spec = date_spec.gsub(/^now/, '')
331
364
  raise TimeRangeUnknownError, orig_date unless date_spec
332
365
 
333
366
  date = DateTime.parse(report_time.raw_value)
334
- # TODO: allow from_translated or similar in ADOC template
367
+ # TODO: PRIO allow from_translated or similar in ADOC template
335
368
  date = date.new_offset(timezone.raw_value) if timezone
336
369
 
337
370
  until date_spec.empty?
338
371
  fit_match = date_spec.match(%r{^/(?<fit>[smhdwMy])})
339
372
  if fit_match
340
373
  date = fit_date(date, fit_match[:fit], is_to_time)
341
- date_spec.slice!(%r{^/#{fit_match[:fit]}})
374
+ date_spec = date_spec.gsub(%r{^/#{fit_match[:fit]}}, '')
342
375
  end
343
376
 
344
377
  delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
345
378
  if delta_match
346
379
  date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
347
- date_spec.slice!(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/)
380
+ date_spec = date_spec.gsub(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/, '')
348
381
  end
349
382
 
350
383
  raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
@@ -387,22 +420,15 @@ module GrafanaReporter
387
420
  def datasource_response_valid?
388
421
  return false if @result.nil?
389
422
  return false unless @result.is_a?(Hash)
390
- # TODO: check if it should be ok if a datasource request returns an empty hash only
391
- return true if @result.empty?
392
- return false unless @result.has_key?(:header)
393
- return false unless @result.has_key?(:content)
423
+ return false if @result.empty?
424
+ return false unless @result.key?(:header)
425
+ return false unless @result.key?(:content)
394
426
  return false unless @result[:header].is_a?(Array)
395
427
  return false unless @result[:content].is_a?(Array)
396
428
 
397
429
  true
398
430
  end
399
431
 
400
- # @return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name
401
- # is prefixed with +var-+
402
- def grafana_variables
403
- @variables.select { |k, _v| k =~ /^var-.+/ }
404
- end
405
-
406
432
  def delta_date(date, delta_count, time_letter)
407
433
  # substract specified time
408
434
  case time_letter
@@ -126,7 +126,7 @@ module GrafanaReporter
126
126
 
127
127
  # Is being called to start the report generation. To execute the specific report generation, this function
128
128
  # calls the abstract {#build} method with the given parameters.
129
- # @param template [String] path to the template to be used, trailing +.adoc+ extension may be omitted
129
+ # @param template [String] path to the template to be used, trailing extension may be omitted, whereas {#default_template_extension} will be appended
130
130
  # @param destination_file_or_path [String or File] path to the destination report or file object to use
131
131
  # @param custom_attributes [Hash] custom attributes, which shall be merged with priority over the configuration
132
132
  # @return [void]
@@ -136,14 +136,16 @@ module GrafanaReporter
136
136
  @destination_file_or_path = destination_file_or_path
137
137
  @custom_attributes = custom_attributes
138
138
 
139
- # automatically add extension, if a file with adoc extension exists
140
- @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)
141
141
  raise MissingTemplateError, @template.to_s unless File.file?(@template.to_s)
142
142
 
143
143
  notify(:on_before_create)
144
144
  @start_time = Time.new
145
145
  logger.info("Report started at #{@start_time}")
146
- build(template, destination_file_or_path, custom_attributes)
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?
148
+ build
147
149
  rescue MissingTemplateError => e
148
150
  @logger.error(e.message)
149
151
  @error = [e.message]
@@ -186,6 +188,18 @@ module GrafanaReporter
186
188
  raise NotImplementedError
187
189
  end
188
190
 
191
+ # @abstract
192
+ # @return [String] specifying the default extension of a template file
193
+ def self.default_template_extension
194
+ raise NotImplementedError
195
+ end
196
+
197
+ # @abstract
198
+ # @return [String] specifying the default extension of a rendered result file
199
+ def self.default_result_extension
200
+ raise NotImplementedError
201
+ end
202
+
189
203
  private
190
204
 
191
205
  # Called, if the report generation has died with an error.
@@ -207,6 +221,7 @@ module GrafanaReporter
207
221
  def done!
208
222
  return if @done
209
223
 
224
+ @destination_file_or_path.close if @destination_file_or_path.is_a?(File)
210
225
  @done = true
211
226
  @end_time = Time.new
212
227
  @start_time ||= @end_time
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ # The abstract base class, which is to be implemented for different table
5
+ # output formats. By implementing this class, you e.g. can decide if a table
6
+ # will be formatted as CSV, JSON or any other format.
7
+ class AbstractTableFormatStrategy
8
+ @@subclasses = []
9
+
10
+ def self.inherited(obj)
11
+ @@subclasses << obj
12
+ end
13
+
14
+ # @param abbreviation [String] name of the requested table format strategy
15
+ # @return [AbstractTableFormatStrategy] fitting strategy instance for the given name
16
+ def self.get(abbreviation)
17
+ @@subclasses.select { |item| item.abbreviation == abbreviation }.first.new
18
+ end
19
+
20
+ # @abstract
21
+ # @return [String] short name of the current stategy, under which it shall be accessible
22
+ def self.abbreviation
23
+ raise NotImplementedError
24
+ end
25
+
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
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
33
+ # @return [String] formatted in table format
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
+ }
72
+ end
73
+ end
74
+ end
@@ -33,7 +33,12 @@ module GrafanaReporter
33
33
  @result = replace_values(@result, @variables.select { |k, _v| k =~ /^replace_values_\d+/ })
34
34
  @result = filter_columns(@result, @variables['filter_columns'])
35
35
 
36
- @result = format_table_output(@result, row_divider: @variables['row_divider'], column_divider: @variables['column_divider'])
36
+ @result = format_table_output(@result,
37
+ row_divider: @variables['row_divider'],
38
+ column_divider: @variables['column_divider'],
39
+ table_formatter: @variables['table_formatter'],
40
+ include_headline: @variables['include_headline'],
41
+ transpose: @variables['transpose'])
37
42
  end
38
43
  end
39
44
  end
@@ -32,7 +32,12 @@ module GrafanaReporter
32
32
  @result = replace_values(@result, @variables.select { |k, _v| k =~ /^replace_values_\d+/ })
33
33
  @result = filter_columns(@result, @variables['filter_columns'])
34
34
 
35
- @result = format_table_output(@result, row_divider: @variables['row_divider'], column_divider: @variables['column_divider'])
35
+ @result = format_table_output(@result,
36
+ row_divider: @variables['row_divider'],
37
+ column_divider: @variables['column_divider'],
38
+ table_formatter: @variables['table_formatter'],
39
+ include_headline: @variables['include_headline'],
40
+ transpose: @variables['transpose'])
36
41
  end
37
42
  end
38
43
  end
@@ -13,7 +13,6 @@ module GrafanaReporter
13
13
  # It can be run to test the grafana connection, render a single template
14
14
  # or run as a service.
15
15
  class Application
16
-
17
16
  # Contains the {Configuration} object of the application.
18
17
  attr_accessor :config
19
18
 
@@ -140,7 +139,13 @@ module GrafanaReporter
140
139
 
141
140
  when Configuration::MODE_SINGLE_RENDER
142
141
  begin
143
- config.report_class.new(config).create_report(config.template, config.to_file)
142
+ template_ext = config.report_class.default_template_extension
143
+ report_ext = config.report_class.default_result_extension
144
+ default_to_file = File.basename(config.template.to_s.gsub(/(?:\.#{template_ext})?$/, ".#{report_ext}"))
145
+
146
+ to_file = config.to_file
147
+ to_file = "#{config.reports_folder}#{default_to_file}" if to_file == true
148
+ config.report_class.new(config).create_report(config.template, to_file)
144
149
  rescue StandardError => e
145
150
  puts "#{e.message}\n#{e.backtrace.join("\n")}"
146
151
  end