Class: GrafanaReporter::AbstractQuery Abstract

Inherits:
Object
  • Object
show all
Defined in:
lib/grafana_reporter/abstract_query.rb

Overview

This class is abstract.

Override #pre_process and #post_process in subclass.

Superclass containing everything for all queries towards grafana.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(grafana_obj, opts = {}) ⇒ AbstractQuery

Returns a new instance of AbstractQuery.

Parameters:

Options Hash (opts):

  • :variables (Hash)

    hash of variables, which shall be used to replace variable references in the query

  • :ignore_dashboard_defaults (Boolean)

    True if #assign_dashboard_defaults should not be called

  • :do_not_use_translated_times (Boolean)

    True if given from and to times should used as is, without being resolved to reporter times - using this parameter can lead to inconsistent report contents



27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/grafana_reporter/abstract_query.rb', line 27

def initialize(grafana_obj, opts = {})
  if grafana_obj.is_a?(Grafana::Panel)
    @panel = grafana_obj
    @dashboard = @panel.dashboard
    @grafana = @dashboard.grafana

  elsif grafana_obj.is_a?(Grafana::Dashboard)
    @dashboard = grafana_obj
    @grafana = @dashboard.grafana

  elsif grafana_obj.is_a?(Grafana::Grafana)
    @grafana = grafana_obj

  elsif !grafana_obj
    # nil given

  else
    raise GrafanaReporterError, "Internal error in AbstractQuery: given object is of type #{grafana_obj.class.name}, which is not supported"
  end
  @logger = @grafana ? @grafana.logger : ::Logger.new($stderr, level: :info)
  @variables = {}
  @variables['from'] = Grafana::Variable.new(nil)
  @variables['to'] = Grafana::Variable.new(nil)

  assign_dashboard_defaults unless opts[:ignore_dashboard_defaults]
  opts[:variables].each { |k, v| assign_variable(k, v) } if opts[:variables].is_a?(Hash)

  @translate_times = true
  @translate_times = false if opts[:do_not_use_translated_times]
end

Instance Attribute Details

#dashboardObject (readonly)

Returns the value of attribute dashboard.



13
14
15
# File 'lib/grafana_reporter/abstract_query.rb', line 13

def dashboard
  @dashboard
end

#datasourceObject

Returns the value of attribute datasource.



11
12
13
# File 'lib/grafana_reporter/abstract_query.rb', line 11

def datasource
  @datasource
end

#panelObject (readonly)

Returns the value of attribute panel.



13
14
15
# File 'lib/grafana_reporter/abstract_query.rb', line 13

def panel
  @panel
end

#raw_queryObject

Overwrite this function to extract a proper raw query value from this object.

If the property @raw_query is not set manually by the calling object, this method may be overwritten to extract the raw query from this object instead.



111
112
113
# File 'lib/grafana_reporter/abstract_query.rb', line 111

def raw_query
  @raw_query
end

#resultObject (readonly)

Returns the value of attribute result.



13
14
15
# File 'lib/grafana_reporter/abstract_query.rb', line 13

def result
  @result
end

#variablesObject (readonly)

Returns the value of attribute variables.



13
14
15
# File 'lib/grafana_reporter/abstract_query.rb', line 13

def variables
  @variables
end

Instance Method Details

#apply(result, actions, variables) ⇒ Object

Applies a given action string, separated by commas, in the given order to the results.



398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/grafana_reporter/abstract_query.rb', line 398

def apply(result, actions, variables)
  actions.raw_value.split(',').each do |action|
    case action.strip
    when 'filter_columns'
      result = filter_columns(result, variables['filter_columns'])
    when 'format'
      result = format_columns(result, variables['format'])
    when 'replace_values'
      result = replace_values(result, variables.select { |k, _v| k =~ /^replace_values_\d+/ })
    when 'transpose!'
      result = transpose(result, Variable.new('true'))
    when 'transpose'
      result = transpose(result, variables['transpose'])
    else
      @logger.warn("Unsupported action '#{action}' configured in 'after_fetch' or 'after_calculate'. Only" \
                   " the following options are supported: filter_columns, format, replace_values, transpose,"\
                   " transpose!")
    end
  end

  result
end

#executeHash

This method is abstract.

Runs the whole process to receive values properly from this query:

Returns:

  • (Hash)

    result of the query in standardized format

Raises:



66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/grafana_reporter/abstract_query.rb', line 66

def execute
  return @result unless @result.nil?

  from = @variables['from'].raw_value
  to = @variables['to'].raw_value
  if @translate_times
    from = translate_date(@variables['from'], @variables['grafana_report_timestamp'], false, @variables['from_timezone'] ||
                          @variables['grafana_default_from_timezone'])
    to = translate_date(@variables['to'], @variables['grafana_report_timestamp'], true, @variables['to_timezone'] ||
                        @variables['grafana_default_to_timezone'])
  end

  pre_process
  raise DatasourceNotSupportedError.new(@datasource, self) if @datasource.is_a?(Grafana::UnsupportedDatasource)

  begin
    @result = @datasource.request(from: from, to: to, raw_query: raw_query, variables: @variables,
                                  prepared_request: @grafana.prepare_request, timeout: timeout,
                                  grafana_version: @grafana.version)
    if @variables['verbose_log']
      @logger.debug("Raw result: #{@result}") if @variables['verbose_log'].raw_value.downcase == "true"
    end
  rescue ::Grafana::GrafanaError
    # grafana errors will be directly passed through
    raise
  rescue GrafanaReporterError
    # grafana errors will be directly passed through
    raise
  rescue StandardError => e
    raise DatasourceRequestInternalError.new(@datasource, "#{e.message}\n#{e.backtrace.join("\n")}")
  end

  raise DatasourceRequestInvalidReturnValueError.new(@datasource, @result) unless datasource_response_valid?

  post_process
  if @variables['verbose_log']
    @logger.debug("Formatted result: #{@result}") if @variables['verbose_log'].raw_value.downcase == "true"
  end
  @result
end

#filter_columns(result, filter_columns_variable) ⇒ Hash

Filters columns out of the query result.

Multiple columns may be filtered. Therefore the column titles have to be named in the Grafana::Variable#raw_value and have to be separated by , (comma).

Commas can be used in a format string, but need to be escaped by using _,.

Parameters:

Returns:

  • (Hash)

    filtered query result



157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# File 'lib/grafana_reporter/abstract_query.rb', line 157

def filter_columns(result, filter_columns_variable)
  return result unless filter_columns_variable

  filter_columns = filter_columns_variable.raw_value
  filter_columns.split(/(?<!_),/).each do |filter_column|
    pos = result[:header].index(filter_column.gsub("_,", ","))

    unless pos.nil?
      result[:header].delete_at(pos)
      result[:content].each { |row| row.delete_at(pos) }
    end
  end

  result
end

#format_columns(result, formats) ⇒ Hash

Uses the Kernel#format method to format values in the query results.

The formatting will be applied separately for every column. Therefore the column formats have to be named in the Grafana::Variable#raw_value and have to be separated by , (comma). If no value is specified for a column, no change will happen.

It is also possible to format milliseconds as dates by specifying date formats, e.g. date:iso. It is possible to use any date format according https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#from-and-to

Commas can be used in a format string, but need to be escaped by using _,.

Parameters:

Returns:

  • (Hash)

    formatted query result



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/grafana_reporter/abstract_query.rb', line 187

def format_columns(result, formats)
  return result unless formats

  formats.text.split(/(?<!_),/).each_index do |i|
    format = formats.text.split(/(?<!_),/)[i].gsub("_,", ",")
    next if format.empty?

    result[:content].map do |row|
      next unless row.length > i

      begin
        if format =~ /^date:/
          row[i] = ::Grafana::Variable.format_as_date(row[i], format.sub(/^date:/, '')) if row[i]
        else
          row[i] = format % row[i] if row[i]
        end
      rescue StandardError => e
        @logger.warn("Formatting of row #{i} with content '#{row[i]}' and format request '#{format}'"\
                     " was not possible. Row is left unchanged (message: #{e.message})")
      end
    end
  end
  result
end

#format_table_output(result, opts) ⇒ String

Used to build a table output in a custom format.

Parameters:

Options Hash (opts):

  • :row_divider (Grafana::Variable)

    requested row divider for the result table, only to be used with table_formatter ‘adoc_deprecated`

  • :column_divider (Grafana::Variable)

    requested row divider for the result table, only to be used with table_formatter ‘adoc_deprecated`

  • :include_headline (Grafana::Variable)

    specifies if table should contain headline, defaults to false

  • :table_formatter (Grafana::Variable)

    specifies which formatter shall be used, defaults to ‘csv’

  • :transposed (Grafana::Variable)

    specifies whether the result table is transposed

Returns:

  • (String)

    table in custom output format



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/grafana_reporter/abstract_query.rb', line 321

def format_table_output(result, opts)
  opts = { include_headline: Grafana::Variable.new('false'),
           table_formatter: Grafana::Variable.new('csv'),
           row_divider: Grafana::Variable.new('| '),
           column_divider: Grafana::Variable.new(' | '),
           transpose: Grafana::Variable.new('false') }.merge(opts.delete_if {|_k, v| v.nil? })

  if opts[:table_formatter].raw_value == 'adoc_deprecated'
    @logger.warn("You are using deprecated 'table_formatter' named 'adoc_deprecated', which will be "\
                 "removed in a future version. Start using 'adoc_plain' or register your own "\
                 "implementation of AbstractTableFormatStrategy.")
    return result[:content].map do |row|
      opts[:row_divider].raw_value + row.map do |item|
        item.to_s.gsub('|', '\\|')
      end.join(opts[:column_divider].raw_value)
    end.join("\n")
  end

  AbstractTableFormatStrategy.get(opts[:table_formatter].raw_value).format(result, opts[:include_headline].raw_value.downcase == 'true', opts[:transpose].raw_value.downcase == 'true')
end

#post_processObject

This method is abstract.

Use this function to format the raw result of the @result variable to conform to the expected return value.

Raises:

  • (NotImplementedError)


129
130
131
# File 'lib/grafana_reporter/abstract_query.rb', line 129

def post_process
  raise NotImplementedError
end

#pre_processObject

This method is abstract.

Overwrite this function to perform all necessary actions, before the query is actually executed. Here you can e.g. set values of variables or similar.

Especially for direct queries, it is essential to set the @datasource variable at latest here in the subclass.

Raises:

  • (NotImplementedError)


122
123
124
# File 'lib/grafana_reporter/abstract_query.rb', line 122

def pre_process
  raise NotImplementedError
end

#replace_values(result, configs) ⇒ Hash

Used to replace values in a query result according given configurations.

The given variables will be applied to an appropriate column, depending on the naming of the variable. The variable name ending specifies the column, e.g. a variable named replace_values_2 will be applied to the second column.

The Grafana::Variable#text needs to contain the replace specification. Multiple replacements can be specified by separating them with ,. If a literal comma is needed, it can be escaped with a backslash: \,.

The rule will be separated from the replacement text with a colon :. If a literal colon is wanted, it can be escaped with a backslash: \:.

Examples:

  • Basic string replacement

    MyTest:ThisValue
    

will replace all occurences of the text ‘MyTest’ with ‘ThisValue’.

  • Number comparison

    <=10:OK
    

will replace all values smaller or equal to 10 with ‘OK’.

  • Regular expression

    ^[^ ]\\+ (\d+)$:\1 is the answer
    

will replace all values matching the pattern, e.g. ‘answerToAllQuestions 42’ to ‘42 is the answer’. Important to know: the regular expressions always have to start with ^ and end with $, i.e. the expression itself always has to match the whole content in one field.

Parameters:

Returns:

  • (Hash)

    query result with replaced values



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
# File 'lib/grafana_reporter/abstract_query.rb', line 241

def replace_values(result, configs)
  return result if configs.empty?

  configs.each do |key, formats|
    cols = key.split('_')[2..-1].map(&:to_i)

    formats.text.split(/(?<!\\),/).each_index do |j|
      format = formats.text.split(/(?<!\\),/)[j]

      arr = format.split(/(?<!\\):/)
      raise MalformedReplaceValuesStatementError, format if arr.length != 2

      k = arr[0]
      v = arr[1]

      # allow keys and values to contain escaped colons or commas
      k = k.gsub(/\\([:,])/, '\1')
      v = v.gsub(/\\([:,])/, '\1')

      result[:content].map do |row|
        (row.length - 1).downto 0 do |i|
          if cols.include?(i + 1) || cols.empty?

            # handle regular expressions
            if k.start_with?('^') && k.end_with?('$')
              begin
                row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
              rescue StandardError => e
                @logger.error(e.message)
                row[i] = e.message
              end

            # handle value comparisons
            elsif (match = k.match(/^ *(?<operator>[<>]=?|<>|=) *(?<number>[+-]?\d+(?:\.\d+)?)$/))
              skip = false
              begin
                val = Float(row[i])
              rescue StandardError
                # value cannot be converted to number, simply ignore it as the comparison does not fit here
                skip = true
              end

              unless skip
                begin
                  op = match[:operator].gsub(/^=$/, '==').gsub(/^<>$/, '!=')
                  if val.public_send(op.to_sym, Float(match[:number]))
                    row[i] = if v.include?('\\1')
                               v.gsub(/\\1/, row[i].to_s)
                             else
                               v
                             end
                  end
                rescue StandardError => e
                  @logger.error(e.message)
                  row[i] = e.message
                end
              end

            # handle as normal comparison
            elsif row[i].to_s == k
              row[i] = v
            end
          end
        end
      end
    end
  end

  result
end

#timeoutObject



15
16
17
18
19
20
# File 'lib/grafana_reporter/abstract_query.rb', line 15

def timeout
  return @variables['timeout'].raw_value if @variables['timeout']
  return @variables['grafana_default_timeout'].raw_value if @variables['grafana_default_timeout']

  nil
end

#translate_date(orig_date, report_time, is_to_time, timezone = nil) ⇒ String

Used to translate the relative date strings used by grafana, e.g. now-5d/w to the correct timestamp. Reason is that grafana does this in the frontend, which we have to emulate here for the reporter.

Additionally providing this function the report_time assures that all queries rendered within one report will use exactly the same timestamp in those relative times, i.e. there shouldn’t appear any time differences, no matter how long the report is running.

Parameters:

  • orig_date (String)

    time string provided by grafana, usually from or to.

  • report_time (Grafana::Variable)

    report start time

  • is_to_time (Boolean)

    true, if the time should be calculated for to, false if it shall be calculated for from

  • timezone (Grafana::Variable) (defaults to: nil)

    timezone to use, if not system timezone

Returns:

  • (String)

    translated date as timestamp string

Raises:



356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/grafana_reporter/abstract_query.rb', line 356

def translate_date(orig_date, report_time, is_to_time, timezone = nil)
  @logger.warn("#translate_date has been called without 'report_time' - using current time as fallback.") unless report_time
  report_time ||= ::Grafana::Variable.new(Time.now.to_s)
  orig_date = orig_date.raw_value if orig_date.is_a?(Grafana::Variable)

  return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
  return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
  return orig_date if orig_date =~ /^\d+$/

  # check if a relative date is mentioned
  date_spec = orig_date.clone

  date_spec = date_spec.gsub(/^now/, '')
  raise TimeRangeUnknownError, orig_date unless date_spec

  date = DateTime.parse(report_time.raw_value)
  # TODO: PRIO allow from_translated or similar in ADOC template
  date = date.new_offset(timezone.raw_value) if timezone

  until date_spec.empty?
    fit_match = date_spec.match(%r{^/(?<fit>[smhdwMy])})
    if fit_match
      date = fit_date(date, fit_match[:fit], is_to_time)
      date_spec = date_spec.gsub(%r{^/#{fit_match[:fit]}}, '')
    end

    delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
    if delta_match
      date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
      date_spec = date_spec.gsub(/^#{delta_match[:op] == '+' ? '\+' : '-'}#{delta_match[:count]}#{delta_match[:unit]}/, '')
    end

    raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
  end

  # step back one second, if this is the 'to' time
  date = (date.to_time - 1).to_datetime if is_to_time

  (Time.at(date.to_time.to_i).to_i * 1000).to_s
end

#transpose(result, transpose_variable) ⇒ Hash

Transposes the given result.

NOTE: Only the :content of the given result hash is transposed. The :header is ignored.

Parameters:

Returns:

  • (Hash)

    transposed query result



140
141
142
143
144
145
146
# File 'lib/grafana_reporter/abstract_query.rb', line 140

def transpose(result, transpose_variable)
  return result unless transpose_variable
  return result unless transpose_variable.raw_value == 'true'
  result[:content] = result[:content].transpose

  result
end