ruby-grafana-reporter 0.1.7 → 0.2.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +166 -339
  3. data/bin/ruby-grafana-reporter +5 -4
  4. data/lib/VERSION.rb +5 -3
  5. data/lib/grafana/abstract_panel_query.rb +22 -20
  6. data/lib/grafana/abstract_query.rb +132 -127
  7. data/lib/grafana/abstract_sql_query.rb +51 -42
  8. data/lib/grafana/dashboard.rb +77 -66
  9. data/lib/grafana/errors.rb +66 -61
  10. data/lib/grafana/grafana.rb +130 -131
  11. data/lib/grafana/panel.rb +41 -39
  12. data/lib/grafana/panel_image_query.rb +52 -49
  13. data/lib/grafana/variable.rb +217 -259
  14. data/lib/grafana_reporter/abstract_report.rb +112 -109
  15. data/lib/grafana_reporter/application/application.rb +404 -229
  16. data/lib/grafana_reporter/application/errors.rb +33 -30
  17. data/lib/grafana_reporter/application/webservice.rb +231 -0
  18. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +104 -99
  19. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +99 -96
  20. data/lib/grafana_reporter/asciidoctor/errors.rb +40 -37
  21. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +92 -86
  22. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +91 -86
  23. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +69 -67
  24. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +68 -65
  25. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +61 -58
  26. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +78 -75
  27. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +73 -70
  28. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +20 -18
  29. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +43 -41
  30. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +70 -67
  31. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +66 -65
  32. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +61 -57
  33. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +34 -32
  34. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +25 -23
  35. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +44 -43
  36. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +38 -36
  37. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +310 -309
  38. data/lib/grafana_reporter/asciidoctor/report.rb +177 -159
  39. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +37 -34
  40. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +39 -32
  41. data/lib/grafana_reporter/configuration.rb +257 -326
  42. data/lib/grafana_reporter/errors.rb +48 -38
  43. data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
  44. data/lib/ruby-grafana-reporter.rb +29 -27
  45. metadata +10 -23
@@ -1,36 +1,38 @@
1
- require_relative 'sql_table_query'
2
-
3
- module GrafanaReporter
4
- module Asciidoctor
5
- # (see SqlTableQuery)
6
- #
7
- # The SQL query as well as the datasource configuration are thereby captured from a
8
- # {Grafana::Panel}.
9
- class PanelTableQuery < SqlTableQuery
10
- include QueryMixin
11
-
12
- # @param panel [Grafana::Panel] panel which contains the query
13
- # @param query_letter [String] letter of the query within the panel, which shall be used, e.g. +C+
14
- def initialize(panel, query_letter)
15
- super(nil, nil)
16
- @panel = panel
17
- @query_letter = query_letter
18
- extract_dashboard_variables(@panel.dashboard)
19
- end
20
-
21
- # Retrieves the SQL query and the configured datasource from the panel.
22
- # @see Grafana::AbstractSqlQuery#pre_process
23
- # @param grafana [Grafana::Grafana] grafana instance against which the query shall be executed
24
- # @return [void]
25
- def pre_process(grafana)
26
- @sql = @panel.query(@query_letter)
27
- # resolve datasource name
28
- @datasource = @panel.field('datasource')
29
- @datasource_id = grafana.datasource_id(@datasource)
30
- super(grafana)
31
- @from = translate_date(@from, @variables['grafana-report-timestamp'], false)
32
- @to = translate_date(@to, @variables['grafana-report-timestamp'], true)
33
- end
34
- end
35
- end
36
- end
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'sql_table_query'
4
+
5
+ module GrafanaReporter
6
+ module Asciidoctor
7
+ # (see SqlTableQuery)
8
+ #
9
+ # The SQL query as well as the datasource configuration are thereby captured from a
10
+ # {Grafana::Panel}.
11
+ class PanelTableQuery < SqlTableQuery
12
+ include QueryMixin
13
+
14
+ # @param panel [Grafana::Panel] panel which contains the query
15
+ # @param query_letter [String] letter of the query within the panel, which shall be used, e.g. +C+
16
+ def initialize(panel, query_letter)
17
+ super(nil, nil)
18
+ @panel = panel
19
+ @query_letter = query_letter
20
+ extract_dashboard_variables(@panel.dashboard)
21
+ end
22
+
23
+ # Retrieves the SQL query and the configured datasource from the panel.
24
+ # @see Grafana::AbstractSqlQuery#pre_process
25
+ # @param grafana [Grafana::Grafana] grafana instance against which the query shall be executed
26
+ # @return [void]
27
+ def pre_process(grafana)
28
+ @sql = @panel.query(@query_letter)
29
+ # resolve datasource name
30
+ @datasource = @panel.field('datasource')
31
+ @datasource_id = grafana.datasource_id(@datasource)
32
+ super(grafana)
33
+ @from = translate_date(@from, @variables['grafana-report-timestamp'], false)
34
+ @to = translate_date(@to, @variables['grafana-report-timestamp'], true)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -1,309 +1,310 @@
1
- module GrafanaReporter
2
- module Asciidoctor
3
- # This mixin contains several common methods, which can be used within the queries.
4
- module QueryMixin
5
- # Merges the given hashes to the current object by using the {Grafana::AbstractQuery#merge_variables} method.
6
- # It respects the priorities of the hashes and the object and allows only valid variables to be passed.
7
- # @param document_hash [Hash] variables from report template level
8
- # @param item_hash [Hash] variables from item configuration level, i.e. specific call, which may override document
9
- # @return [void]
10
- def merge_hash_variables(document_hash, item_hash)
11
- merge_variables(document_hash.select { |k, _v| k =~ /^var-/ || k == 'grafana-report-timestamp' }.transform_values { |item| ::Grafana::Variable.new(item) })
12
- # TODO: add documentation for transpose, column_divider and row_divider
13
- merge_variables(item_hash.select { |k, _v| k =~ /^var-/ || k =~ /^render-/ || k =~ /filter_columns|format|replace_values_.*|transpose|column_divider|row_divider/ }.transform_values { |item| ::Grafana::Variable.new(item) })
14
- # TODO: add documentation for timeout and grafana-default-timeout
15
- self.timeout = item_hash['timeout'] || document_hash['grafana-default-timeout'] || timeout
16
- self.from = item_hash['from'] || document_hash['from'] || from
17
- self.to = item_hash['to'] || document_hash['to'] || to
18
- end
19
-
20
- # Formats the SQL results returned from grafana to an easier to use format.
21
- #
22
- # The result is being formatted as stated below:
23
- #
24
- # {
25
- # :header => [column_title_1, column_title_2],
26
- # :content => [
27
- # [row_1_column_1, row_1_column_2],
28
- # [row_2_column_1, row_2_column_2]
29
- # ]
30
- # }
31
- # @param raw_result [Hash] query result hash from grafana
32
- # @return [Hash] sql result formatted as stated above
33
- # TODO: support series query results properly
34
- def preformat_sql_result(raw_result)
35
- results = {}
36
- results.default = []
37
-
38
- JSON.parse(raw_result)['results'].values.each do |query_result|
39
- if query_result.key?('error')
40
- results[:header] = results[:header] << ['SQL Error']
41
- results[:content] = [[query_result['error']]]
42
- elsif query_result['tables']
43
- query_result['tables'].each do |table|
44
- results[:header] = results[:header] << table['columns'].map { |header| header['text'] }
45
- results[:content] = table['rows']
46
- end
47
- else
48
- # TODO: add test for series results
49
- results[:header] = 'time'
50
- query_result['series'].each do |table|
51
- results[:header] << table[:name]
52
- results[:content] = []
53
- content_position = results[:header].length - 1
54
- table[:points].each do |point|
55
- result = []
56
- result << point[1]
57
- (content_position - 1).times {result << nil}
58
- result << point[0]
59
- results[:content][0] << result
60
- end
61
- end
62
- end
63
- end
64
-
65
- results
66
- end
67
-
68
- # Transposes the given result.
69
- #
70
- # NOTE: Only the +:content+ of the given result hash is transposed. The +:header+ is ignored.
71
- #
72
- # @param result [Hash] preformatted sql hash, (see {#preformat_sql_result})
73
- # @param transpose_variable [Grafana::Variable] true, if the result hash shall be transposed
74
- # @return [Hash] transposed query result
75
- def transpose(result, transpose_variable)
76
- return result unless transpose_variable
77
- return result unless transpose_variable.raw_value == 'true'
78
-
79
- result[:content] = result[:content].transpose
80
-
81
- result
82
- end
83
-
84
- # Filters columns out of the query result.
85
- #
86
- # Multiple columns may be filtered. Therefore the column titles have to be named in the
87
- # {Grafana::Variable#raw_value} and have to be separated by +,+ (comma).
88
- # @param result [Hash] preformatted sql hash, (see {#preformat_sql_result})
89
- # @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result
90
- # @return [Hash] filtered query result
91
- def filter_columns(result, filter_columns_variable)
92
- return result unless filter_columns_variable
93
-
94
- filter_columns = filter_columns_variable.raw_value
95
- filter_columns.split(',').each do |filter_column|
96
- pos = result[:header][0].index(filter_column)
97
-
98
- unless pos.nil?
99
- result[:header][0].delete_at(pos)
100
- result[:content].each { |row| row.delete_at(pos) }
101
- end
102
- end
103
-
104
- result
105
- end
106
-
107
- # Uses the {Kernel#format} method to format values in the query results.
108
- #
109
- # The formatting will be applied separately for every column. Therefore the column formats have to be named
110
- # in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for
111
- # a column, no change will happen.
112
- # @param result [Hash] preformatted sql hash, (see {#preformat_sql_result})
113
- # @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result
114
- # @return [Hash] formatted query result
115
- # TODO: make sure that caught errors are also visible in logger
116
- def format_columns(result, formats)
117
- return result unless formats
118
-
119
- formats.text.split(',').each_index do |i|
120
- format = formats.text.split(',')[i]
121
- next if format.empty?
122
-
123
- result[:content].map do |row|
124
- next unless row.length > i
125
-
126
- begin
127
- row[i] = format % row[i] if row[i]
128
- rescue StandardError => e
129
- row[i] = e.message
130
- end
131
- end
132
- end
133
- result
134
- end
135
-
136
- # Used to replace values in a query result according given configurations.
137
- #
138
- # The given variables will be applied to an appropriate column, depending
139
- # on the naming of the variable. The variable name ending specifies the column,
140
- # e.g. a variable named +replace_values_2+ will be applied to the second column.
141
- #
142
- # The {Grafana::Variable#text} needs to contain the replace specification.
143
- # Multiple replacements can be specified by separating them with +,+. If a
144
- # literal comma is needed, it can be escaped with a backslash: +\\,+.
145
- #
146
- # The rule will be separated from the replacement text with a colon +:+.
147
- # If a literal colon is wanted, it can be escaped with a backslash: +\\:+.
148
- #
149
- # Examples:
150
- # - Basic string replacement
151
- # MyTest:ThisValue
152
- # will replace all occurences of the text 'MyTest' with 'ThisValue'.
153
- # - Number comparison
154
- # <=10:OK
155
- # will replace all values smaller or equal to 10 with 'OK'.
156
- # - Regular expression
157
- # ^[^ ]\\+ (\d+)$:\1 is the answer
158
- # will replace all values matching the pattern, e.g. 'answerToAllQuestions 42' to
159
- # '42 is the answer'. Important to know: the regular expressions always have to start
160
- # with +^+ and end with +$+, i.e. the expression itself always has to match
161
- # the whole content in one field.
162
- # @param result [Hash] preformatted query result (see {#preformat_sql_result}.
163
- # @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
164
- # @return [Hash] query result with replaced values
165
- # TODO: make sure that caught errors are also visible in logger
166
- def replace_values(result, configs)
167
- return result if configs.empty?
168
-
169
- configs.each do |key, formats|
170
- cols = key.split('_')[2..-1].map(&:to_i)
171
-
172
- formats.text.split(/(?<!\\),/).each_index do |j|
173
- format = formats.text.split(/(?<!\\),/)[j]
174
-
175
- arr = format.split(/(?<!\\):/)
176
- raise MalformedReplaceValuesStatementError, format if arr.length != 2
177
-
178
- k = arr[0]
179
- v = arr[1]
180
- k.gsub!(/\\([:,])/, '\1')
181
- v.gsub!(/\\([:,])/, '\1')
182
- result[:content].map do |row|
183
- (row.length - 1).downto 0 do |i|
184
- if cols.include?(i + 1) || cols.empty?
185
-
186
- # handle regular expressions
187
- if k.start_with?('^') && k.end_with?('$')
188
- begin
189
- row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
190
- rescue StandardError => e
191
- row[i] = e.message
192
- end
193
-
194
- # handle value comparisons
195
- elsif match = k.match(/^ *(?<operator>[<>]=?|<>|=) *(?<number>[+-]?\d+(?:\.\d+)?)$/)
196
- skip = false
197
- begin
198
- val = Float(row[i])
199
- rescue
200
- # value cannot be converted to number, simply ignore it as the comparison does not fit here
201
- skip = true
202
- end
203
-
204
- if not skip
205
- begin
206
- op = match[:operator].gsub(/^=$/, '==').gsub(/^<>$/, '!=')
207
- if val.public_send(op.to_sym, Float(match[:number]))
208
- row[i] = if v.include?('\\1')
209
- v.gsub(/\\1/, row[i].to_s)
210
- else
211
- v
212
- end
213
- end
214
- rescue StandardError => e
215
- row[i] = e.message
216
- end
217
- end
218
-
219
- # handle as normal comparison
220
- else
221
- row[i] = v if row[i].to_s == k
222
- end
223
- end
224
- end
225
- end
226
- end
227
- end
228
-
229
- result
230
- end
231
-
232
- # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
233
- # correct timestamp. Reason is that grafana does this in the frontend, which we have
234
- # to emulate here for the reporter.
235
- #
236
- # Additionally providing this function the +report_time+ assures that all queries
237
- # rendered within one report will use _exactly_ the same timestamp in those relative
238
- # times, i.e. there shouldn't appear any time differences, no matter how long the
239
- # report is running.
240
- # @param orig_date [String] time string provided by grafana, usually +from+ or +to+.
241
- # @param report_time [Grafana::Variable] report start time
242
- # @param is_to_time [Boolean] true, if the time should be calculated for +to+, false if it shall be calculated for +from+
243
- # @return [String] translated date as timestamp string
244
- def translate_date(orig_date, report_time, is_to_time)
245
- report_time ||= Variable.new(Time.now.to_s)
246
- return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
247
- return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
248
- return orig_date if orig_date =~ /^\d+$/
249
-
250
- # replace grafana from and to values using now, now-2d etc.
251
- date_splitted = orig_date.match(%r{^(?<now>now)(?:-(?<sub_count>\d+)?(?<sub_unit>[smhdwMy]?))?(?:/(?<fit>[smhdwMy]))?$})
252
- raise TimeRangeUnknownError, orig_date unless date_splitted
253
-
254
- date = DateTime.parse(report_time.raw_value)
255
- # substract specified time
256
- count = 1
257
- count = date_splitted[:sub_count].to_i if date_splitted[:sub_count]
258
- case date_splitted[:sub_unit]
259
- when 's'
260
- date = (date.to_time - (count * 1)).to_datetime
261
- when 'm'
262
- date = (date.to_time - (count * 60)).to_datetime
263
- when 'h'
264
- date = (date.to_time - (count * 60 * 60)).to_datetime
265
- when 'd'
266
- date = date.prev_day(count)
267
- when 'w'
268
- date = date.prev_day(count * 7)
269
- when 'M'
270
- date = date.prev_month(count)
271
- when 'y'
272
- date = date.prev_year(count)
273
- end
274
-
275
- # fit to specified time frame
276
- case date_splitted[:fit]
277
- when 's'
278
- date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, date.sec, date.zone)
279
- date = (date.to_time + 1).to_datetime if is_to_time
280
- when 'm'
281
- date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, 0, date.zone)
282
- date = (date.to_time + 60).to_datetime if is_to_time
283
- when 'h'
284
- date = DateTime.new(date.year, date.month, date.day, date.hour, 0, 0, date.zone)
285
- date = (date.to_time + 60 * 60).to_datetime if is_to_time
286
- when 'd'
287
- date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
288
- date = date.next_day(1) if is_to_time
289
- when 'w'
290
- date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
291
- date = if date.wday.zero?
292
- date.prev_day(7)
293
- else
294
- date.prev_day(date.wday - 1)
295
- end
296
- date = date.next_day(7) if is_to_time
297
- when 'M'
298
- date = DateTime.new(date.year, date.month, 1, 0, 0, 0, date.zone)
299
- date = date.next_month if is_to_time
300
- when 'y'
301
- date = DateTime.new(date.year, 1, 1, 0, 0, 0, date.zone)
302
- date = date.next_year if is_to_time
303
- end
304
-
305
- (date.to_time.to_i * 1000).to_s
306
- end
307
- end
308
- end
309
- end
1
+ module GrafanaReporter
2
+ module Asciidoctor
3
+ # This mixin contains several common methods, which can be used within the queries.
4
+ module QueryMixin
5
+ # Merges the given hashes to the current object by using the {Grafana::AbstractQuery#merge_variables} method.
6
+ # It respects the priorities of the hashes and the object and allows only valid variables to be passed.
7
+ # @param document_hash [Hash] variables from report template level
8
+ # @param item_hash [Hash] variables from item configuration level, i.e. specific call, which may override document
9
+ # @return [void]
10
+ def merge_hash_variables(document_hash, item_hash)
11
+ merge_variables(document_hash.select { |k, _v| k =~ /^var-/ || k == 'grafana-report-timestamp' }.transform_values { |item| ::Grafana::Variable.new(item) })
12
+ # TODO: add documentation for transpose, column_divider and row_divider
13
+ merge_variables(item_hash.select { |k, _v| k =~ /^var-/ || k =~ /^render-/ || k =~ /filter_columns|format|replace_values_.*|transpose|column_divider|row_divider/ }.transform_values { |item| ::Grafana::Variable.new(item) })
14
+ # TODO: add documentation for timeout and grafana-default-timeout
15
+ self.timeout = item_hash['timeout'] || document_hash['grafana-default-timeout'] || timeout
16
+ self.from = item_hash['from'] || document_hash['from'] || from
17
+ self.to = item_hash['to'] || document_hash['to'] || to
18
+ end
19
+
20
+ # Formats the SQL results returned from grafana to an easier to use format.
21
+ #
22
+ # The result is being formatted as stated below:
23
+ #
24
+ # {
25
+ # :header => [column_title_1, column_title_2],
26
+ # :content => [
27
+ # [row_1_column_1, row_1_column_2],
28
+ # [row_2_column_1, row_2_column_2]
29
+ # ]
30
+ # }
31
+ # @param raw_result [Hash] query result hash from grafana
32
+ # @return [Hash] sql result formatted as stated above
33
+ # TODO: support series query results properly
34
+ def preformat_sql_result(raw_result)
35
+ results = {}
36
+ results.default = []
37
+
38
+ JSON.parse(raw_result)['results'].each_value do |query_result|
39
+ if query_result.key?('error')
40
+ results[:header] = results[:header] << ['SQL Error']
41
+ results[:content] = [[query_result['error']]]
42
+ elsif query_result['tables']
43
+ query_result['tables'].each do |table|
44
+ results[:header] = results[:header] << table['columns'].map { |header| header['text'] }
45
+ results[:content] = table['rows']
46
+ end
47
+ else
48
+ # TODO: add test for series results
49
+ results[:header] = 'time'
50
+ query_result['series'].each do |table|
51
+ results[:header] << table[:name]
52
+ results[:content] = []
53
+ content_position = results[:header].length - 1
54
+ table[:points].each do |point|
55
+ result = []
56
+ result << point[1]
57
+ (content_position - 1).times { result << nil }
58
+ result << point[0]
59
+ results[:content][0] << result
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ results
66
+ end
67
+
68
+ # Transposes the given result.
69
+ #
70
+ # NOTE: Only the +:content+ of the given result hash is transposed. The +:header+ is ignored.
71
+ #
72
+ # @param result [Hash] preformatted sql hash, (see {#preformat_sql_result})
73
+ # @param transpose_variable [Grafana::Variable] true, if the result hash shall be transposed
74
+ # @return [Hash] transposed query result
75
+ def transpose(result, transpose_variable)
76
+ return result unless transpose_variable
77
+ return result unless transpose_variable.raw_value == 'true'
78
+
79
+ result[:content] = result[:content].transpose
80
+
81
+ result
82
+ end
83
+
84
+ # Filters columns out of the query result.
85
+ #
86
+ # Multiple columns may be filtered. Therefore the column titles have to be named in the
87
+ # {Grafana::Variable#raw_value} and have to be separated by +,+ (comma).
88
+ # @param result [Hash] preformatted sql hash, (see {#preformat_sql_result})
89
+ # @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result
90
+ # @return [Hash] filtered query result
91
+ def filter_columns(result, filter_columns_variable)
92
+ return result unless filter_columns_variable
93
+
94
+ filter_columns = filter_columns_variable.raw_value
95
+ filter_columns.split(',').each do |filter_column|
96
+ pos = result[:header][0].index(filter_column)
97
+
98
+ unless pos.nil?
99
+ result[:header][0].delete_at(pos)
100
+ result[:content].each { |row| row.delete_at(pos) }
101
+ end
102
+ end
103
+
104
+ result
105
+ end
106
+
107
+ # Uses the {Kernel#format} method to format values in the query results.
108
+ #
109
+ # The formatting will be applied separately for every column. Therefore the column formats have to be named
110
+ # in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for
111
+ # a column, no change will happen.
112
+ # @param result [Hash] preformatted sql hash, (see {#preformat_sql_result})
113
+ # @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result
114
+ # @return [Hash] formatted query result
115
+ # TODO: make sure that caught errors are also visible in logger
116
+ def format_columns(result, formats)
117
+ return result unless formats
118
+
119
+ formats.text.split(',').each_index do |i|
120
+ format = formats.text.split(',')[i]
121
+ next if format.empty?
122
+
123
+ result[:content].map do |row|
124
+ next unless row.length > i
125
+
126
+ begin
127
+ row[i] = format % row[i] if row[i]
128
+ rescue StandardError => e
129
+ row[i] = e.message
130
+ end
131
+ end
132
+ end
133
+ result
134
+ end
135
+
136
+ # Used to replace values in a query result according given configurations.
137
+ #
138
+ # The given variables will be applied to an appropriate column, depending
139
+ # on the naming of the variable. The variable name ending specifies the column,
140
+ # e.g. a variable named +replace_values_2+ will be applied to the second column.
141
+ #
142
+ # The {Grafana::Variable#text} needs to contain the replace specification.
143
+ # Multiple replacements can be specified by separating them with +,+. If a
144
+ # literal comma is needed, it can be escaped with a backslash: +\\,+.
145
+ #
146
+ # The rule will be separated from the replacement text with a colon +:+.
147
+ # If a literal colon is wanted, it can be escaped with a backslash: +\\:+.
148
+ #
149
+ # Examples:
150
+ # - Basic string replacement
151
+ # MyTest:ThisValue
152
+ # will replace all occurences of the text 'MyTest' with 'ThisValue'.
153
+ # - Number comparison
154
+ # <=10:OK
155
+ # will replace all values smaller or equal to 10 with 'OK'.
156
+ # - Regular expression
157
+ # ^[^ ]\\+ (\d+)$:\1 is the answer
158
+ # will replace all values matching the pattern, e.g. 'answerToAllQuestions 42' to
159
+ # '42 is the answer'. Important to know: the regular expressions always have to start
160
+ # with +^+ and end with +$+, i.e. the expression itself always has to match
161
+ # the whole content in one field.
162
+ # @param result [Hash] preformatted query result (see {#preformat_sql_result}.
163
+ # @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
164
+ # @return [Hash] query result with replaced values
165
+ # TODO: make sure that caught errors are also visible in logger
166
+ def replace_values(result, configs)
167
+ return result if configs.empty?
168
+
169
+ configs.each do |key, formats|
170
+ cols = key.split('_')[2..-1].map(&:to_i)
171
+
172
+ formats.text.split(/(?<!\\),/).each_index do |j|
173
+ format = formats.text.split(/(?<!\\),/)[j]
174
+
175
+ arr = format.split(/(?<!\\):/)
176
+ raise MalformedReplaceValuesStatementError, format if arr.length != 2
177
+
178
+ k = arr[0]
179
+ v = arr[1]
180
+ k.gsub!(/\\([:,])/, '\1')
181
+ v.gsub!(/\\([:,])/, '\1')
182
+ result[:content].map do |row|
183
+ (row.length - 1).downto 0 do |i|
184
+ if cols.include?(i + 1) || cols.empty?
185
+
186
+ # handle regular expressions
187
+ if k.start_with?('^') && k.end_with?('$')
188
+ begin
189
+ row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
190
+ rescue StandardError => e
191
+ row[i] = e.message
192
+ end
193
+
194
+ # handle value comparisons
195
+ elsif (match = k.match(/^ *(?<operator>[<>]=?|<>|=) *(?<number>[+-]?\d+(?:\.\d+)?)$/))
196
+ skip = false
197
+ begin
198
+ val = Float(row[i])
199
+ rescue StandardError
200
+ # value cannot be converted to number, simply ignore it as the comparison does not fit here
201
+ skip = true
202
+ end
203
+
204
+ unless skip
205
+ begin
206
+ op = match[:operator].gsub(/^=$/, '==').gsub(/^<>$/, '!=')
207
+ if val.public_send(op.to_sym, Float(match[:number]))
208
+ row[i] = if v.include?('\\1')
209
+ v.gsub(/\\1/, row[i].to_s)
210
+ else
211
+ v
212
+ end
213
+ end
214
+ rescue StandardError => e
215
+ row[i] = e.message
216
+ end
217
+ end
218
+
219
+ # handle as normal comparison
220
+ elsif row[i].to_s == k
221
+ row[i] = v
222
+ end
223
+ end
224
+ end
225
+ end
226
+ end
227
+ end
228
+
229
+ result
230
+ end
231
+
232
+ # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
233
+ # correct timestamp. Reason is that grafana does this in the frontend, which we have
234
+ # to emulate here for the reporter.
235
+ #
236
+ # Additionally providing this function the +report_time+ assures that all queries
237
+ # rendered within one report will use _exactly_ the same timestamp in those relative
238
+ # times, i.e. there shouldn't appear any time differences, no matter how long the
239
+ # report is running.
240
+ # @param orig_date [String] time string provided by grafana, usually +from+ or +to+.
241
+ # @param report_time [Grafana::Variable] report start time
242
+ # @param is_to_time [Boolean] true, if the time should be calculated for +to+, false if it shall be
243
+ # calculated for +from+
244
+ # @return [String] translated date as timestamp string
245
+ def translate_date(orig_date, report_time, is_to_time)
246
+ report_time ||= Variable.new(Time.now.to_s)
247
+ return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
248
+ return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
249
+ return orig_date if orig_date =~ /^\d+$/
250
+
251
+ # replace grafana from and to values using now, now-2d etc.
252
+ date_splitted = orig_date.match(%r{^(?<now>now)(?:-(?<sub_count>\d+)?(?<sub_unit>[smhdwMy]?))?(?:/(?<fit>[smhdwMy]))?$})
253
+ raise TimeRangeUnknownError, orig_date unless date_splitted
254
+
255
+ date = DateTime.parse(report_time.raw_value)
256
+ # substract specified time
257
+ count = 1
258
+ count = date_splitted[:sub_count].to_i if date_splitted[:sub_count]
259
+ case date_splitted[:sub_unit]
260
+ when 's'
261
+ date = (date.to_time - (count * 1)).to_datetime
262
+ when 'm'
263
+ date = (date.to_time - (count * 60)).to_datetime
264
+ when 'h'
265
+ date = (date.to_time - (count * 60 * 60)).to_datetime
266
+ when 'd'
267
+ date = date.prev_day(count)
268
+ when 'w'
269
+ date = date.prev_day(count * 7)
270
+ when 'M'
271
+ date = date.prev_month(count)
272
+ when 'y'
273
+ date = date.prev_year(count)
274
+ end
275
+
276
+ # fit to specified time frame
277
+ case date_splitted[:fit]
278
+ when 's'
279
+ date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, date.sec, date.zone)
280
+ date = (date.to_time + 1).to_datetime if is_to_time
281
+ when 'm'
282
+ date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, 0, date.zone)
283
+ date = (date.to_time + 60).to_datetime if is_to_time
284
+ when 'h'
285
+ date = DateTime.new(date.year, date.month, date.day, date.hour, 0, 0, date.zone)
286
+ date = (date.to_time + 60 * 60).to_datetime if is_to_time
287
+ when 'd'
288
+ date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
289
+ date = date.next_day(1) if is_to_time
290
+ when 'w'
291
+ date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
292
+ date = if date.wday.zero?
293
+ date.prev_day(7)
294
+ else
295
+ date.prev_day(date.wday - 1)
296
+ end
297
+ date = date.next_day(7) if is_to_time
298
+ when 'M'
299
+ date = DateTime.new(date.year, date.month, 1, 0, 0, 0, date.zone)
300
+ date = date.next_month if is_to_time
301
+ when 'y'
302
+ date = DateTime.new(date.year, 1, 1, 0, 0, 0, date.zone)
303
+ date = date.next_year if is_to_time
304
+ end
305
+
306
+ (date.to_time.to_i * 1000).to_s
307
+ end
308
+ end
309
+ end
310
+ end