class GrafanaReporter::AbstractQuery

@abstract Override {#pre_process} and {#post_process} in subclass.

Superclass containing everything for all queries towards grafana.

Attributes

dashboard[R]
datasource[RW]
panel[R]
raw_query[W]
result[R]
variables[R]

Public Class Methods

new(grafana_obj, opts = {}) click to toggle source

@param grafana_obj [Object] {Grafana::Grafana}, {Grafana::Dashboard} or {Grafana::Panel} object for which the query is executed @param opts [Hash] hash options, which may consist of: @option opts [Hash] :variables hash of variables, which shall be used to replace variable references in the query @option opts [Boolean] :ignore_dashboard_defaults True if {#assign_dashboard_defaults} should not be called @option opts [Boolean] :do_not_use_translated_times 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

# File lib/grafana_reporter/abstract_query.rb, line 28
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
  @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

Public Instance Methods

execute() click to toggle source

@abstract

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

  • calls {#pre_process}

  • executes this query against the {Grafana::AbstractDatasource} implementation instance

  • calls {#post_process}

@return [Hash] result of the query in standardized format

# 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: grafana_variables,
                                  prepared_request: @grafana.prepare_request, timeout: timeout)
  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)
  end

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

  post_process
  @result
end
filter_columns(result, filter_columns_variable) click to toggle source

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). @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request}) @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result @return [Hash] filtered query result

# File lib/grafana_reporter/abstract_query.rb, line 149
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)

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

  result
end
format_columns(result, formats) click to toggle source

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. @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request}) @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result @return [Hash] formatted query result

# File lib/grafana_reporter/abstract_query.rb, line 173
def format_columns(result, formats)
  return result unless formats

  formats.text.split(',').each_index do |i|
    format = formats.text.split(',')[i]
    next if format.empty?

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

      begin
        row[i] = format % row[i] if row[i]
      rescue StandardError => e
        @grafana.logger.error(e.message)
        row[i] = e.message
      end
    end
  end
  result
end
format_table_output(result, opts) click to toggle source

Used to build a table output in a custom format. @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request}) @param opts [Hash] options for the formatting: @option opts [Grafana::Variable] :row_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated` @option opts [Grafana::Variable] :column_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated` @option opts [Grafana::Variable] :include_headline specifies if table should contain headline, defaults to false @option opts [Grafana::Variable] :table_formatter specifies which formatter shall be used, defaults to 'csv' @return [String] table in custom output format

# File lib/grafana_reporter/abstract_query.rb, line 302
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(' | ') }.merge(opts.delete_if {|_k, v| v.nil? })

  if opts[:table_formatter].raw_value == 'adoc_deprecated'
    @grafana.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')
end
post_process() click to toggle source

@abstract

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

# File lib/grafana_reporter/abstract_query.rb, line 122
def post_process
  raise NotImplementedError
end
pre_process() click to toggle source

@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.

# File lib/grafana_reporter/abstract_query.rb, line 115
def pre_process
  raise NotImplementedError
end
raw_query() click to toggle source

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.

# File lib/grafana_reporter/abstract_query.rb, line 104
def raw_query
  @raw_query
end
replace_values(result, configs) click to toggle source

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. @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#request}. @param configs [Array<Grafana::Variable>] one variable for replacing values in one column @return [Hash] query result with replaced values

# File lib/grafana_reporter/abstract_query.rb, line 223
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
                @grafana.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
                  @grafana.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
timeout() click to toggle source
# File lib/grafana_reporter/abstract_query.rb, line 15
def timeout
  # TODO: PRIO check where value priorities should be evaluated
  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) click to toggle source

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. @param orig_date [String] time string provided by grafana, usually from or to. @param report_time [Grafana::Variable] report start time @param is_to_time [Boolean] true, if the time should be calculated for to, false if it shall be

calculated for +from+

@param timezone [Grafana::Variable] timezone to use, if not system timezone @return [String] translated date as timestamp string

# File lib/grafana_reporter/abstract_query.rb, line 336
def translate_date(orig_date, report_time, is_to_time, timezone = nil)
  @grafana.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) click to toggle source

Transposes the given result.

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

@param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request}) @param transpose_variable [Grafana::Variable] true, if the result hash shall be transposed @return [Hash] transposed query result

# File lib/grafana_reporter/abstract_query.rb, line 133
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

Private Instance Methods

assign_dashboard_defaults() click to toggle source

Sets default configurations from the given {Grafana::Dashboard} and store them as settings in the {AbstractQuery}.

Following data is extracted:

  • from, by {Grafana::Dashboard#from_time}

  • to, by {Grafana::Dashboard#to_time}

  • and all variables as {Grafana::Variable}, prefixed with var-, as grafana also does it

# File lib/grafana_reporter/abstract_query.rb, line 397
def assign_dashboard_defaults
  return unless @dashboard

  assign_variable('from', @dashboard.from_time)
  assign_variable('to', @dashboard.to_time)
  @dashboard.variables.each { |item| assign_variable("var-#{item.name}", item) }
end
assign_variable(name, variable) click to toggle source

Used to specify variables to be used for this query. This method ensures, that only the values of the {Grafana::Variable} stored in the variables Array are overwritten. @param name [String] name of the variable to set @param variable [Grafana::Variable] variable from which the {Grafana::Variable#raw_value} will be assigned to the query variables

# File lib/grafana_reporter/abstract_query.rb, line 383
def assign_variable(name, variable)
  variable = Grafana::Variable.new(variable) unless variable.is_a?(Grafana::Variable)

  @variables[name] ||= variable
  @variables[name].raw_value = variable.raw_value
end
datasource_response_valid?() click to toggle source
# File lib/grafana_reporter/abstract_query.rb, line 405
def datasource_response_valid?
  return false if @result.nil?
  return false unless @result.is_a?(Hash)
  # TODO: compare how empty valid responses look like in grafana
  return true if @result.empty?
  return false unless @result.key?(:header)
  return false unless @result.key?(:content)
  return false unless @result[:header].is_a?(Array)
  return false unless @result[:content].is_a?(Array)

  true
end
delta_date(date, delta_count, time_letter) click to toggle source
# File lib/grafana_reporter/abstract_query.rb, line 424
def delta_date(date, delta_count, time_letter)
  # substract specified time
  case time_letter
  when 's'
    (date.to_time + (delta_count * 1)).to_datetime
  when 'm'
    (date.to_time + (delta_count * 60)).to_datetime
  when 'h'
    (date.to_time + (delta_count * 60 * 60)).to_datetime
  when 'd'
    date.next_day(delta_count)
  when 'w'
    date.next_day(delta_count * 7)
  when 'M'
    date.next_month(delta_count)
  when 'y'
    date.next_year(delta_count)
  end
end
fit_date(date, fit_letter, is_to_time) click to toggle source
# File lib/grafana_reporter/abstract_query.rb, line 444
def fit_date(date, fit_letter, is_to_time)
  # fit to specified time frame
  case fit_letter
  when 's'
    date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, date.sec, date.zone)
    date = (date.to_time + 1).to_datetime if is_to_time
  when 'm'
    date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, 0, date.zone)
    date = (date.to_time + 60).to_datetime if is_to_time
  when 'h'
    date = DateTime.new(date.year, date.month, date.day, date.hour, 0, 0, date.zone)
    date = (date.to_time + 60 * 60).to_datetime if is_to_time
  when 'd'
    date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
    date = date.next_day(1) if is_to_time
  when 'w'
    date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
    date = if date.wday.zero?
             date.prev_day(7)
           else
             date.prev_day(date.wday - 1)
           end
    date = date.next_day(7) if is_to_time
  when 'M'
    date = DateTime.new(date.year, date.month, 1, 0, 0, 0, date.zone)
    date = date.next_month if is_to_time
  when 'y'
    date = DateTime.new(date.year, 1, 1, 0, 0, 0, date.zone)
    date = date.next_year if is_to_time
  end

  date
end
grafana_variables() click to toggle source

@return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name

is prefixed with +var-+
# File lib/grafana_reporter/abstract_query.rb, line 420
def grafana_variables
  @variables.select { |k, _v| k =~ /^var-.+/ }
end