ruby-grafana-reporter 0.2.0 → 0.4.1

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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +105 -86
  3. data/bin/ruby-grafana-reporter +5 -5
  4. data/lib/VERSION.rb +3 -2
  5. data/lib/grafana/abstract_datasource.rb +136 -0
  6. data/lib/grafana/dashboard.rb +21 -23
  7. data/lib/grafana/errors.rb +8 -1
  8. data/lib/grafana/grafana.rb +61 -65
  9. data/lib/grafana/grafana_alerts_datasource.rb +57 -0
  10. data/lib/grafana/grafana_annotations_datasource.rb +56 -0
  11. data/lib/grafana/grafana_property_datasource.rb +25 -0
  12. data/lib/grafana/graphite_datasource.rb +50 -0
  13. data/lib/grafana/image_rendering_datasource.rb +44 -0
  14. data/lib/grafana/panel.rb +9 -3
  15. data/lib/grafana/prometheus_datasource.rb +45 -0
  16. data/lib/grafana/sql_datasource.rb +71 -0
  17. data/lib/grafana/unsupported_datasource.rb +7 -0
  18. data/lib/grafana/variable.rb +3 -2
  19. data/lib/grafana/webrequest.rb +71 -0
  20. data/lib/grafana_reporter/abstract_query.rb +359 -0
  21. data/lib/grafana_reporter/abstract_report.rb +119 -17
  22. data/lib/grafana_reporter/alerts_table_query.rb +44 -0
  23. data/lib/grafana_reporter/annotations_table_query.rb +43 -0
  24. data/lib/grafana_reporter/application/application.rb +49 -297
  25. data/lib/grafana_reporter/application/webservice.rb +49 -14
  26. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +90 -0
  27. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +89 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +77 -0
  29. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +79 -0
  30. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +73 -0
  31. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +99 -0
  32. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
  33. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +64 -0
  34. data/lib/grafana_reporter/asciidoctor/report.rb +47 -76
  35. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  36. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  37. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +92 -0
  38. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +88 -0
  39. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  40. data/lib/grafana_reporter/configuration.rb +108 -43
  41. data/lib/grafana_reporter/console_configuration_wizard.rb +311 -0
  42. data/lib/grafana_reporter/demo_report_wizard.rb +101 -0
  43. data/lib/grafana_reporter/erb/report.rb +43 -0
  44. data/lib/grafana_reporter/errors.rb +41 -0
  45. data/lib/grafana_reporter/help.rb +443 -0
  46. data/lib/grafana_reporter/logger/{two_way_logger.rb → two_way_delegate_logger.rb} +1 -1
  47. data/lib/grafana_reporter/panel_image_query.rb +29 -0
  48. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  49. data/lib/grafana_reporter/query_value_query.rb +79 -0
  50. data/lib/grafana_reporter/report_webhook.rb +35 -0
  51. data/lib/ruby_grafana_extension.rb +8 -0
  52. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +13 -0
  53. metadata +47 -43
  54. data/lib/grafana/abstract_panel_query.rb +0 -22
  55. data/lib/grafana/abstract_query.rb +0 -132
  56. data/lib/grafana/abstract_sql_query.rb +0 -51
  57. data/lib/grafana/panel_image_query.rb +0 -52
  58. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -104
  59. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -99
  60. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -40
  61. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -92
  62. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -91
  63. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -69
  64. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -68
  65. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -61
  66. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -78
  67. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -73
  68. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -20
  69. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -43
  70. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -202
  71. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -70
  72. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -66
  73. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -61
  74. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -34
  75. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -25
  76. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -44
  77. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -38
  78. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -310
  79. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -37
  80. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -39
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the interface to Prometheus datasources.
5
+ class PrometheusDatasource < AbstractDatasource
6
+ # @see AbstractDatasource#handles?
7
+ def self.handles?(model)
8
+ tmp = new(model)
9
+ tmp.type == 'prometheus'
10
+ end
11
+
12
+ # +:raw_query+ needs to contain a Prometheus query as String
13
+ # @see AbstractDatasource#request
14
+ def request(query_description)
15
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
16
+
17
+ url = "/api/datasources/proxy/#{id}/api/v1/query_range?"\
18
+ "start=#{query_description[:from]}&end=#{query_description[:to]}"\
19
+ "&query=#{replace_variables(query_description[:raw_query], query_description[:variables])}"
20
+
21
+ webrequest = query_description[:prepared_request]
22
+ webrequest.relative_url = url
23
+ webrequest.options.merge!({ request: Net::HTTP::Get })
24
+
25
+ result = webrequest.execute(query_description[:timeout])
26
+ preformat_response(result.body)
27
+ end
28
+
29
+ # @see AbstractDatasource#raw_query_from_panel_model
30
+ def raw_query_from_panel_model(panel_query_target)
31
+ panel_query_target['expr']
32
+ end
33
+
34
+ private
35
+
36
+ # @see AbstractDatasource#preformat_response
37
+ def preformat_response(response_body)
38
+ # TODO: support multiple metrics as return types
39
+ {
40
+ header: %w[time value],
41
+ content: JSON.parse(response_body)['data']['result'].first['values']
42
+ }
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the interface to all SQL based datasources (tested with PostgreSQL and MariaDB/MySQL).
5
+ class SqlDatasource < AbstractDatasource
6
+ # @see AbstractDatasource#handles?
7
+ def self.handles?(model)
8
+ tmp = new(model)
9
+ tmp.category == 'sql'
10
+ end
11
+
12
+ # +:raw_query+ needs to contain a SQL query as String in the respective database dialect
13
+ # @see AbstractDatasource#request
14
+ def request(query_description)
15
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
16
+
17
+ sql = replace_variables(query_description[:raw_query], query_description[:variables])
18
+ request = {
19
+ body: {
20
+ from: query_description[:from],
21
+ to: query_description[:to],
22
+ queries: [rawSql: prepare_sql(sql), datasourceId: id, format: 'table']
23
+ }.to_json,
24
+ request: Net::HTTP::Post
25
+ }
26
+
27
+ webrequest = query_description[:prepared_request]
28
+ webrequest.relative_url = '/api/tsdb/query'
29
+ webrequest.options.merge!(request)
30
+
31
+ result = webrequest.execute(query_description[:timeout])
32
+ preformat_response(result.body)
33
+ end
34
+
35
+ # @see AbstractDatasource#raw_query_from_panel_model
36
+ def raw_query_from_panel_model(panel_query_target)
37
+ panel_query_target['rawSql']
38
+ end
39
+
40
+ private
41
+
42
+ def preformat_response(response_body)
43
+ results = {}
44
+ results.default = []
45
+
46
+ JSON.parse(response_body)['results'].each_value do |query_result|
47
+ if query_result.key?('error')
48
+ results[:header] = results[:header] << ['SQL Error']
49
+ results[:content] = [[query_result['error']]]
50
+
51
+ elsif query_result['tables']
52
+ query_result['tables'].each do |table|
53
+ results[:header] = results[:header] << table['columns'].map { |header| header['text'] }
54
+ results[:content] = table['rows']
55
+ end
56
+
57
+ end
58
+ end
59
+
60
+ results
61
+ end
62
+
63
+ def prepare_sql(sql)
64
+ # remove comments in query
65
+ sql.gsub!(/--[^\r\n]*(?:[\r\n]+|$)/, ' ')
66
+ sql.gsub!(/\r\n/, ' ')
67
+ sql.gsub!(/\n/, ' ')
68
+ sql
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Dummy class, which is used, if a datasource is currently unsupported.
5
+ class UnsupportedDatasource < AbstractDatasource
6
+ end
7
+ end
@@ -10,6 +10,7 @@ module Grafana
10
10
  class Variable
11
11
  attr_reader :name, :text, :raw_value
12
12
 
13
+ # Translation table to support {https://momentjs.com/docs/#/displaying/}.
13
14
  DATE_MATCHES = { 'M' => '%-m', 'MM' => '%m', 'MMM' => '%b', 'MMMM' => '%B',
14
15
  'D' => '%-d', 'DD' => '%d', 'DDD' => '%-j', 'DDDD' => '%j',
15
16
  'd' => '%w', 'ddd' => '%a', 'dddd' => '%A',
@@ -198,10 +199,10 @@ module Grafana
198
199
  h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
199
200
  if tmp.empty?
200
201
  matches << work_string[0]
201
- work_string.delete_prefix!(work_string[0])
202
+ work_string.sub!(/^#{work_string[0]}/, '')
202
203
  else
203
204
  matches << tmp[0]
204
- work_string.delete_prefix!(tmp[0])
205
+ work_string.sub!(/^#{tmp[0]}/, '')
205
206
  end
206
207
  end
207
208
 
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # This class standardizes all webcalls. Key functionality is to properly support HTTPS calls as a base functionality.
5
+ class WebRequest
6
+ attr_accessor :relative_url, :options
7
+
8
+ @ssl_cert = nil
9
+
10
+ class << self
11
+ attr_accessor :ssl_cert
12
+ end
13
+
14
+ # Initializes a specific HTTP request.
15
+ #
16
+ # Default (can be overridden, by specifying the options Hash):
17
+ # accept: 'application/json'
18
+ # request: Net::HTTP::Get
19
+ # content_type: 'application/json'
20
+ #
21
+ # @param base_url [String] URL which shall be queried
22
+ # @param options [Hash] options, which shall be merged to the request. Also allows `+logger+` option
23
+ def initialize(base_url, options = {})
24
+ @base_url = base_url
25
+ default_options = { accept: 'application/json', request: Net::HTTP::Get, content_type: 'application/json' }
26
+ @options = default_options.merge(options.reject { |k, _v| k == :logger && k == :relative_url })
27
+ @relative_url = options[:relative_url]
28
+ @logger = options[:logger] || Logger.new(nil)
29
+ end
30
+
31
+ # Executes the HTTP request
32
+ #
33
+ # @param timeout [Integer] number of seconds to wait, before the http request is cancelled, defaults to 60 seconds
34
+ # @return [Response] HTTP response object
35
+ def execute(timeout = nil)
36
+ timeout ||= 60
37
+
38
+ uri = URI.parse("#{@base_url}#{@relative_url}")
39
+ @http = Net::HTTP.new(uri.host, uri.port)
40
+ configure_ssl if @base_url =~ /^https/
41
+
42
+ @http.read_timeout = timeout.to_i
43
+
44
+ request = @options[:request].new(uri.request_uri)
45
+ request['Accept'] = @options[:accept] if @options[:accept]
46
+ request['Content-Type'] = @options[:content_type] if @options[:content_type]
47
+ request['Authorization'] = @options[:authorization] if @options[:authorization]
48
+ request.body = @options[:body]
49
+
50
+ @logger.debug("Requesting #{uri} with '#{@options[:body]}' and timeout '#{timeout}'")
51
+ response = @http.request(request)
52
+ @logger.debug("Received response #{response}")
53
+
54
+ response
55
+ end
56
+
57
+ private
58
+
59
+ def configure_ssl
60
+ @http.use_ssl = true
61
+ @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
62
+ if self.class.ssl_cert && !File.file?(self.class.ssl_cert)
63
+ @logger.warn('SSL certificate file does not exist.')
64
+ elsif self.class.ssl_cert
65
+ @http.cert_store = OpenSSL::X509::Store.new
66
+ @http.cert_store.set_default_paths
67
+ @http.cert_store.add_file(self.class.ssl_cert)
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,359 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ # @abstract Override {#pre_process} and {#post_process} in subclass.
5
+ #
6
+ # Superclass containing everything for all queries towards grafana.
7
+ class AbstractQuery
8
+ attr_accessor :datasource, :timeout, :from, :to
9
+ attr_writer :raw_query
10
+ attr_reader :variables, :result, :panel
11
+
12
+ # @param grafana_or_panel [Object] {Grafana::Grafana} or {Grafana::Panel} object for which the query is executed
13
+ def initialize(grafana_or_panel)
14
+ if grafana_or_panel.is_a?(Grafana::Panel)
15
+ @panel = grafana_or_panel
16
+ @grafana = @panel.dashboard.grafana
17
+ else
18
+ @grafana = grafana_or_panel
19
+ end
20
+ @variables = {}
21
+ end
22
+
23
+ # @abstract
24
+ #
25
+ # Runs the whole process to receive values properly from this query:
26
+ # - calls {#pre_process}
27
+ # - executes this query against the {Grafana::AbstractDatasource} implementation instance
28
+ # - calls {#post_process}
29
+ #
30
+ # @return [Hash] result of the query in standardized format
31
+ def execute
32
+ return @result unless @result.nil?
33
+
34
+ pre_process
35
+ raise DatasourceNotSupportedError.new(@datasource, self) if @datasource.is_a?(Grafana::UnsupportedDatasource)
36
+
37
+ @result = @datasource.request(from: @from, to: @to, raw_query: raw_query, variables: grafana_variables,
38
+ prepared_request: @grafana.prepare_request, timeout: timeout)
39
+ post_process
40
+ @result
41
+ end
42
+
43
+ # Overwrite this function to extract a proper raw query value from this object.
44
+ #
45
+ # If the property +@raw_query+ is not set manually by the calling object, this
46
+ # method may be overwritten to extract the raw query from this object instead.
47
+ def raw_query
48
+ @raw_query
49
+ end
50
+
51
+ # @abstract
52
+ #
53
+ # Overwrite this function to perform all necessary actions, before the query is actually executed.
54
+ # Here you can e.g. set values of variables or similar.
55
+ #
56
+ # Especially for direct queries, it is essential to set the +@datasource+ variable at latest here in the
57
+ # subclass.
58
+ def pre_process
59
+ raise NotImplementedError
60
+ end
61
+
62
+ # @abstract
63
+ #
64
+ # Use this function to format the raw result of the @result variable to conform to the expected return value.
65
+ def post_process
66
+ raise NotImplementedError
67
+ end
68
+
69
+ # Used to specify variables to be used for this query. This method ensures, that only the values of the
70
+ # {Grafana::Variable} stored in the +variables+ Array are overwritten.
71
+ # @param name [String] name of the variable to set
72
+ # @param variable [Grafana::Variable] variable from which the {Grafana::Variable#raw_value} will be assigned to the query variables
73
+ def assign_variable(name, variable)
74
+ raise GrafanaReporterError, "Provided variable is not of type Grafana::Variable (name: '#{name}', value: '#{value}')" unless variable.is_a?(Grafana::Variable)
75
+
76
+ @variables[name] ||= variable
77
+ @variables[name].raw_value = variable.raw_value
78
+ end
79
+
80
+ # Transposes the given result.
81
+ #
82
+ # NOTE: Only the +:content+ of the given result hash is transposed. The +:header+ is ignored.
83
+ #
84
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
85
+ # @param transpose_variable [Grafana::Variable] true, if the result hash shall be transposed
86
+ # @return [Hash] transposed query result
87
+ def transpose(result, transpose_variable)
88
+ return result unless transpose_variable
89
+ return result unless transpose_variable.raw_value == 'true'
90
+
91
+ result[:content] = result[:content].transpose
92
+
93
+ result
94
+ end
95
+
96
+ # Filters columns out of the query result.
97
+ #
98
+ # Multiple columns may be filtered. Therefore the column titles have to be named in the
99
+ # {Grafana::Variable#raw_value} and have to be separated by +,+ (comma).
100
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
101
+ # @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result
102
+ # @return [Hash] filtered query result
103
+ def filter_columns(result, filter_columns_variable)
104
+ return result unless filter_columns_variable
105
+
106
+ filter_columns = filter_columns_variable.raw_value
107
+ filter_columns.split(',').each do |filter_column|
108
+ pos = result[:header][0].index(filter_column)
109
+
110
+ unless pos.nil?
111
+ result[:header][0].delete_at(pos)
112
+ result[:content].each { |row| row.delete_at(pos) }
113
+ end
114
+ end
115
+
116
+ result
117
+ end
118
+
119
+ # Uses the Kernel#format method to format values in the query results.
120
+ #
121
+ # The formatting will be applied separately for every column. Therefore the column formats have to be named
122
+ # in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for
123
+ # a column, no change will happen.
124
+ # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
125
+ # @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result
126
+ # @return [Hash] formatted query result
127
+ def format_columns(result, formats)
128
+ return result unless formats
129
+
130
+ formats.text.split(',').each_index do |i|
131
+ format = formats.text.split(',')[i]
132
+ next if format.empty?
133
+
134
+ result[:content].map do |row|
135
+ next unless row.length > i
136
+
137
+ begin
138
+ row[i] = format % row[i] if row[i]
139
+ rescue StandardError => e
140
+ @grafana.logger.error(e.message)
141
+ row[i] = e.message
142
+ end
143
+ end
144
+ end
145
+ result
146
+ end
147
+
148
+ # Used to replace values in a query result according given configurations.
149
+ #
150
+ # The given variables will be applied to an appropriate column, depending
151
+ # on the naming of the variable. The variable name ending specifies the column,
152
+ # e.g. a variable named +replace_values_2+ will be applied to the second column.
153
+ #
154
+ # The {Grafana::Variable#text} needs to contain the replace specification.
155
+ # Multiple replacements can be specified by separating them with +,+. If a
156
+ # literal comma is needed, it can be escaped with a backslash: +\\,+.
157
+ #
158
+ # The rule will be separated from the replacement text with a colon +:+.
159
+ # If a literal colon is wanted, it can be escaped with a backslash: +\\:+.
160
+ #
161
+ # Examples:
162
+ # - Basic string replacement
163
+ # MyTest:ThisValue
164
+ # will replace all occurences of the text 'MyTest' with 'ThisValue'.
165
+ # - Number comparison
166
+ # <=10:OK
167
+ # will replace all values smaller or equal to 10 with 'OK'.
168
+ # - Regular expression
169
+ # ^[^ ]\\+ (\d+)$:\1 is the answer
170
+ # will replace all values matching the pattern, e.g. 'answerToAllQuestions 42' to
171
+ # '42 is the answer'. Important to know: the regular expressions always have to start
172
+ # with +^+ and end with +$+, i.e. the expression itself always has to match
173
+ # the whole content in one field.
174
+ # @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#request}.
175
+ # @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
176
+ # @return [Hash] query result with replaced values
177
+ # TODO: make sure that caught errors are also visible in logger
178
+ def replace_values(result, configs)
179
+ return result if configs.empty?
180
+
181
+ configs.each do |key, formats|
182
+ cols = key.split('_')[2..-1].map(&:to_i)
183
+
184
+ formats.text.split(/(?<!\\),/).each_index do |j|
185
+ format = formats.text.split(/(?<!\\),/)[j]
186
+
187
+ arr = format.split(/(?<!\\):/)
188
+ raise MalformedReplaceValuesStatementError, format if arr.length != 2
189
+
190
+ k = arr[0]
191
+ v = arr[1]
192
+ k.gsub!(/\\([:,])/, '\1')
193
+ v.gsub!(/\\([:,])/, '\1')
194
+ result[:content].map do |row|
195
+ (row.length - 1).downto 0 do |i|
196
+ if cols.include?(i + 1) || cols.empty?
197
+
198
+ # handle regular expressions
199
+ if k.start_with?('^') && k.end_with?('$')
200
+ begin
201
+ row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
202
+ rescue StandardError => e
203
+ row[i] = e.message
204
+ end
205
+
206
+ # handle value comparisons
207
+ elsif (match = k.match(/^ *(?<operator>[<>]=?|<>|=) *(?<number>[+-]?\d+(?:\.\d+)?)$/))
208
+ skip = false
209
+ begin
210
+ val = Float(row[i])
211
+ rescue StandardError
212
+ # value cannot be converted to number, simply ignore it as the comparison does not fit here
213
+ skip = true
214
+ end
215
+
216
+ unless skip
217
+ begin
218
+ op = match[:operator].gsub(/^=$/, '==').gsub(/^<>$/, '!=')
219
+ if val.public_send(op.to_sym, Float(match[:number]))
220
+ row[i] = if v.include?('\\1')
221
+ v.gsub(/\\1/, row[i].to_s)
222
+ else
223
+ v
224
+ end
225
+ end
226
+ rescue StandardError => e
227
+ row[i] = e.message
228
+ end
229
+ end
230
+
231
+ # handle as normal comparison
232
+ elsif row[i].to_s == k
233
+ row[i] = v
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
239
+ end
240
+
241
+ result
242
+ end
243
+
244
+ # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
245
+ # correct timestamp. Reason is that grafana does this in the frontend, which we have
246
+ # to emulate here for the reporter.
247
+ #
248
+ # Additionally providing this function the +report_time+ assures that all queries
249
+ # rendered within one report will use _exactly_ the same timestamp in those relative
250
+ # times, i.e. there shouldn't appear any time differences, no matter how long the
251
+ # report is running.
252
+ # @param orig_date [String] time string provided by grafana, usually +from+ or +to+.
253
+ # @param report_time [Grafana::Variable] report start time
254
+ # @param is_to_time [Boolean] true, if the time should be calculated for +to+, false if it shall be
255
+ # calculated for +from+
256
+ # @param timezone [Grafana::Variable] timezone to use, if not system timezone
257
+ # @return [String] translated date as timestamp string
258
+ def translate_date(orig_date, report_time, is_to_time, timezone = nil)
259
+ # TODO: add test case for creation of variable, if not given, maybe also print a warning
260
+ report_time ||= ::Grafana::Variable.new(Time.now.to_s)
261
+ return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
262
+ return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
263
+ return orig_date if orig_date =~ /^\d+$/
264
+
265
+ # check if a relative date is mentioned
266
+ date_spec = orig_date.clone
267
+
268
+ date_spec.slice!(/^now/)
269
+ raise TimeRangeUnknownError, orig_date unless date_spec
270
+
271
+ date = DateTime.parse(report_time.raw_value)
272
+ # TODO: allow from_translated or similar in ADOC template
273
+ date = date.new_offset(timezone.raw_value) if timezone
274
+
275
+ until date_spec.empty?
276
+ fit_match = date_spec.match(%r{^/(?<fit>[smhdwMy])})
277
+ if fit_match
278
+ date = fit_date(date, fit_match[:fit], is_to_time)
279
+ date_spec.slice!(%r{^/#{fit_match[:fit]}})
280
+ end
281
+
282
+ delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
283
+ if delta_match
284
+ date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
285
+ date_spec.slice!(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/)
286
+ end
287
+
288
+ raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
289
+ end
290
+
291
+ # step back one second, if this is the 'to' time
292
+ date = (date.to_time - 1).to_datetime if is_to_time
293
+
294
+ (Time.at(date.to_time.to_i).to_i * 1000).to_s
295
+ end
296
+
297
+ private
298
+
299
+ # @return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name
300
+ # is prefixed with +var-+
301
+ def grafana_variables
302
+ @variables.select { |k, _v| k =~ /^var-.+/ }
303
+ end
304
+
305
+ def delta_date(date, delta_count, time_letter)
306
+ # substract specified time
307
+ case time_letter
308
+ when 's'
309
+ (date.to_time + (delta_count * 1)).to_datetime
310
+ when 'm'
311
+ (date.to_time + (delta_count * 60)).to_datetime
312
+ when 'h'
313
+ (date.to_time + (delta_count * 60 * 60)).to_datetime
314
+ when 'd'
315
+ date.next_day(delta_count)
316
+ when 'w'
317
+ date.next_day(delta_count * 7)
318
+ when 'M'
319
+ date.next_month(delta_count)
320
+ when 'y'
321
+ date.next_year(delta_count)
322
+ end
323
+ end
324
+
325
+ def fit_date(date, fit_letter, is_to_time)
326
+ # fit to specified time frame
327
+ case fit_letter
328
+ when 's'
329
+ date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, date.sec, date.zone)
330
+ date = (date.to_time + 1).to_datetime if is_to_time
331
+ when 'm'
332
+ date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, 0, date.zone)
333
+ date = (date.to_time + 60).to_datetime if is_to_time
334
+ when 'h'
335
+ date = DateTime.new(date.year, date.month, date.day, date.hour, 0, 0, date.zone)
336
+ date = (date.to_time + 60 * 60).to_datetime if is_to_time
337
+ when 'd'
338
+ date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
339
+ date = date.next_day(1) if is_to_time
340
+ when 'w'
341
+ date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
342
+ date = if date.wday.zero?
343
+ date.prev_day(7)
344
+ else
345
+ date.prev_day(date.wday - 1)
346
+ end
347
+ date = date.next_day(7) if is_to_time
348
+ when 'M'
349
+ date = DateTime.new(date.year, date.month, 1, 0, 0, 0, date.zone)
350
+ date = date.next_month if is_to_time
351
+ when 'y'
352
+ date = DateTime.new(date.year, 1, 1, 0, 0, 0, date.zone)
353
+ date = date.next_year if is_to_time
354
+ end
355
+
356
+ date
357
+ end
358
+ end
359
+ end