ruby-grafana-reporter 0.4.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +144 -11
  3. data/lib/VERSION.rb +2 -2
  4. data/lib/grafana/abstract_datasource.rb +9 -3
  5. data/lib/grafana/dashboard.rb +6 -1
  6. data/lib/grafana/errors.rb +4 -11
  7. data/lib/grafana/grafana.rb +25 -1
  8. data/lib/grafana/grafana_environment_datasource.rb +56 -0
  9. data/lib/grafana/grafana_property_datasource.rb +8 -1
  10. data/lib/grafana/image_rendering_datasource.rb +5 -1
  11. data/lib/grafana/influxdb_datasource.rb +87 -3
  12. data/lib/grafana/panel.rb +1 -1
  13. data/lib/grafana/prometheus_datasource.rb +11 -2
  14. data/lib/grafana/sql_datasource.rb +3 -9
  15. data/lib/grafana/variable.rb +67 -36
  16. data/lib/grafana/webrequest.rb +1 -0
  17. data/lib/grafana_reporter/abstract_query.rb +65 -39
  18. data/lib/grafana_reporter/abstract_report.rb +19 -4
  19. data/lib/grafana_reporter/abstract_table_format_strategy.rb +74 -0
  20. data/lib/grafana_reporter/alerts_table_query.rb +6 -1
  21. data/lib/grafana_reporter/annotations_table_query.rb +6 -1
  22. data/lib/grafana_reporter/application/application.rb +7 -2
  23. data/lib/grafana_reporter/application/webservice.rb +41 -32
  24. data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +27 -0
  25. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +3 -2
  26. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +3 -2
  27. data/lib/grafana_reporter/asciidoctor/help.rb +470 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +7 -5
  29. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +7 -5
  30. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +5 -1
  31. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +6 -2
  32. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +3 -0
  33. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +3 -2
  34. data/lib/grafana_reporter/asciidoctor/report.rb +15 -13
  35. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +37 -6
  36. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +1 -1
  37. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +6 -2
  38. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +5 -1
  39. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +0 -5
  40. data/lib/grafana_reporter/configuration.rb +27 -0
  41. data/lib/grafana_reporter/console_configuration_wizard.rb +5 -3
  42. data/lib/grafana_reporter/csv_table_format_strategy.rb +25 -0
  43. data/lib/grafana_reporter/demo_report_wizard.rb +3 -7
  44. data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
  45. data/lib/grafana_reporter/erb/report.rb +13 -7
  46. data/lib/grafana_reporter/errors.rb +9 -7
  47. data/lib/grafana_reporter/panel_image_query.rb +1 -1
  48. data/lib/grafana_reporter/query_value_query.rb +7 -1
  49. data/lib/grafana_reporter/report_webhook.rb +12 -8
  50. data/lib/grafana_reporter/reporter_environment_datasource.rb +24 -0
  51. metadata +9 -3
  52. data/lib/grafana_reporter/help.rb +0 -443
@@ -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