ruby-grafana-reporter 0.3.0 → 0.4.4
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 +337 -170
- data/bin/ruby-grafana-reporter +5 -5
- data/lib/VERSION.rb +3 -2
- data/lib/grafana/abstract_datasource.rb +149 -0
- data/lib/grafana/dashboard.rb +1 -3
- data/lib/grafana/errors.rb +20 -5
- data/lib/grafana/grafana.rb +52 -57
- data/lib/grafana/grafana_alerts_datasource.rb +57 -0
- data/lib/grafana/grafana_annotations_datasource.rb +56 -0
- data/lib/grafana/grafana_property_datasource.rb +37 -0
- data/lib/grafana/graphite_datasource.rb +72 -0
- data/lib/grafana/image_rendering_datasource.rb +44 -0
- data/lib/grafana/influxdb_datasource.rb +70 -0
- data/lib/grafana/panel.rb +10 -4
- data/lib/grafana/prometheus_datasource.rb +67 -0
- data/lib/grafana/sql_datasource.rb +70 -0
- data/lib/grafana/unsupported_datasource.rb +7 -0
- data/lib/grafana/variable.rb +27 -21
- data/lib/grafana/webrequest.rb +71 -0
- data/lib/grafana_reporter/abstract_query.rb +478 -0
- data/lib/grafana_reporter/abstract_report.rb +152 -18
- data/lib/grafana_reporter/abstract_table_format_strategy.rb +34 -0
- data/lib/grafana_reporter/alerts_table_query.rb +43 -0
- data/lib/grafana_reporter/annotations_table_query.rb +42 -0
- data/lib/grafana_reporter/application/application.rb +28 -25
- data/lib/grafana_reporter/application/webservice.rb +80 -39
- data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +25 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +92 -0
- data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +91 -0
- data/lib/grafana_reporter/asciidoctor/help.rb +336 -313
- data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +78 -0
- data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +80 -0
- data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +74 -0
- data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +99 -0
- data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
- data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +50 -0
- data/lib/grafana_reporter/asciidoctor/report.rb +41 -82
- data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
- data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
- data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +94 -0
- data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +90 -0
- data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
- data/lib/grafana_reporter/configuration.rb +26 -8
- data/lib/grafana_reporter/console_configuration_wizard.rb +109 -67
- data/lib/grafana_reporter/csv_table_format_strategy.rb +23 -0
- data/lib/grafana_reporter/demo_report_wizard.rb +104 -0
- data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
- data/lib/grafana_reporter/erb/report.rb +36 -0
- data/lib/grafana_reporter/erb/report_jail.rb +21 -0
- data/lib/grafana_reporter/errors.rb +57 -0
- data/lib/grafana_reporter/logger/{two_way_logger.rb → two_way_delegate_logger.rb} +1 -1
- data/lib/grafana_reporter/panel_image_query.rb +25 -0
- data/lib/grafana_reporter/panel_property_query.rb +22 -0
- data/lib/grafana_reporter/query_value_query.rb +61 -0
- data/lib/grafana_reporter/report_webhook.rb +39 -0
- data/lib/ruby_grafana_extension.rb +8 -0
- data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +1 -3
- metadata +49 -38
- data/lib/grafana/abstract_panel_query.rb +0 -22
- data/lib/grafana/abstract_query.rb +0 -132
- data/lib/grafana/abstract_sql_query.rb +0 -51
- data/lib/grafana/panel_image_query.rb +0 -52
- data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -101
- data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
- data/lib/grafana_reporter/asciidoctor/errors.rb +0 -40
- data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -92
- data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -91
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -69
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -68
- data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -61
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -78
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -73
- data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -20
- data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -43
- data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -30
- data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -70
- data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -66
- data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -88
- data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -36
- data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -28
- data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -44
- data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -40
- data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -312
- data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -42
- data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -44
data/lib/grafana/variable.rb
CHANGED
@@ -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',
|
@@ -62,92 +63,96 @@ module Grafana
|
|
62
63
|
value = @raw_value
|
63
64
|
|
64
65
|
# handle value 'All' properly
|
65
|
-
|
66
|
-
if (value == 'All') || (@text == 'All')
|
66
|
+
if value == '$__all'
|
67
67
|
if !@config['options'].empty?
|
68
|
+
# this query contains predefined values, so capture them and format the values accordingly
|
69
|
+
# this happens either if type='custom' or a query, which is never updated
|
68
70
|
value = @config['options'].map { |item| item['value'] }
|
69
|
-
|
70
|
-
|
71
|
+
|
72
|
+
elsif @config['type'] == 'query' && !@config['query'].empty?
|
73
|
+
# TODO: replace variables in query, execute it and evaluate the results as if they were normally selected
|
74
|
+
# "multiFormat": contains variable replacement in "query", e.g. 'regex values' or 'glob'
|
75
|
+
# "datasource": contains name of datasource
|
71
76
|
return @config['query']
|
72
|
-
|
77
|
+
|
73
78
|
else
|
74
|
-
# TODO:
|
79
|
+
# TODO: add support for variable type: 'datasource' and 'adhoc'
|
75
80
|
end
|
76
81
|
end
|
77
82
|
|
78
83
|
case format
|
79
84
|
when 'csv'
|
80
|
-
return value.join(',').to_s if multi?
|
85
|
+
return value.join(',').to_s if multi? && value.is_a?(Array)
|
81
86
|
|
82
87
|
value.to_s
|
83
88
|
|
84
89
|
when 'distributed'
|
85
|
-
return value.join(",#{name}=") if multi?
|
90
|
+
return value.join(",#{name}=") if multi? && value.is_a?(Array)
|
86
91
|
|
87
92
|
value
|
88
93
|
when 'doublequote'
|
89
|
-
if multi?
|
94
|
+
if multi? && value.is_a?(Array)
|
90
95
|
value = value.map { |item| "\"#{item.gsub(/\\/, '\\\\').gsub(/"/, '\\"')}\"" }
|
91
96
|
return value.join(',')
|
92
97
|
end
|
93
98
|
"\"#{value.gsub(/"/, '\\"')}\""
|
94
99
|
|
95
100
|
when 'json'
|
96
|
-
if multi?
|
101
|
+
if multi? && value.is_a?(Array)
|
97
102
|
value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\\0')}\"" }
|
98
103
|
return "[#{value.join(',')}]"
|
99
104
|
end
|
100
105
|
"\"#{value.gsub(/"/, '\\"')}\""
|
101
106
|
|
102
107
|
when 'percentencode'
|
103
|
-
value = "{#{value.join(',')}}" if multi?
|
108
|
+
value = "{#{value.join(',')}}" if multi? && value.is_a?(Array)
|
104
109
|
ERB::Util.url_encode(value)
|
105
110
|
|
106
111
|
when 'pipe'
|
107
|
-
return value.join('|') if multi?
|
112
|
+
return value.join('|') if multi? && value.is_a?(Array)
|
108
113
|
|
109
114
|
value
|
110
115
|
|
111
116
|
when 'raw'
|
112
|
-
return "{#{value.join(',')}}" if multi?
|
117
|
+
return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
|
113
118
|
|
114
119
|
value
|
115
120
|
|
116
121
|
when 'regex'
|
117
|
-
if multi?
|
122
|
+
if multi? && value.is_a?(Array)
|
118
123
|
value = value.map { |item| item.gsub(%r{[/$.|\\]}, '\\\\\0') }
|
119
124
|
return "(#{value.join('|')})"
|
120
125
|
end
|
121
126
|
value.gsub(%r{[/$.|\\]}, '\\\\\0')
|
122
127
|
|
123
128
|
when 'singlequote'
|
124
|
-
if multi?
|
129
|
+
if multi? && value.is_a?(Array)
|
125
130
|
value = value.map { |item| "'#{item.gsub(/'/, '\\\\\0')}'" }
|
126
131
|
return value.join(',')
|
127
132
|
end
|
128
133
|
"'#{value.gsub(/'/, '\\\\\0')}'"
|
129
134
|
|
130
135
|
when 'sqlstring'
|
131
|
-
if multi?
|
136
|
+
if multi? && value.is_a?(Array)
|
132
137
|
value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
|
133
138
|
return value.join(',')
|
134
139
|
end
|
135
140
|
"'#{value.gsub(/'/, "''")}'"
|
136
141
|
|
137
142
|
when 'lucene'
|
138
|
-
if multi?
|
143
|
+
if multi? && value.is_a?(Array)
|
139
144
|
value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\\0')}\"" }
|
140
145
|
return "(#{value.join(' OR ')})"
|
141
146
|
end
|
142
147
|
value.gsub(%r{[" |=/\\]}, '\\\\\0')
|
143
148
|
|
144
149
|
when /^date(?::(?<format>.*))?$/
|
145
|
-
# TODO:
|
150
|
+
# TODO: grafana does not seem to allow multivariables with date format - so properly handle here as well
|
146
151
|
get_date_formatted(value, Regexp.last_match(1))
|
147
152
|
|
148
153
|
when ''
|
149
154
|
# default
|
150
|
-
if multi?
|
155
|
+
if multi? && value.is_a?(Array)
|
151
156
|
value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
|
152
157
|
return value.join(',')
|
153
158
|
end
|
@@ -155,14 +160,13 @@ module Grafana
|
|
155
160
|
|
156
161
|
else
|
157
162
|
# glob and all unknown
|
158
|
-
# TODO add check for array value properly for all cases
|
159
163
|
return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
|
160
164
|
|
161
165
|
value
|
162
166
|
end
|
163
167
|
end
|
164
168
|
|
165
|
-
# @return [Boolean] true, if the value can contain multiple selections, i.e.
|
169
|
+
# @return [Boolean] true, if the value can contain multiple selections, i.e. can contain an Array
|
166
170
|
def multi?
|
167
171
|
return @config['multi'] unless @config['multi'].nil?
|
168
172
|
|
@@ -196,6 +200,8 @@ module Grafana
|
|
196
200
|
until work_string.empty?
|
197
201
|
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}|
|
198
202
|
h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
|
203
|
+
|
204
|
+
# TODO: add test for sub! and switch to non-modifying frozen string action
|
199
205
|
if tmp.empty?
|
200
206
|
matches << work_string[0]
|
201
207
|
work_string.sub!(/^#{work_string[0]}/, '')
|
@@ -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,478 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'abstract_table_format_strategy'
|
4
|
+
require_relative 'csv_table_format_strategy'
|
5
|
+
|
6
|
+
module GrafanaReporter
|
7
|
+
# @abstract Override {#pre_process} and {#post_process} in subclass.
|
8
|
+
#
|
9
|
+
# Superclass containing everything for all queries towards grafana.
|
10
|
+
class AbstractQuery
|
11
|
+
attr_accessor :datasource
|
12
|
+
attr_writer :raw_query
|
13
|
+
attr_reader :variables, :result, :panel, :dashboard
|
14
|
+
|
15
|
+
def timeout
|
16
|
+
# TODO: 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
|
43
|
+
|
44
|
+
else
|
45
|
+
raise GrafanaReporterError, "Internal error in AbstractQuery: given object is of type #{grafana_obj.class.name}, which is not supported"
|
46
|
+
end
|
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]
|
56
|
+
end
|
57
|
+
|
58
|
+
# @abstract
|
59
|
+
#
|
60
|
+
# Runs the whole process to receive values properly from this query:
|
61
|
+
# - calls {#pre_process}
|
62
|
+
# - executes this query against the {Grafana::AbstractDatasource} implementation instance
|
63
|
+
# - calls {#post_process}
|
64
|
+
#
|
65
|
+
# @return [Hash] result of the query in standardized format
|
66
|
+
def execute
|
67
|
+
return @result unless @result.nil?
|
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
|
+
|
78
|
+
pre_process
|
79
|
+
raise DatasourceNotSupportedError.new(@datasource, self) if @datasource.is_a?(Grafana::UnsupportedDatasource)
|
80
|
+
|
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
|
+
|
96
|
+
post_process
|
97
|
+
@result
|
98
|
+
end
|
99
|
+
|
100
|
+
# Overwrite this function to extract a proper raw query value from this object.
|
101
|
+
#
|
102
|
+
# If the property +@raw_query+ is not set manually by the calling object, this
|
103
|
+
# method may be overwritten to extract the raw query from this object instead.
|
104
|
+
def raw_query
|
105
|
+
@raw_query
|
106
|
+
end
|
107
|
+
|
108
|
+
# @abstract
|
109
|
+
#
|
110
|
+
# Overwrite this function to perform all necessary actions, before the query is actually executed.
|
111
|
+
# Here you can e.g. set values of variables or similar.
|
112
|
+
#
|
113
|
+
# Especially for direct queries, it is essential to set the +@datasource+ variable at latest here in the
|
114
|
+
# subclass.
|
115
|
+
def pre_process
|
116
|
+
raise NotImplementedError
|
117
|
+
end
|
118
|
+
|
119
|
+
# @abstract
|
120
|
+
#
|
121
|
+
# Use this function to format the raw result of the @result variable to conform to the expected return value.
|
122
|
+
def post_process
|
123
|
+
raise NotImplementedError
|
124
|
+
end
|
125
|
+
|
126
|
+
# Transposes the given result.
|
127
|
+
#
|
128
|
+
# NOTE: Only the +:content+ of the given result hash is transposed. The +:header+ is ignored.
|
129
|
+
#
|
130
|
+
# @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
|
131
|
+
# @param transpose_variable [Grafana::Variable] true, if the result hash shall be transposed
|
132
|
+
# @return [Hash] transposed query result
|
133
|
+
def transpose(result, transpose_variable)
|
134
|
+
return result unless transpose_variable
|
135
|
+
return result unless transpose_variable.raw_value == 'true'
|
136
|
+
|
137
|
+
result[:content] = result[:content].transpose
|
138
|
+
|
139
|
+
result
|
140
|
+
end
|
141
|
+
|
142
|
+
# Filters columns out of the query result.
|
143
|
+
#
|
144
|
+
# Multiple columns may be filtered. Therefore the column titles have to be named in the
|
145
|
+
# {Grafana::Variable#raw_value} and have to be separated by +,+ (comma).
|
146
|
+
# @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
|
147
|
+
# @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result
|
148
|
+
# @return [Hash] filtered query result
|
149
|
+
def filter_columns(result, filter_columns_variable)
|
150
|
+
return result unless filter_columns_variable
|
151
|
+
|
152
|
+
filter_columns = filter_columns_variable.raw_value
|
153
|
+
filter_columns.split(',').each do |filter_column|
|
154
|
+
pos = result[:header].index(filter_column)
|
155
|
+
|
156
|
+
unless pos.nil?
|
157
|
+
result[:header].delete_at(pos)
|
158
|
+
result[:content].each { |row| row.delete_at(pos) }
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
result
|
163
|
+
end
|
164
|
+
|
165
|
+
# Uses the Kernel#format method to format values in the query results.
|
166
|
+
#
|
167
|
+
# The formatting will be applied separately for every column. Therefore the column formats have to be named
|
168
|
+
# in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for
|
169
|
+
# a column, no change will happen.
|
170
|
+
# @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
|
171
|
+
# @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result
|
172
|
+
# @return [Hash] formatted query result
|
173
|
+
def format_columns(result, formats)
|
174
|
+
return result unless formats
|
175
|
+
|
176
|
+
formats.text.split(',').each_index do |i|
|
177
|
+
format = formats.text.split(',')[i]
|
178
|
+
next if format.empty?
|
179
|
+
|
180
|
+
result[:content].map do |row|
|
181
|
+
next unless row.length > i
|
182
|
+
|
183
|
+
begin
|
184
|
+
row[i] = format % row[i] if row[i]
|
185
|
+
rescue StandardError => e
|
186
|
+
@grafana.logger.error(e.message)
|
187
|
+
row[i] = e.message
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
result
|
192
|
+
end
|
193
|
+
|
194
|
+
# Used to replace values in a query result according given configurations.
|
195
|
+
#
|
196
|
+
# The given variables will be applied to an appropriate column, depending
|
197
|
+
# on the naming of the variable. The variable name ending specifies the column,
|
198
|
+
# e.g. a variable named +replace_values_2+ will be applied to the second column.
|
199
|
+
#
|
200
|
+
# The {Grafana::Variable#text} needs to contain the replace specification.
|
201
|
+
# Multiple replacements can be specified by separating them with +,+. If a
|
202
|
+
# literal comma is needed, it can be escaped with a backslash: +\\,+.
|
203
|
+
#
|
204
|
+
# The rule will be separated from the replacement text with a colon +:+.
|
205
|
+
# If a literal colon is wanted, it can be escaped with a backslash: +\\:+.
|
206
|
+
#
|
207
|
+
# Examples:
|
208
|
+
# - Basic string replacement
|
209
|
+
# MyTest:ThisValue
|
210
|
+
# will replace all occurences of the text 'MyTest' with 'ThisValue'.
|
211
|
+
# - Number comparison
|
212
|
+
# <=10:OK
|
213
|
+
# will replace all values smaller or equal to 10 with 'OK'.
|
214
|
+
# - Regular expression
|
215
|
+
# ^[^ ]\\+ (\d+)$:\1 is the answer
|
216
|
+
# will replace all values matching the pattern, e.g. 'answerToAllQuestions 42' to
|
217
|
+
# '42 is the answer'. Important to know: the regular expressions always have to start
|
218
|
+
# with +^+ and end with +$+, i.e. the expression itself always has to match
|
219
|
+
# the whole content in one field.
|
220
|
+
# @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#request}.
|
221
|
+
# @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
|
222
|
+
# @return [Hash] query result with replaced values
|
223
|
+
def replace_values(result, configs)
|
224
|
+
return result if configs.empty?
|
225
|
+
|
226
|
+
configs.each do |key, formats|
|
227
|
+
cols = key.split('_')[2..-1].map(&:to_i)
|
228
|
+
|
229
|
+
formats.text.split(/(?<!\\),/).each_index do |j|
|
230
|
+
format = formats.text.split(/(?<!\\),/)[j]
|
231
|
+
|
232
|
+
arr = format.split(/(?<!\\):/)
|
233
|
+
raise MalformedReplaceValuesStatementError, format if arr.length != 2
|
234
|
+
|
235
|
+
k = arr[0]
|
236
|
+
v = arr[1]
|
237
|
+
|
238
|
+
# allow keys and values to contain escaped colons or commas
|
239
|
+
k = k.gsub(/\\([:,])/, '\1')
|
240
|
+
v = v.gsub(/\\([:,])/, '\1')
|
241
|
+
|
242
|
+
result[:content].map do |row|
|
243
|
+
(row.length - 1).downto 0 do |i|
|
244
|
+
if cols.include?(i + 1) || cols.empty?
|
245
|
+
|
246
|
+
# handle regular expressions
|
247
|
+
if k.start_with?('^') && k.end_with?('$')
|
248
|
+
begin
|
249
|
+
row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
|
250
|
+
rescue StandardError => e
|
251
|
+
@grafana.logger.error(e.message)
|
252
|
+
row[i] = e.message
|
253
|
+
end
|
254
|
+
|
255
|
+
# handle value comparisons
|
256
|
+
elsif (match = k.match(/^ *(?<operator>[<>]=?|<>|=) *(?<number>[+-]?\d+(?:\.\d+)?)$/))
|
257
|
+
skip = false
|
258
|
+
begin
|
259
|
+
val = Float(row[i])
|
260
|
+
rescue StandardError
|
261
|
+
# value cannot be converted to number, simply ignore it as the comparison does not fit here
|
262
|
+
skip = true
|
263
|
+
end
|
264
|
+
|
265
|
+
unless skip
|
266
|
+
begin
|
267
|
+
op = match[:operator].gsub(/^=$/, '==').gsub(/^<>$/, '!=')
|
268
|
+
if val.public_send(op.to_sym, Float(match[:number]))
|
269
|
+
row[i] = if v.include?('\\1')
|
270
|
+
v.gsub(/\\1/, row[i].to_s)
|
271
|
+
else
|
272
|
+
v
|
273
|
+
end
|
274
|
+
end
|
275
|
+
rescue StandardError => e
|
276
|
+
@grafana.logger.error(e.message)
|
277
|
+
row[i] = e.message
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
# handle as normal comparison
|
282
|
+
elsif row[i].to_s == k
|
283
|
+
row[i] = v
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
result
|
292
|
+
end
|
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' or register your own implementation "\
|
311
|
+
"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
|
317
|
+
end
|
318
|
+
|
319
|
+
AbstractTableFormatStrategy.get(opts[:table_formatter].raw_value).format(result, opts[:include_headline].raw_value.downcase == 'true')
|
320
|
+
end
|
321
|
+
|
322
|
+
# Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
|
323
|
+
# correct timestamp. Reason is that grafana does this in the frontend, which we have
|
324
|
+
# to emulate here for the reporter.
|
325
|
+
#
|
326
|
+
# Additionally providing this function the +report_time+ assures that all queries
|
327
|
+
# rendered within one report will use _exactly_ the same timestamp in those relative
|
328
|
+
# times, i.e. there shouldn't appear any time differences, no matter how long the
|
329
|
+
# report is running.
|
330
|
+
# @param orig_date [String] time string provided by grafana, usually +from+ or +to+.
|
331
|
+
# @param report_time [Grafana::Variable] report start time
|
332
|
+
# @param is_to_time [Boolean] true, if the time should be calculated for +to+, false if it shall be
|
333
|
+
# calculated for +from+
|
334
|
+
# @param timezone [Grafana::Variable] timezone to use, if not system timezone
|
335
|
+
# @return [String] translated date as timestamp string
|
336
|
+
def translate_date(orig_date, report_time, is_to_time, timezone = nil)
|
337
|
+
@grafana.logger.warn("#translate_date has been called without 'report_time' - using current time as fallback.") unless report_time
|
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
|
+
|
341
|
+
return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
|
342
|
+
return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
343
|
+
return orig_date if orig_date =~ /^\d+$/
|
344
|
+
|
345
|
+
# check if a relative date is mentioned
|
346
|
+
date_spec = orig_date.clone
|
347
|
+
|
348
|
+
date_spec = date_spec.gsub(/^now/, '')
|
349
|
+
raise TimeRangeUnknownError, orig_date unless date_spec
|
350
|
+
|
351
|
+
date = DateTime.parse(report_time.raw_value)
|
352
|
+
# TODO: allow from_translated or similar in ADOC template
|
353
|
+
date = date.new_offset(timezone.raw_value) if timezone
|
354
|
+
|
355
|
+
until date_spec.empty?
|
356
|
+
fit_match = date_spec.match(%r{^/(?<fit>[smhdwMy])})
|
357
|
+
if fit_match
|
358
|
+
date = fit_date(date, fit_match[:fit], is_to_time)
|
359
|
+
date_spec = date_spec.gsub(%r{^/#{fit_match[:fit]}}, '')
|
360
|
+
end
|
361
|
+
|
362
|
+
delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
|
363
|
+
if delta_match
|
364
|
+
date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
|
365
|
+
date_spec = date_spec.gsub(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/, '')
|
366
|
+
end
|
367
|
+
|
368
|
+
raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
|
369
|
+
end
|
370
|
+
|
371
|
+
# step back one second, if this is the 'to' time
|
372
|
+
date = (date.to_time - 1).to_datetime if is_to_time
|
373
|
+
|
374
|
+
(Time.at(date.to_time.to_i).to_i * 1000).to_s
|
375
|
+
end
|
376
|
+
|
377
|
+
private
|
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
|
+
|
418
|
+
# @return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name
|
419
|
+
# is prefixed with +var-+
|
420
|
+
def grafana_variables
|
421
|
+
@variables.select { |k, _v| k =~ /^var-.+/ }
|
422
|
+
end
|
423
|
+
|
424
|
+
def delta_date(date, delta_count, time_letter)
|
425
|
+
# substract specified time
|
426
|
+
case time_letter
|
427
|
+
when 's'
|
428
|
+
(date.to_time + (delta_count * 1)).to_datetime
|
429
|
+
when 'm'
|
430
|
+
(date.to_time + (delta_count * 60)).to_datetime
|
431
|
+
when 'h'
|
432
|
+
(date.to_time + (delta_count * 60 * 60)).to_datetime
|
433
|
+
when 'd'
|
434
|
+
date.next_day(delta_count)
|
435
|
+
when 'w'
|
436
|
+
date.next_day(delta_count * 7)
|
437
|
+
when 'M'
|
438
|
+
date.next_month(delta_count)
|
439
|
+
when 'y'
|
440
|
+
date.next_year(delta_count)
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
def fit_date(date, fit_letter, is_to_time)
|
445
|
+
# fit to specified time frame
|
446
|
+
case fit_letter
|
447
|
+
when 's'
|
448
|
+
date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, date.sec, date.zone)
|
449
|
+
date = (date.to_time + 1).to_datetime if is_to_time
|
450
|
+
when 'm'
|
451
|
+
date = DateTime.new(date.year, date.month, date.day, date.hour, date.min, 0, date.zone)
|
452
|
+
date = (date.to_time + 60).to_datetime if is_to_time
|
453
|
+
when 'h'
|
454
|
+
date = DateTime.new(date.year, date.month, date.day, date.hour, 0, 0, date.zone)
|
455
|
+
date = (date.to_time + 60 * 60).to_datetime if is_to_time
|
456
|
+
when 'd'
|
457
|
+
date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
|
458
|
+
date = date.next_day(1) if is_to_time
|
459
|
+
when 'w'
|
460
|
+
date = DateTime.new(date.year, date.month, date.day, 0, 0, 0, date.zone)
|
461
|
+
date = if date.wday.zero?
|
462
|
+
date.prev_day(7)
|
463
|
+
else
|
464
|
+
date.prev_day(date.wday - 1)
|
465
|
+
end
|
466
|
+
date = date.next_day(7) if is_to_time
|
467
|
+
when 'M'
|
468
|
+
date = DateTime.new(date.year, date.month, 1, 0, 0, 0, date.zone)
|
469
|
+
date = date.next_month if is_to_time
|
470
|
+
when 'y'
|
471
|
+
date = DateTime.new(date.year, 1, 1, 0, 0, 0, date.zone)
|
472
|
+
date = date.next_year if is_to_time
|
473
|
+
end
|
474
|
+
|
475
|
+
date
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|