ruby-grafana-reporter 0.4.3 → 0.4.4

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +337 -203
  3. data/lib/VERSION.rb +2 -2
  4. data/lib/grafana/abstract_datasource.rb +6 -3
  5. data/lib/grafana/errors.rb +2 -2
  6. data/lib/grafana/grafana_property_datasource.rb +8 -1
  7. data/lib/grafana/panel.rb +1 -1
  8. data/lib/grafana/sql_datasource.rb +1 -9
  9. data/lib/grafana/variable.rb +26 -20
  10. data/lib/grafana_reporter/abstract_query.rb +37 -19
  11. data/lib/grafana_reporter/abstract_report.rb +17 -4
  12. data/lib/grafana_reporter/abstract_table_format_strategy.rb +34 -0
  13. data/lib/grafana_reporter/alerts_table_query.rb +5 -1
  14. data/lib/grafana_reporter/annotations_table_query.rb +5 -1
  15. data/lib/grafana_reporter/application/application.rb +7 -2
  16. data/lib/grafana_reporter/application/webservice.rb +34 -29
  17. data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +25 -0
  18. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +2 -1
  19. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +2 -1
  20. data/lib/grafana_reporter/asciidoctor/help.rb +458 -0
  21. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +5 -1
  22. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +5 -1
  23. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +5 -1
  24. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +5 -1
  25. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +3 -0
  26. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +3 -2
  27. data/lib/grafana_reporter/asciidoctor/report.rb +15 -13
  28. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +1 -1
  29. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +5 -1
  30. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +5 -1
  31. data/lib/grafana_reporter/console_configuration_wizard.rb +2 -2
  32. data/lib/grafana_reporter/csv_table_format_strategy.rb +23 -0
  33. data/lib/grafana_reporter/demo_report_wizard.rb +0 -1
  34. data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
  35. data/lib/grafana_reporter/erb/report.rb +13 -7
  36. data/lib/grafana_reporter/errors.rb +9 -7
  37. data/lib/grafana_reporter/query_value_query.rb +1 -1
  38. data/lib/grafana_reporter/report_webhook.rb +12 -8
  39. metadata +7 -3
  40. data/lib/grafana_reporter/help.rb +0 -443
data/lib/VERSION.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Version information
4
- GRAFANA_REPORTER_VERSION = [0, 4, 3].freeze
4
+ GRAFANA_REPORTER_VERSION = [0, 4, 4].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2021-06-18'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2021-07-12'
@@ -42,12 +42,12 @@ module Grafana
42
42
  @model = model
43
43
  end
44
44
 
45
- # @return [String] category of the datasource, e.g. `tsdb` or `sql`
45
+ # @return [String] category of the datasource, e.g. +tsdb+ or +sql+
46
46
  def category
47
47
  @model['meta']['category']
48
48
  end
49
49
 
50
- # @return [String] type of the datasource, e.g. `mysql`
50
+ # @return [String] type of the datasource, e.g. +mysql+
51
51
  def type
52
52
  @model['type'] || @model['meta']['id']
53
53
  end
@@ -126,10 +126,13 @@ module Grafana
126
126
  while repeat && (repeat_count < 3)
127
127
  repeat = false
128
128
  repeat_count += 1
129
+
129
130
  variables.each do |name, variable|
130
131
  # only set ticks if value is string
131
132
  var_name = name.gsub(/^var-/, '')
132
- res = res.gsub(/(?:\$\{#{var_name}(?::(?<format>\w+))?\}|\$#{var_name})/) do
133
+ next unless var_name =~ /^\w+$/
134
+
135
+ res = res.gsub(/(?:\$\{#{var_name}(?::(?<format>\w+))?\}|\$#{var_name}(?!\w))/) do
133
136
  format = default_variable_format
134
137
  if $LAST_MATCH_INFO
135
138
  format = $LAST_MATCH_INFO[:format] if $LAST_MATCH_INFO[:format]
@@ -37,9 +37,9 @@ module Grafana
37
37
 
38
38
  # Raised if a given datasource does not exist in a specific {Grafana} instance.
39
39
  class DatasourceDoesNotExistError < GrafanaError
40
- # @param field [String] specifies, how the datasource has been searched, e.g. 'id' or 'name'
40
+ # @param field [String] specifies, how the datasource has been searched, e.g. +id+ or +name+
41
41
  # @param datasource_identifier [String] identifier of the datasource, which could not be found,
42
- # e.g. the specifiy id or name
42
+ # e.g. the specified id or name
43
43
  def initialize(field, datasource_identifier)
44
44
  super("Datasource with #{field} '#{datasource_identifier}' does not exist.")
45
45
  end
@@ -16,6 +16,8 @@ module Grafana
16
16
  panel = query_description[:raw_query][:panel]
17
17
  property_name = query_description[:raw_query][:property_name]
18
18
 
19
+ return "Panel property '#{property_name}' does not exist for panel '#{panel.id}'" unless panel.field(property_name)
20
+
19
21
  {
20
22
  header: [query_description[:raw_query][:property_name]],
21
23
  content: [replace_variables(panel.field(property_name), query_description[:variables])]
@@ -24,7 +26,12 @@ module Grafana
24
26
 
25
27
  # @see AbstractDatasource#default_variable_format
26
28
  def default_variable_format
27
- 'glob'
29
+ 'glob'
30
+ end
31
+
32
+ # @see AbstractDatasource#name
33
+ def name
34
+ self.class.to_s
28
35
  end
29
36
  end
30
37
  end
data/lib/grafana/panel.rb CHANGED
@@ -18,7 +18,7 @@ module Grafana
18
18
  def field(field)
19
19
  return @model[field] if @model.key?(field)
20
20
 
21
- ''
21
+ nil
22
22
  end
23
23
 
24
24
  # @return [String] panel ID
@@ -19,7 +19,7 @@ module Grafana
19
19
  body: {
20
20
  from: query_description[:from],
21
21
  to: query_description[:to],
22
- queries: [rawSql: prepare_sql(sql), datasourceId: id, format: 'table']
22
+ queries: [rawSql: sql, datasourceId: id, format: 'table']
23
23
  }.to_json,
24
24
  request: Net::HTTP::Post
25
25
  }
@@ -66,13 +66,5 @@ module Grafana
66
66
 
67
67
  results
68
68
  end
69
-
70
- def prepare_sql(sql)
71
- # remove comments in query
72
- sql.gsub!(/--[^\r\n]*(?:[\r\n]+|$)/, ' ')
73
- sql.gsub!(/\r\n/, ' ')
74
- sql.gsub!(/\n/, ' ')
75
- sql
76
- end
77
69
  end
78
70
  end
@@ -63,92 +63,96 @@ module Grafana
63
63
  value = @raw_value
64
64
 
65
65
  # handle value 'All' properly
66
- # TODO: fix check for selection of All properly
67
- if (value == 'All') || (@text == 'All')
66
+ if value == '$__all'
68
67
  if !@config['options'].empty?
68
+ # this query contains predefined values, so capture them and format the values accordingly
69
+ # this happens either if type='custom' or a query, which is never updated
69
70
  value = @config['options'].map { |item| item['value'] }
70
- elsif !@config['query'].empty?
71
- # TODO: replace variables in this query, too
71
+
72
+ elsif @config['type'] == 'query' && !@config['query'].empty?
73
+ # TODO: replace variables in query, execute it and evaluate the results as if they were normally selected
74
+ # "multiFormat": contains variable replacement in "query", e.g. 'regex values' or 'glob'
75
+ # "datasource": contains name of datasource
72
76
  return @config['query']
73
- # TODO: handle 'All' value properly for query attributes
77
+
74
78
  else
75
- # TODO: how to handle All selection properly at this point?
79
+ # TODO: add support for variable type: 'datasource' and 'adhoc'
76
80
  end
77
81
  end
78
82
 
79
83
  case format
80
84
  when 'csv'
81
- return value.join(',').to_s if multi?
85
+ return value.join(',').to_s if multi? && value.is_a?(Array)
82
86
 
83
87
  value.to_s
84
88
 
85
89
  when 'distributed'
86
- return value.join(",#{name}=") if multi?
90
+ return value.join(",#{name}=") if multi? && value.is_a?(Array)
87
91
 
88
92
  value
89
93
  when 'doublequote'
90
- if multi?
94
+ if multi? && value.is_a?(Array)
91
95
  value = value.map { |item| "\"#{item.gsub(/\\/, '\\\\').gsub(/"/, '\\"')}\"" }
92
96
  return value.join(',')
93
97
  end
94
98
  "\"#{value.gsub(/"/, '\\"')}\""
95
99
 
96
100
  when 'json'
97
- if multi?
101
+ if multi? && value.is_a?(Array)
98
102
  value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\\0')}\"" }
99
103
  return "[#{value.join(',')}]"
100
104
  end
101
105
  "\"#{value.gsub(/"/, '\\"')}\""
102
106
 
103
107
  when 'percentencode'
104
- value = "{#{value.join(',')}}" if multi?
108
+ value = "{#{value.join(',')}}" if multi? && value.is_a?(Array)
105
109
  ERB::Util.url_encode(value)
106
110
 
107
111
  when 'pipe'
108
- return value.join('|') if multi?
112
+ return value.join('|') if multi? && value.is_a?(Array)
109
113
 
110
114
  value
111
115
 
112
116
  when 'raw'
113
- return "{#{value.join(',')}}" if multi?
117
+ return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
114
118
 
115
119
  value
116
120
 
117
121
  when 'regex'
118
- if multi?
122
+ if multi? && value.is_a?(Array)
119
123
  value = value.map { |item| item.gsub(%r{[/$.|\\]}, '\\\\\0') }
120
124
  return "(#{value.join('|')})"
121
125
  end
122
126
  value.gsub(%r{[/$.|\\]}, '\\\\\0')
123
127
 
124
128
  when 'singlequote'
125
- if multi?
129
+ if multi? && value.is_a?(Array)
126
130
  value = value.map { |item| "'#{item.gsub(/'/, '\\\\\0')}'" }
127
131
  return value.join(',')
128
132
  end
129
133
  "'#{value.gsub(/'/, '\\\\\0')}'"
130
134
 
131
135
  when 'sqlstring'
132
- if multi?
136
+ if multi? && value.is_a?(Array)
133
137
  value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
134
138
  return value.join(',')
135
139
  end
136
140
  "'#{value.gsub(/'/, "''")}'"
137
141
 
138
142
  when 'lucene'
139
- if multi?
143
+ if multi? && value.is_a?(Array)
140
144
  value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\\0')}\"" }
141
145
  return "(#{value.join(' OR ')})"
142
146
  end
143
147
  value.gsub(%r{[" |=/\\]}, '\\\\\0')
144
148
 
145
149
  when /^date(?::(?<format>.*))?$/
146
- # TODO: validate how grafana handles multivariables with date format
150
+ # TODO: grafana does not seem to allow multivariables with date format - so properly handle here as well
147
151
  get_date_formatted(value, Regexp.last_match(1))
148
152
 
149
153
  when ''
150
154
  # default
151
- if multi?
155
+ if multi? && value.is_a?(Array)
152
156
  value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
153
157
  return value.join(',')
154
158
  end
@@ -162,7 +166,7 @@ module Grafana
162
166
  end
163
167
  end
164
168
 
165
- # @return [Boolean] true, if the value can contain multiple selections, i.e. is an Array
169
+ # @return [Boolean] true, if the value can contain multiple selections, i.e. can contain an Array
166
170
  def multi?
167
171
  return @config['multi'] unless @config['multi'].nil?
168
172
 
@@ -196,6 +200,8 @@ module Grafana
196
200
  until work_string.empty?
197
201
  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
202
  h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
203
+
204
+ # TODO: add test for sub! and switch to non-modifying frozen string action
199
205
  if tmp.empty?
200
206
  matches << work_string[0]
201
207
  work_string.sub!(/^#{work_string[0]}/, '')
@@ -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
  #
@@ -89,6 +92,7 @@ module GrafanaReporter
89
92
  end
90
93
 
91
94
  raise DatasourceRequestInvalidReturnValueError.new(@datasource, @result) unless datasource_response_valid?
95
+
92
96
  post_process
93
97
  @result
94
98
  end
@@ -216,7 +220,6 @@ module GrafanaReporter
216
220
  # @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#request}.
217
221
  # @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
218
222
  # @return [Hash] query result with replaced values
219
- # TODO: make sure that caught errors are also visible in logger
220
223
  def replace_values(result, configs)
221
224
  return result if configs.empty?
222
225
 
@@ -231,8 +234,11 @@ module GrafanaReporter
231
234
 
232
235
  k = arr[0]
233
236
  v = arr[1]
234
- k.gsub!(/\\([:,])/, '\1')
235
- 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
+
236
242
  result[:content].map do |row|
237
243
  (row.length - 1).downto 0 do |i|
238
244
  if cols.include?(i + 1) || cols.empty?
@@ -267,6 +273,7 @@ module GrafanaReporter
267
273
  end
268
274
  end
269
275
  rescue StandardError => e
276
+ @grafana.logger.error(e.message)
270
277
  row[i] = e.message
271
278
  end
272
279
  end
@@ -284,22 +291,32 @@ module GrafanaReporter
284
291
  result
285
292
  end
286
293
 
287
- # Used to build a output format matching the requested report format.
294
+ # Used to build a table output in a custom format.
288
295
  # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
289
296
  # @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
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
295
302
  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)
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' or register your own implementation "\
311
+ "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
302
317
  end
318
+
319
+ AbstractTableFormatStrategy.get(opts[:table_formatter].raw_value).format(result, opts[:include_headline].raw_value.downcase == 'true')
303
320
  end
304
321
 
305
322
  # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
@@ -317,9 +334,10 @@ module GrafanaReporter
317
334
  # @param timezone [Grafana::Variable] timezone to use, if not system timezone
318
335
  # @return [String] translated date as timestamp string
319
336
  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
337
+ @grafana.logger.warn("#translate_date has been called without 'report_time' - using current time as fallback.") unless report_time
321
338
  report_time ||= ::Grafana::Variable.new(Time.now.to_s)
322
339
  orig_date = orig_date.raw_value if orig_date.is_a?(Grafana::Variable)
340
+
323
341
  return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
324
342
  return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
325
343
  return orig_date if orig_date =~ /^\d+$/
@@ -387,10 +405,10 @@ module GrafanaReporter
387
405
  def datasource_response_valid?
388
406
  return false if @result.nil?
389
407
  return false unless @result.is_a?(Hash)
390
- # TODO: check if it should be ok if a datasource request returns an empty hash only
408
+ # TODO: compare how empty valid responses look like in grafana
391
409
  return true if @result.empty?
392
- return false unless @result.has_key?(:header)
393
- return false unless @result.has_key?(:content)
410
+ return false unless @result.key?(:header)
411
+ return false unless @result.key?(:content)
394
412
  return false unless @result[:header].is_a?(Array)
395
413
  return false unless @result[:content].is_a?(Array)
396
414
 
@@ -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,14 @@ 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
+ build
147
147
  rescue MissingTemplateError => e
148
148
  @logger.error(e.message)
149
149
  @error = [e.message]
@@ -186,6 +186,18 @@ module GrafanaReporter
186
186
  raise NotImplementedError
187
187
  end
188
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
+
189
201
  private
190
202
 
191
203
  # Called, if the report generation has died with an error.
@@ -207,6 +219,7 @@ module GrafanaReporter
207
219
  def done!
208
220
  return if @done
209
221
 
222
+ @destination_file_or_path.close if @destination_file_or_path.is_a?(File)
210
223
  @done = true
211
224
  @end_time = Time.new
212
225
  @start_time ||= @end_time
@@ -0,0 +1,34 @@
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
+ # @abstract
27
+ # @param column [Array] datasource table result
28
+ # @param include_headline [Boolean] true, if headline should be included in result
29
+ # @return [String] formatted in table format
30
+ def format(content, include_headline)
31
+ raise NotImplementedError
32
+ end
33
+ end
34
+ end