ruby-grafana-reporter 0.4.1 → 0.4.5
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.
- checksums.yaml +4 -4
- data/README.md +336 -185
- data/lib/VERSION.rb +2 -2
- data/lib/grafana/abstract_datasource.rb +30 -17
- data/lib/grafana/errors.rb +4 -4
- data/lib/grafana/grafana.rb +2 -0
- data/lib/grafana/grafana_property_datasource.rb +12 -0
- data/lib/grafana/graphite_datasource.rb +27 -5
- data/lib/grafana/influxdb_datasource.rb +156 -0
- data/lib/grafana/panel.rb +1 -1
- data/lib/grafana/prometheus_datasource.rb +37 -6
- data/lib/grafana/sql_datasource.rb +10 -11
- data/lib/grafana/variable.rb +29 -22
- data/lib/grafana_reporter/abstract_query.rb +150 -31
- data/lib/grafana_reporter/abstract_report.rb +37 -5
- data/lib/grafana_reporter/abstract_table_format_strategy.rb +34 -0
- data/lib/grafana_reporter/alerts_table_query.rb +5 -6
- data/lib/grafana_reporter/annotations_table_query.rb +5 -6
- data/lib/grafana_reporter/application/application.rb +7 -2
- data/lib/grafana_reporter/application/webservice.rb +35 -30
- data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +25 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +7 -5
- data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +7 -5
- data/lib/grafana_reporter/asciidoctor/help.rb +458 -0
- data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +5 -4
- data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +5 -4
- data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +5 -4
- data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +6 -6
- data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +4 -4
- data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +21 -35
- data/lib/grafana_reporter/asciidoctor/report.rb +16 -26
- data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +1 -1
- data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +6 -4
- data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +5 -3
- data/lib/grafana_reporter/console_configuration_wizard.rb +2 -2
- data/lib/grafana_reporter/csv_table_format_strategy.rb +23 -0
- data/lib/grafana_reporter/demo_report_wizard.rb +5 -2
- data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
- data/lib/grafana_reporter/erb/report.rb +14 -21
- data/lib/grafana_reporter/erb/report_jail.rb +21 -0
- data/lib/grafana_reporter/errors.rb +19 -3
- data/lib/grafana_reporter/panel_image_query.rb +2 -5
- data/lib/grafana_reporter/query_value_query.rb +1 -19
- data/lib/grafana_reporter/report_webhook.rb +12 -8
- data/lib/ruby_grafana_reporter.rb +10 -11
- metadata +9 -3
- data/lib/grafana_reporter/help.rb +0 -443
    
        data/lib/grafana/variable.rb
    CHANGED
    
    | @@ -33,6 +33,7 @@ module Grafana | |
| 33 33 | 
             
                  if config_or_value.is_a? Hash
         | 
| 34 34 | 
             
                    @config = config_or_value
         | 
| 35 35 | 
             
                    @name = @config['name']
         | 
| 36 | 
            +
                    # TODO: if a variable uses type 'query' which is never updated, the selected values are stored in 'options'
         | 
| 36 37 | 
             
                    unless @config['current'].nil?
         | 
| 37 38 | 
             
                      @raw_value = @config['current']['value']
         | 
| 38 39 | 
             
                      @text = @config['current']['text']
         | 
| @@ -62,93 +63,98 @@ module Grafana | |
| 62 63 | 
             
                def value_formatted(format = '')
         | 
| 63 64 | 
             
                  value = @raw_value
         | 
| 64 65 |  | 
| 65 | 
            -
                  #  | 
| 66 | 
            -
                  #  | 
| 67 | 
            -
                  if  | 
| 66 | 
            +
                  # if 'All' is selected for this template variable, capture all values properly
         | 
| 67 | 
            +
                  # (from grafana config or query) and format the results afterwards accordingly
         | 
| 68 | 
            +
                  if value == '$__all'
         | 
| 68 69 | 
             
                    if !@config['options'].empty?
         | 
| 70 | 
            +
                      # this query contains predefined values, so capture them and format the values accordingly
         | 
| 71 | 
            +
                      # this happens either if type='custom' or a query, which is never updated
         | 
| 69 72 | 
             
                      value = @config['options'].map { |item| item['value'] }
         | 
| 70 | 
            -
             | 
| 71 | 
            -
             | 
| 73 | 
            +
             | 
| 74 | 
            +
                    elsif @config['type'] == 'query' && !@config['query'].empty?
         | 
| 75 | 
            +
                      # TODO: replace variables in query, execute it and evaluate the results as if they were normally selected
         | 
| 76 | 
            +
                      # "multiFormat": contains variable replacement in "query", e.g. 'regex values' or 'glob'
         | 
| 77 | 
            +
                      # "datasource": contains name of datasource
         | 
| 72 78 | 
             
                      return @config['query']
         | 
| 73 | 
            -
             | 
| 79 | 
            +
             | 
| 74 80 | 
             
                    else
         | 
| 75 | 
            -
                      # TODO:  | 
| 81 | 
            +
                      # TODO: add support for variable type: 'datasource' and 'adhoc'
         | 
| 76 82 | 
             
                    end
         | 
| 77 83 | 
             
                  end
         | 
| 78 84 |  | 
| 79 85 | 
             
                  case format
         | 
| 80 86 | 
             
                  when 'csv'
         | 
| 81 | 
            -
                    return value.join(',').to_s if multi?
         | 
| 87 | 
            +
                    return value.join(',').to_s if multi? && value.is_a?(Array)
         | 
| 82 88 |  | 
| 83 89 | 
             
                    value.to_s
         | 
| 84 90 |  | 
| 85 91 | 
             
                  when 'distributed'
         | 
| 86 | 
            -
                    return value.join(",#{name}=") if multi?
         | 
| 92 | 
            +
                    return value.join(",#{name}=") if multi? && value.is_a?(Array)
         | 
| 87 93 |  | 
| 88 94 | 
             
                    value
         | 
| 89 95 | 
             
                  when 'doublequote'
         | 
| 90 | 
            -
                    if multi?
         | 
| 96 | 
            +
                    if multi? && value.is_a?(Array)
         | 
| 91 97 | 
             
                      value = value.map { |item| "\"#{item.gsub(/\\/, '\\\\').gsub(/"/, '\\"')}\"" }
         | 
| 92 98 | 
             
                      return value.join(',')
         | 
| 93 99 | 
             
                    end
         | 
| 94 100 | 
             
                    "\"#{value.gsub(/"/, '\\"')}\""
         | 
| 95 101 |  | 
| 96 102 | 
             
                  when 'json'
         | 
| 97 | 
            -
                    if multi?
         | 
| 103 | 
            +
                    if multi? && value.is_a?(Array)
         | 
| 98 104 | 
             
                      value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\\0')}\"" }
         | 
| 99 105 | 
             
                      return "[#{value.join(',')}]"
         | 
| 100 106 | 
             
                    end
         | 
| 101 107 | 
             
                    "\"#{value.gsub(/"/, '\\"')}\""
         | 
| 102 108 |  | 
| 103 109 | 
             
                  when 'percentencode'
         | 
| 104 | 
            -
                    value = "{#{value.join(',')}}" if multi?
         | 
| 110 | 
            +
                    value = "{#{value.join(',')}}" if multi? && value.is_a?(Array)
         | 
| 105 111 | 
             
                    ERB::Util.url_encode(value)
         | 
| 106 112 |  | 
| 107 113 | 
             
                  when 'pipe'
         | 
| 108 | 
            -
                    return value.join('|') if multi?
         | 
| 114 | 
            +
                    return value.join('|') if multi? && value.is_a?(Array)
         | 
| 109 115 |  | 
| 110 116 | 
             
                    value
         | 
| 111 117 |  | 
| 112 118 | 
             
                  when 'raw'
         | 
| 113 | 
            -
                    return "{#{value.join(',')}}" if multi?
         | 
| 119 | 
            +
                    return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
         | 
| 114 120 |  | 
| 115 121 | 
             
                    value
         | 
| 116 122 |  | 
| 117 123 | 
             
                  when 'regex'
         | 
| 118 | 
            -
                    if multi?
         | 
| 124 | 
            +
                    if multi? && value.is_a?(Array)
         | 
| 119 125 | 
             
                      value = value.map { |item| item.gsub(%r{[/$.|\\]}, '\\\\\0') }
         | 
| 120 126 | 
             
                      return "(#{value.join('|')})"
         | 
| 121 127 | 
             
                    end
         | 
| 122 128 | 
             
                    value.gsub(%r{[/$.|\\]}, '\\\\\0')
         | 
| 123 129 |  | 
| 124 130 | 
             
                  when 'singlequote'
         | 
| 125 | 
            -
                    if multi?
         | 
| 131 | 
            +
                    if multi? && value.is_a?(Array)
         | 
| 126 132 | 
             
                      value = value.map { |item| "'#{item.gsub(/'/, '\\\\\0')}'" }
         | 
| 127 133 | 
             
                      return value.join(',')
         | 
| 128 134 | 
             
                    end
         | 
| 129 135 | 
             
                    "'#{value.gsub(/'/, '\\\\\0')}'"
         | 
| 130 136 |  | 
| 131 137 | 
             
                  when 'sqlstring'
         | 
| 132 | 
            -
                    if multi?
         | 
| 138 | 
            +
                    if multi? && value.is_a?(Array)
         | 
| 133 139 | 
             
                      value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
         | 
| 134 140 | 
             
                      return value.join(',')
         | 
| 135 141 | 
             
                    end
         | 
| 136 142 | 
             
                    "'#{value.gsub(/'/, "''")}'"
         | 
| 137 143 |  | 
| 138 144 | 
             
                  when 'lucene'
         | 
| 139 | 
            -
                    if multi?
         | 
| 145 | 
            +
                    if multi? && value.is_a?(Array)
         | 
| 140 146 | 
             
                      value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\\0')}\"" }
         | 
| 141 147 | 
             
                      return "(#{value.join(' OR ')})"
         | 
| 142 148 | 
             
                    end
         | 
| 143 149 | 
             
                    value.gsub(%r{[" |=/\\]}, '\\\\\0')
         | 
| 144 150 |  | 
| 145 151 | 
             
                  when /^date(?::(?<format>.*))?$/
         | 
| 146 | 
            -
                    # TODO:  | 
| 152 | 
            +
                    # TODO: grafana does not seem to allow multiselection of variables with date format - raise an error if this happens anyway
         | 
| 147 153 | 
             
                    get_date_formatted(value, Regexp.last_match(1))
         | 
| 148 154 |  | 
| 149 155 | 
             
                  when ''
         | 
| 150 156 | 
             
                    # default
         | 
| 151 | 
            -
                    if multi?
         | 
| 157 | 
            +
                    if multi? && value.is_a?(Array)
         | 
| 152 158 | 
             
                      value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
         | 
| 153 159 | 
             
                      return value.join(',')
         | 
| 154 160 | 
             
                    end
         | 
| @@ -156,14 +162,13 @@ module Grafana | |
| 156 162 |  | 
| 157 163 | 
             
                  else
         | 
| 158 164 | 
             
                    # glob and all unknown
         | 
| 159 | 
            -
                    # TODO add check for array value properly for all cases
         | 
| 160 165 | 
             
                    return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
         | 
| 161 166 |  | 
| 162 167 | 
             
                    value
         | 
| 163 168 | 
             
                  end
         | 
| 164 169 | 
             
                end
         | 
| 165 170 |  | 
| 166 | 
            -
                # @return [Boolean] true, if the value can contain multiple selections, i.e.  | 
| 171 | 
            +
                # @return [Boolean] true, if the value can contain multiple selections, i.e. can contain an Array
         | 
| 167 172 | 
             
                def multi?
         | 
| 168 173 | 
             
                  return @config['multi'] unless @config['multi'].nil?
         | 
| 169 174 |  | 
| @@ -197,6 +202,8 @@ module Grafana | |
| 197 202 | 
             
                  until work_string.empty?
         | 
| 198 203 | 
             
                    tmp = work_string.scan(/^(?:M{1,4}|D{1,4}|d{1,4}|e|E|w{1,2}|W{1,2}|Y{4}|Y{2}|A|a|H{1,2}|
         | 
| 199 204 | 
             
                                                h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
         | 
| 205 | 
            +
             | 
| 206 | 
            +
                    # TODO: add test for sub! and switch to non-modifying frozen string action
         | 
| 200 207 | 
             
                    if tmp.empty?
         | 
| 201 208 | 
             
                      matches << work_string[0]
         | 
| 202 209 | 
             
                      work_string.sub!(/^#{work_string[0]}/, '')
         | 
| @@ -1,23 +1,58 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 | 
            +
            require_relative 'abstract_table_format_strategy'
         | 
| 4 | 
            +
            require_relative 'csv_table_format_strategy'
         | 
| 5 | 
            +
             | 
| 3 6 | 
             
            module GrafanaReporter
         | 
| 4 7 | 
             
              # @abstract Override {#pre_process} and {#post_process} in subclass.
         | 
| 5 8 | 
             
              #
         | 
| 6 9 | 
             
              # Superclass containing everything for all queries towards grafana.
         | 
| 7 10 | 
             
              class AbstractQuery
         | 
| 8 | 
            -
                attr_accessor :datasource | 
| 11 | 
            +
                attr_accessor :datasource
         | 
| 9 12 | 
             
                attr_writer :raw_query
         | 
| 10 | 
            -
                attr_reader :variables, :result, :panel
         | 
| 13 | 
            +
                attr_reader :variables, :result, :panel, :dashboard
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def timeout
         | 
| 16 | 
            +
                  # TODO: PRIO check where value priorities should be evaluated
         | 
| 17 | 
            +
                  return @variables['timeout'].raw_value if @variables['timeout']
         | 
| 18 | 
            +
                  return @variables['grafana_default_timeout'].raw_value if @variables['grafana_default_timeout']
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  nil
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                # @param grafana_obj [Object] {Grafana::Grafana}, {Grafana::Dashboard} or {Grafana::Panel} object for which the query is executed
         | 
| 24 | 
            +
                # @param opts [Hash] hash options, which may consist of:
         | 
| 25 | 
            +
                # @option opts [Hash] :variables hash of variables, which shall be used to replace variable references in the query
         | 
| 26 | 
            +
                # @option opts [Boolean] :ignore_dashboard_defaults True if {#assign_dashboard_defaults} should not be called
         | 
| 27 | 
            +
                # @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
         | 
| 28 | 
            +
                def initialize(grafana_obj, opts = {})
         | 
| 29 | 
            +
                  if grafana_obj.is_a?(Grafana::Panel)
         | 
| 30 | 
            +
                    @panel = grafana_obj
         | 
| 31 | 
            +
                    @dashboard = @panel.dashboard
         | 
| 32 | 
            +
                    @grafana = @dashboard.grafana
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  elsif grafana_obj.is_a?(Grafana::Dashboard)
         | 
| 35 | 
            +
                    @dashboard = grafana_obj
         | 
| 36 | 
            +
                    @grafana = @dashboard.grafana
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  elsif grafana_obj.is_a?(Grafana::Grafana)
         | 
| 39 | 
            +
                    @grafana = grafana_obj
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  elsif !grafana_obj
         | 
| 42 | 
            +
                    # nil given
         | 
| 11 43 |  | 
| 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 44 | 
             
                  else
         | 
| 18 | 
            -
                     | 
| 45 | 
            +
                    raise GrafanaReporterError, "Internal error in AbstractQuery: given object is of type #{grafana_obj.class.name}, which is not supported"
         | 
| 19 46 | 
             
                  end
         | 
| 20 47 | 
             
                  @variables = {}
         | 
| 48 | 
            +
                  @variables['from'] = Grafana::Variable.new(nil)
         | 
| 49 | 
            +
                  @variables['to'] = Grafana::Variable.new(nil)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                  assign_dashboard_defaults unless opts[:ignore_dashboard_defaults]
         | 
| 52 | 
            +
                  opts[:variables].each { |k, v| assign_variable(k, v) } if opts[:variables].is_a?(Hash)
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                  @translate_times = true
         | 
| 55 | 
            +
                  @translate_times = false if opts[:do_not_use_translated_times]
         | 
| 21 56 | 
             
                end
         | 
| 22 57 |  | 
| 23 58 | 
             
                # @abstract
         | 
| @@ -31,11 +66,33 @@ module GrafanaReporter | |
| 31 66 | 
             
                def execute
         | 
| 32 67 | 
             
                  return @result unless @result.nil?
         | 
| 33 68 |  | 
| 69 | 
            +
                  from = @variables['from'].raw_value
         | 
| 70 | 
            +
                  to = @variables['to'].raw_value
         | 
| 71 | 
            +
                  if @translate_times
         | 
| 72 | 
            +
                    from = translate_date(@variables['from'], @variables['grafana_report_timestamp'], false, @variables['from_timezone'] ||
         | 
| 73 | 
            +
                                          @variables['grafana_default_from_timezone'])
         | 
| 74 | 
            +
                    to = translate_date(@variables['to'], @variables['grafana_report_timestamp'], true, @variables['to_timezone'] ||
         | 
| 75 | 
            +
                                        @variables['grafana_default_to_timezone'])
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 34 78 | 
             
                  pre_process
         | 
| 35 79 | 
             
                  raise DatasourceNotSupportedError.new(@datasource, self) if @datasource.is_a?(Grafana::UnsupportedDatasource)
         | 
| 36 80 |  | 
| 37 | 
            -
                   | 
| 38 | 
            -
             | 
| 81 | 
            +
                  begin
         | 
| 82 | 
            +
                    @result = @datasource.request(from: from, to: to, raw_query: raw_query, variables: grafana_variables,
         | 
| 83 | 
            +
                                                  prepared_request: @grafana.prepare_request, timeout: timeout)
         | 
| 84 | 
            +
                  rescue ::Grafana::GrafanaError
         | 
| 85 | 
            +
                    # grafana errors will be directly passed through
         | 
| 86 | 
            +
                    raise
         | 
| 87 | 
            +
                  rescue GrafanaReporterError
         | 
| 88 | 
            +
                    # grafana errors will be directly passed through
         | 
| 89 | 
            +
                    raise
         | 
| 90 | 
            +
                  rescue StandardError => e
         | 
| 91 | 
            +
                    raise DatasourceRequestInternalError.new(@datasource, e.message)
         | 
| 92 | 
            +
                  end
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                  raise DatasourceRequestInvalidReturnValueError.new(@datasource, @result) unless datasource_response_valid?
         | 
| 95 | 
            +
             | 
| 39 96 | 
             
                  post_process
         | 
| 40 97 | 
             
                  @result
         | 
| 41 98 | 
             
                end
         | 
| @@ -66,17 +123,6 @@ module GrafanaReporter | |
| 66 123 | 
             
                  raise NotImplementedError
         | 
| 67 124 | 
             
                end
         | 
| 68 125 |  | 
| 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 126 | 
             
                # Transposes the given result.
         | 
| 81 127 | 
             
                #
         | 
| 82 128 | 
             
                # NOTE: Only the +:content+ of the given result hash is transposed. The +:header+ is ignored.
         | 
| @@ -105,10 +151,10 @@ module GrafanaReporter | |
| 105 151 |  | 
| 106 152 | 
             
                  filter_columns = filter_columns_variable.raw_value
         | 
| 107 153 | 
             
                  filter_columns.split(',').each do |filter_column|
         | 
| 108 | 
            -
                    pos = result[:header] | 
| 154 | 
            +
                    pos = result[:header].index(filter_column)
         | 
| 109 155 |  | 
| 110 156 | 
             
                    unless pos.nil?
         | 
| 111 | 
            -
                      result[:header] | 
| 157 | 
            +
                      result[:header].delete_at(pos)
         | 
| 112 158 | 
             
                      result[:content].each { |row| row.delete_at(pos) }
         | 
| 113 159 | 
             
                    end
         | 
| 114 160 | 
             
                  end
         | 
| @@ -174,7 +220,6 @@ module GrafanaReporter | |
| 174 220 | 
             
                # @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#request}.
         | 
| 175 221 | 
             
                # @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
         | 
| 176 222 | 
             
                # @return [Hash] query result with replaced values
         | 
| 177 | 
            -
                # TODO: make sure that caught errors are also visible in logger
         | 
| 178 223 | 
             
                def replace_values(result, configs)
         | 
| 179 224 | 
             
                  return result if configs.empty?
         | 
| 180 225 |  | 
| @@ -189,8 +234,11 @@ module GrafanaReporter | |
| 189 234 |  | 
| 190 235 | 
             
                      k = arr[0]
         | 
| 191 236 | 
             
                      v = arr[1]
         | 
| 192 | 
            -
             | 
| 193 | 
            -
                       | 
| 237 | 
            +
             | 
| 238 | 
            +
                      # allow keys and values to contain escaped colons or commas
         | 
| 239 | 
            +
                      k = k.gsub(/\\([:,])/, '\1')
         | 
| 240 | 
            +
                      v = v.gsub(/\\([:,])/, '\1')
         | 
| 241 | 
            +
             | 
| 194 242 | 
             
                      result[:content].map do |row|
         | 
| 195 243 | 
             
                        (row.length - 1).downto 0 do |i|
         | 
| 196 244 | 
             
                          if cols.include?(i + 1) || cols.empty?
         | 
| @@ -200,6 +248,7 @@ module GrafanaReporter | |
| 200 248 | 
             
                              begin
         | 
| 201 249 | 
             
                                row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
         | 
| 202 250 | 
             
                              rescue StandardError => e
         | 
| 251 | 
            +
                                @grafana.logger.error(e.message)
         | 
| 203 252 | 
             
                                row[i] = e.message
         | 
| 204 253 | 
             
                              end
         | 
| 205 254 |  | 
| @@ -224,6 +273,7 @@ module GrafanaReporter | |
| 224 273 | 
             
                                             end
         | 
| 225 274 | 
             
                                  end
         | 
| 226 275 | 
             
                                rescue StandardError => e
         | 
| 276 | 
            +
                                  @grafana.logger.error(e.message)
         | 
| 227 277 | 
             
                                  row[i] = e.message
         | 
| 228 278 | 
             
                                end
         | 
| 229 279 | 
             
                              end
         | 
| @@ -241,6 +291,34 @@ module GrafanaReporter | |
| 241 291 | 
             
                  result
         | 
| 242 292 | 
             
                end
         | 
| 243 293 |  | 
| 294 | 
            +
                # Used to build a table output in a custom format.
         | 
| 295 | 
            +
                # @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
         | 
| 296 | 
            +
                # @param opts [Hash] options for the formatting:
         | 
| 297 | 
            +
                # @option opts [Grafana::Variable] :row_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated`
         | 
| 298 | 
            +
                # @option opts [Grafana::Variable] :column_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated`
         | 
| 299 | 
            +
                # @option opts [Grafana::Variable] :include_headline specifies if table should contain headline, defaults to false
         | 
| 300 | 
            +
                # @option opts [Grafana::Variable] :table_formatter specifies which formatter shall be used, defaults to 'csv'
         | 
| 301 | 
            +
                # @return [String] table in custom output format
         | 
| 302 | 
            +
                def format_table_output(result, opts)
         | 
| 303 | 
            +
                  opts = { include_headline: Grafana::Variable.new('false'),
         | 
| 304 | 
            +
                           table_formatter: Grafana::Variable.new('csv'),
         | 
| 305 | 
            +
                           row_divider: Grafana::Variable.new('| '),
         | 
| 306 | 
            +
                           column_divider: Grafana::Variable.new(' | ') }.merge(opts.delete_if {|_k, v| v.nil? })
         | 
| 307 | 
            +
             | 
| 308 | 
            +
                  if opts[:table_formatter].raw_value == 'adoc_deprecated'
         | 
| 309 | 
            +
                    @grafana.logger.warn("You are using deprecated 'table_formatter' named 'adoc_deprecated', which will be "\
         | 
| 310 | 
            +
                                         "removed in a future version. Start using 'adoc_plain' or register your own "\
         | 
| 311 | 
            +
                                         "implementation of AbstractTableFormatStrategy.")
         | 
| 312 | 
            +
                    return result[:content].map do |row|
         | 
| 313 | 
            +
                      opts[:row_divider].raw_value + row.map do |item|
         | 
| 314 | 
            +
                        item.to_s.gsub('|', '\\|')
         | 
| 315 | 
            +
                      end.join(opts[:column_divider].raw_value)
         | 
| 316 | 
            +
                    end.join("\n")
         | 
| 317 | 
            +
                  end
         | 
| 318 | 
            +
             | 
| 319 | 
            +
                  AbstractTableFormatStrategy.get(opts[:table_formatter].raw_value).format(result, opts[:include_headline].raw_value.downcase == 'true')
         | 
| 320 | 
            +
                end
         | 
| 321 | 
            +
             | 
| 244 322 | 
             
                # Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
         | 
| 245 323 | 
             
                # correct timestamp. Reason is that grafana does this in the frontend, which we have
         | 
| 246 324 | 
             
                # to emulate here for the reporter.
         | 
| @@ -256,8 +334,10 @@ module GrafanaReporter | |
| 256 334 | 
             
                # @param timezone [Grafana::Variable] timezone to use, if not system timezone
         | 
| 257 335 | 
             
                # @return [String] translated date as timestamp string
         | 
| 258 336 | 
             
                def translate_date(orig_date, report_time, is_to_time, timezone = nil)
         | 
| 259 | 
            -
                  #  | 
| 337 | 
            +
                  @grafana.logger.warn("#translate_date has been called without 'report_time' - using current time as fallback.") unless report_time
         | 
| 260 338 | 
             
                  report_time ||= ::Grafana::Variable.new(Time.now.to_s)
         | 
| 339 | 
            +
                  orig_date = orig_date.raw_value if orig_date.is_a?(Grafana::Variable)
         | 
| 340 | 
            +
             | 
| 261 341 | 
             
                  return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
         | 
| 262 342 | 
             
                  return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
         | 
| 263 343 | 
             
                  return orig_date if orig_date =~ /^\d+$/
         | 
| @@ -265,24 +345,24 @@ module GrafanaReporter | |
| 265 345 | 
             
                  # check if a relative date is mentioned
         | 
| 266 346 | 
             
                  date_spec = orig_date.clone
         | 
| 267 347 |  | 
| 268 | 
            -
                  date_spec. | 
| 348 | 
            +
                  date_spec = date_spec.gsub(/^now/, '')
         | 
| 269 349 | 
             
                  raise TimeRangeUnknownError, orig_date unless date_spec
         | 
| 270 350 |  | 
| 271 351 | 
             
                  date = DateTime.parse(report_time.raw_value)
         | 
| 272 | 
            -
                  # TODO: allow from_translated or similar in ADOC template
         | 
| 352 | 
            +
                  # TODO: PRIO allow from_translated or similar in ADOC template
         | 
| 273 353 | 
             
                  date = date.new_offset(timezone.raw_value) if timezone
         | 
| 274 354 |  | 
| 275 355 | 
             
                  until date_spec.empty?
         | 
| 276 356 | 
             
                    fit_match = date_spec.match(%r{^/(?<fit>[smhdwMy])})
         | 
| 277 357 | 
             
                    if fit_match
         | 
| 278 358 | 
             
                      date = fit_date(date, fit_match[:fit], is_to_time)
         | 
| 279 | 
            -
                      date_spec. | 
| 359 | 
            +
                      date_spec = date_spec.gsub(%r{^/#{fit_match[:fit]}}, '')
         | 
| 280 360 | 
             
                    end
         | 
| 281 361 |  | 
| 282 362 | 
             
                    delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
         | 
| 283 363 | 
             
                    if delta_match
         | 
| 284 364 | 
             
                      date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
         | 
| 285 | 
            -
                      date_spec. | 
| 365 | 
            +
                      date_spec = date_spec.gsub(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/, '')
         | 
| 286 366 | 
             
                    end
         | 
| 287 367 |  | 
| 288 368 | 
             
                    raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
         | 
| @@ -296,6 +376,45 @@ module GrafanaReporter | |
| 296 376 |  | 
| 297 377 | 
             
                private
         | 
| 298 378 |  | 
| 379 | 
            +
                # Used to specify variables to be used for this query. This method ensures, that only the values of the
         | 
| 380 | 
            +
                # {Grafana::Variable} stored in the +variables+ Array are overwritten.
         | 
| 381 | 
            +
                # @param name [String] name of the variable to set
         | 
| 382 | 
            +
                # @param variable [Grafana::Variable] variable from which the {Grafana::Variable#raw_value} will be assigned to the query variables
         | 
| 383 | 
            +
                def assign_variable(name, variable)
         | 
| 384 | 
            +
                  variable = Grafana::Variable.new(variable) unless variable.is_a?(Grafana::Variable)
         | 
| 385 | 
            +
             | 
| 386 | 
            +
                  @variables[name] ||= variable
         | 
| 387 | 
            +
                  @variables[name].raw_value = variable.raw_value
         | 
| 388 | 
            +
                end
         | 
| 389 | 
            +
             | 
| 390 | 
            +
                # Sets default configurations from the given {Grafana::Dashboard} and store them as settings in the
         | 
| 391 | 
            +
                # {AbstractQuery}.
         | 
| 392 | 
            +
                #
         | 
| 393 | 
            +
                # Following data is extracted:
         | 
| 394 | 
            +
                # - +from+, by {Grafana::Dashboard#from_time}
         | 
| 395 | 
            +
                # - +to+, by {Grafana::Dashboard#to_time}
         | 
| 396 | 
            +
                # - and all variables as {Grafana::Variable}, prefixed with +var-+, as grafana also does it
         | 
| 397 | 
            +
                def assign_dashboard_defaults
         | 
| 398 | 
            +
                  return unless @dashboard
         | 
| 399 | 
            +
             | 
| 400 | 
            +
                  assign_variable('from', @dashboard.from_time)
         | 
| 401 | 
            +
                  assign_variable('to', @dashboard.to_time)
         | 
| 402 | 
            +
                  @dashboard.variables.each { |item| assign_variable("var-#{item.name}", item) }
         | 
| 403 | 
            +
                end
         | 
| 404 | 
            +
             | 
| 405 | 
            +
                def datasource_response_valid?
         | 
| 406 | 
            +
                  return false if @result.nil?
         | 
| 407 | 
            +
                  return false unless @result.is_a?(Hash)
         | 
| 408 | 
            +
                  # TODO: compare how empty valid responses look like in grafana
         | 
| 409 | 
            +
                  return true if @result.empty?
         | 
| 410 | 
            +
                  return false unless @result.key?(:header)
         | 
| 411 | 
            +
                  return false unless @result.key?(:content)
         | 
| 412 | 
            +
                  return false unless @result[:header].is_a?(Array)
         | 
| 413 | 
            +
                  return false unless @result[:content].is_a?(Array)
         | 
| 414 | 
            +
             | 
| 415 | 
            +
                  true
         | 
| 416 | 
            +
                end
         | 
| 417 | 
            +
             | 
| 299 418 | 
             
                # @return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name
         | 
| 300 419 | 
             
                #  is prefixed with +var-+
         | 
| 301 420 | 
             
                def grafana_variables
         | 
| @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 2 |  | 
| 3 3 | 
             
            module GrafanaReporter
         | 
| 4 | 
            -
              # @abstract Override {# | 
| 4 | 
            +
              # @abstract Override {#build} and {#progress}.
         | 
| 5 5 | 
             
              #
         | 
| 6 6 | 
             
              # This class is used to build a report on basis of a given configuration and
         | 
| 7 7 | 
             
              # template.
         | 
| @@ -124,8 +124,9 @@ module GrafanaReporter | |
| 124 124 | 
             
                  logger.internal_messages
         | 
| 125 125 | 
             
                end
         | 
| 126 126 |  | 
| 127 | 
            -
                # Is being called to start the report generation.
         | 
| 128 | 
            -
                #  | 
| 127 | 
            +
                # Is being called to start the report generation. To execute the specific report generation, this function
         | 
| 128 | 
            +
                # calls the abstract {#build} method with the given parameters.
         | 
| 129 | 
            +
                # @param template [String] path to the template to be used, trailing extension may be omitted, whereas {#default_template_extension} will be appended
         | 
| 129 130 | 
             
                # @param destination_file_or_path [String or File] path to the destination report or file object to use
         | 
| 130 131 | 
             
                # @param custom_attributes [Hash] custom attributes, which shall be merged with priority over the configuration
         | 
| 131 132 | 
             
                # @return [void]
         | 
| @@ -135,13 +136,31 @@ module GrafanaReporter | |
| 135 136 | 
             
                  @destination_file_or_path = destination_file_or_path
         | 
| 136 137 | 
             
                  @custom_attributes = custom_attributes
         | 
| 137 138 |  | 
| 138 | 
            -
                  # automatically add extension, if a file with  | 
| 139 | 
            -
                  @template = "#{@template}. | 
| 139 | 
            +
                  # automatically add extension, if a file with default template extension exists
         | 
| 140 | 
            +
                  @template = "#{@template}.#{self.class.default_template_extension}" if File.file?("#{@template}.#{self.class.default_template_extension}") && !File.file?(@template.to_s)
         | 
| 140 141 | 
             
                  raise MissingTemplateError, @template.to_s unless File.file?(@template.to_s)
         | 
| 141 142 |  | 
| 142 143 | 
             
                  notify(:on_before_create)
         | 
| 143 144 | 
             
                  @start_time = Time.new
         | 
| 144 145 | 
             
                  logger.info("Report started at #{@start_time}")
         | 
| 146 | 
            +
                  build
         | 
| 147 | 
            +
                rescue MissingTemplateError => e
         | 
| 148 | 
            +
                  @logger.error(e.message)
         | 
| 149 | 
            +
                  @error = [e.message]
         | 
| 150 | 
            +
                  done!
         | 
| 151 | 
            +
                  raise e
         | 
| 152 | 
            +
                rescue StandardError => e
         | 
| 153 | 
            +
                  # catch all errors during execution
         | 
| 154 | 
            +
                  died_with_error(e)
         | 
| 155 | 
            +
                  raise e
         | 
| 156 | 
            +
                ensure
         | 
| 157 | 
            +
                  done!
         | 
| 158 | 
            +
                end
         | 
| 159 | 
            +
             | 
| 160 | 
            +
                # @abstract
         | 
| 161 | 
            +
                # Needs to be overridden by the report implementation.
         | 
| 162 | 
            +
                def build(template, destination_file_or_path, custom_attributes)
         | 
| 163 | 
            +
                  raise NotImplementedError
         | 
| 145 164 | 
             
                end
         | 
| 146 165 |  | 
| 147 166 | 
             
                # Used to calculate the progress of a report. By default expects +@total_steps+ to contain the total
         | 
| @@ -167,6 +186,18 @@ module GrafanaReporter | |
| 167 186 | 
             
                  raise NotImplementedError
         | 
| 168 187 | 
             
                end
         | 
| 169 188 |  | 
| 189 | 
            +
                # @abstract
         | 
| 190 | 
            +
                # @return [String] specifying the default extension of a template file
         | 
| 191 | 
            +
                def self.default_template_extension
         | 
| 192 | 
            +
                  raise NotImplementedError
         | 
| 193 | 
            +
                end
         | 
| 194 | 
            +
             | 
| 195 | 
            +
                # @abstract
         | 
| 196 | 
            +
                # @return [String] specifying the default extension of a rendered result file
         | 
| 197 | 
            +
                def self.default_result_extension
         | 
| 198 | 
            +
                  raise NotImplementedError
         | 
| 199 | 
            +
                end
         | 
| 200 | 
            +
             | 
| 170 201 | 
             
                private
         | 
| 171 202 |  | 
| 172 203 | 
             
                # Called, if the report generation has died with an error.
         | 
| @@ -188,6 +219,7 @@ module GrafanaReporter | |
| 188 219 | 
             
                def done!
         | 
| 189 220 | 
             
                  return if @done
         | 
| 190 221 |  | 
| 222 | 
            +
                  @destination_file_or_path.close if @destination_file_or_path.is_a?(File)
         | 
| 191 223 | 
             
                  @done = true
         | 
| 192 224 | 
             
                  @end_time = Time.new
         | 
| 193 225 | 
             
                  @start_time ||= @end_time
         |