ruby-grafana-reporter 0.4.2 → 0.5.0
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 +144 -11
- data/lib/VERSION.rb +2 -2
- data/lib/grafana/abstract_datasource.rb +9 -3
- data/lib/grafana/dashboard.rb +6 -1
- data/lib/grafana/errors.rb +4 -11
- data/lib/grafana/grafana.rb +25 -1
- data/lib/grafana/grafana_environment_datasource.rb +56 -0
- data/lib/grafana/grafana_property_datasource.rb +8 -1
- data/lib/grafana/image_rendering_datasource.rb +5 -1
- data/lib/grafana/influxdb_datasource.rb +87 -3
- data/lib/grafana/panel.rb +1 -1
- data/lib/grafana/prometheus_datasource.rb +11 -2
- data/lib/grafana/sql_datasource.rb +3 -9
- data/lib/grafana/variable.rb +67 -36
- data/lib/grafana/webrequest.rb +1 -0
- data/lib/grafana_reporter/abstract_query.rb +65 -39
- data/lib/grafana_reporter/abstract_report.rb +19 -4
- data/lib/grafana_reporter/abstract_table_format_strategy.rb +74 -0
- data/lib/grafana_reporter/alerts_table_query.rb +6 -1
- data/lib/grafana_reporter/annotations_table_query.rb +6 -1
- data/lib/grafana_reporter/application/application.rb +7 -2
- data/lib/grafana_reporter/application/webservice.rb +41 -32
- data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +27 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +3 -2
- data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +3 -2
- data/lib/grafana_reporter/asciidoctor/help.rb +470 -0
- data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +7 -5
- data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +7 -5
- data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +5 -1
- data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +6 -2
- data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +3 -0
- data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +3 -2
- data/lib/grafana_reporter/asciidoctor/report.rb +15 -13
- data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +37 -6
- data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +1 -1
- data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +6 -2
- data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +5 -1
- data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +0 -5
- data/lib/grafana_reporter/configuration.rb +27 -0
- data/lib/grafana_reporter/console_configuration_wizard.rb +5 -3
- data/lib/grafana_reporter/csv_table_format_strategy.rb +25 -0
- data/lib/grafana_reporter/demo_report_wizard.rb +3 -7
- data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
- data/lib/grafana_reporter/erb/report.rb +13 -7
- data/lib/grafana_reporter/errors.rb +9 -7
- data/lib/grafana_reporter/panel_image_query.rb +1 -1
- data/lib/grafana_reporter/query_value_query.rb +7 -1
- data/lib/grafana_reporter/report_webhook.rb +12 -8
- data/lib/grafana_reporter/reporter_environment_datasource.rb +24 -0
- metadata +9 -3
- data/lib/grafana_reporter/help.rb +0 -443
data/lib/grafana/variable.rb
CHANGED
@@ -29,14 +29,14 @@ module Grafana
|
|
29
29
|
|
30
30
|
# @param config_or_value [Hash, Object] configuration hash of a variable out of an {Dashboard} instance
|
31
31
|
# or a value of any kind.
|
32
|
-
|
32
|
+
# @param dashboard [Dashboard] parent dashboard, if applicable; especially needed for query variable
|
33
|
+
# evaluation.
|
34
|
+
def initialize(config_or_value, dashboard = nil)
|
33
35
|
if config_or_value.is_a? Hash
|
36
|
+
@dashboard = dashboard
|
34
37
|
@config = config_or_value
|
35
38
|
@name = @config['name']
|
36
|
-
|
37
|
-
@raw_value = @config['current']['value']
|
38
|
-
@text = @config['current']['text']
|
39
|
-
end
|
39
|
+
init_values
|
40
40
|
else
|
41
41
|
@config = {}
|
42
42
|
@raw_value = config_or_value
|
@@ -62,93 +62,105 @@ module Grafana
|
|
62
62
|
def value_formatted(format = '')
|
63
63
|
value = @raw_value
|
64
64
|
|
65
|
-
#
|
66
|
-
#
|
67
|
-
if
|
65
|
+
# if 'All' is selected for this template variable, capture all values properly
|
66
|
+
# (from grafana config or query) and format the results afterwards accordingly
|
67
|
+
if value == '$__all'
|
68
68
|
if !@config['options'].empty?
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
69
|
+
# this query contains predefined values, so capture them and format the values accordingly
|
70
|
+
# this happens either for type='custom' or type 'query', if it is never updated
|
71
|
+
value = @config['options'].reject { |item| item['value'] == '$__all' }.map { |item| item['value'] }
|
72
|
+
|
73
|
+
elsif @config['type'] == 'query' && !@config['query'].empty?
|
74
|
+
# variables in this configuration are not stored in grafana, i.e. if all is selected here,
|
75
|
+
# the values have to be fetched from the datasource
|
76
|
+
query = ::GrafanaReporter::QueryValueQuery.new(@dashboard)
|
77
|
+
query.datasource = @dashboard.grafana.datasource_by_name(@config['datasource'])
|
78
|
+
query.variables['result_type'] = Variable.new('object')
|
79
|
+
query.raw_query = @config['query']
|
80
|
+
result = query.execute
|
81
|
+
|
82
|
+
value = result[:content].map { |item| item[0].to_s }
|
83
|
+
|
74
84
|
else
|
75
|
-
# TODO:
|
85
|
+
# TODO: add support for variable type: 'datasource' and 'adhoc'
|
76
86
|
end
|
77
87
|
end
|
78
88
|
|
79
89
|
case format
|
80
90
|
when 'csv'
|
81
|
-
return value.join(',').to_s if multi?
|
91
|
+
return value.join(',').to_s if multi? && value.is_a?(Array)
|
82
92
|
|
83
93
|
value.to_s
|
84
94
|
|
85
95
|
when 'distributed'
|
86
|
-
return value.join(",#{name}=") if multi?
|
96
|
+
return value.join(",#{name}=") if multi? && value.is_a?(Array)
|
87
97
|
|
88
98
|
value
|
89
99
|
when 'doublequote'
|
90
|
-
if multi?
|
100
|
+
if multi? && value.is_a?(Array)
|
91
101
|
value = value.map { |item| "\"#{item.gsub(/\\/, '\\\\').gsub(/"/, '\\"')}\"" }
|
92
102
|
return value.join(',')
|
93
103
|
end
|
94
104
|
"\"#{value.gsub(/"/, '\\"')}\""
|
95
105
|
|
96
106
|
when 'json'
|
97
|
-
if multi?
|
107
|
+
if multi? && value.is_a?(Array)
|
98
108
|
value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\\0')}\"" }
|
99
109
|
return "[#{value.join(',')}]"
|
100
110
|
end
|
101
111
|
"\"#{value.gsub(/"/, '\\"')}\""
|
102
112
|
|
103
113
|
when 'percentencode'
|
104
|
-
value = "{#{value.join(',')}}" if multi?
|
114
|
+
value = "{#{value.join(',')}}" if multi? && value.is_a?(Array)
|
105
115
|
ERB::Util.url_encode(value)
|
106
116
|
|
107
117
|
when 'pipe'
|
108
|
-
return value.join('|') if multi?
|
118
|
+
return value.join('|') if multi? && value.is_a?(Array)
|
109
119
|
|
110
120
|
value
|
111
121
|
|
112
122
|
when 'raw'
|
113
|
-
return "{#{value.join(',')}}" if multi?
|
123
|
+
return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
|
114
124
|
|
115
125
|
value
|
116
126
|
|
117
127
|
when 'regex'
|
118
|
-
if multi?
|
128
|
+
if multi? && value.is_a?(Array)
|
119
129
|
value = value.map { |item| item.gsub(%r{[/$.|\\]}, '\\\\\0') }
|
120
130
|
return "(#{value.join('|')})"
|
121
131
|
end
|
122
132
|
value.gsub(%r{[/$.|\\]}, '\\\\\0')
|
123
133
|
|
124
134
|
when 'singlequote'
|
125
|
-
if multi?
|
135
|
+
if multi? && value.is_a?(Array)
|
126
136
|
value = value.map { |item| "'#{item.gsub(/'/, '\\\\\0')}'" }
|
127
137
|
return value.join(',')
|
128
138
|
end
|
129
139
|
"'#{value.gsub(/'/, '\\\\\0')}'"
|
130
140
|
|
131
141
|
when 'sqlstring'
|
132
|
-
if multi?
|
142
|
+
if multi? && value.is_a?(Array)
|
133
143
|
value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
|
134
144
|
return value.join(',')
|
135
145
|
end
|
136
146
|
"'#{value.gsub(/'/, "''")}'"
|
137
147
|
|
138
148
|
when 'lucene'
|
139
|
-
if multi?
|
149
|
+
if multi? && value.is_a?(Array)
|
140
150
|
value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\\0')}\"" }
|
141
151
|
return "(#{value.join(' OR ')})"
|
142
152
|
end
|
143
153
|
value.gsub(%r{[" |=/\\]}, '\\\\\0')
|
144
154
|
|
145
155
|
when /^date(?::(?<format>.*))?$/
|
146
|
-
|
147
|
-
|
156
|
+
if multi? && value.is_a?(Array)
|
157
|
+
raise GrafanaError, "Date format cannot be specified for a variable containing an array of values"
|
158
|
+
end
|
159
|
+
Variable.format_as_date(value, Regexp.last_match(1))
|
148
160
|
|
149
161
|
when ''
|
150
162
|
# default
|
151
|
-
if multi?
|
163
|
+
if multi? && value.is_a?(Array)
|
152
164
|
value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
|
153
165
|
return value.join(',')
|
154
166
|
end
|
@@ -162,8 +174,9 @@ module Grafana
|
|
162
174
|
end
|
163
175
|
end
|
164
176
|
|
165
|
-
# @return [Boolean] true, if the value can contain multiple selections, i.e.
|
177
|
+
# @return [Boolean] true, if the value can contain multiple selections, i.e. can contain an Array or does contain all
|
166
178
|
def multi?
|
179
|
+
return true if @raw_value == '$__all'
|
167
180
|
return @config['multi'] unless @config['multi'].nil?
|
168
181
|
|
169
182
|
@raw_value.is_a? Array
|
@@ -181,12 +194,13 @@ module Grafana
|
|
181
194
|
@text = new_text
|
182
195
|
end
|
183
196
|
|
184
|
-
|
185
|
-
|
186
|
-
# Realize time formatting according
|
197
|
+
# Applies the date format according
|
187
198
|
# {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to}
|
188
|
-
# and {https://momentjs.com/docs/#/displaying/}.
|
189
|
-
|
199
|
+
# and {https://momentjs.com/docs/#/displaying/} to a given value.
|
200
|
+
# @param value [String] time as milliseconds to be formatted
|
201
|
+
# @param format [String] format string in which the time value shall be returned
|
202
|
+
# @return [String] time converted to the specified time format
|
203
|
+
def self.format_as_date(value, format)
|
190
204
|
return (Float(value) / 1000).to_i.to_s if format == 'seconds'
|
191
205
|
return Time.at((Float(value) / 1000).to_i).utc.iso8601(3) if !format || (format == 'iso')
|
192
206
|
|
@@ -196,12 +210,13 @@ module Grafana
|
|
196
210
|
until work_string.empty?
|
197
211
|
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
212
|
h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
|
213
|
+
|
199
214
|
if tmp.empty?
|
200
215
|
matches << work_string[0]
|
201
|
-
work_string.sub
|
216
|
+
work_string = work_string.sub(/^#{work_string[0]}/, '')
|
202
217
|
else
|
203
218
|
matches << tmp[0]
|
204
|
-
work_string.sub
|
219
|
+
work_string = work_string.sub(/^#{tmp[0]}/, '')
|
205
220
|
end
|
206
221
|
end
|
207
222
|
|
@@ -213,5 +228,21 @@ module Grafana
|
|
213
228
|
|
214
229
|
Time.at((Float(value) / 1000).to_i).strftime(format_string)
|
215
230
|
end
|
231
|
+
|
232
|
+
private
|
233
|
+
|
234
|
+
def init_values
|
235
|
+
case @config['type']
|
236
|
+
when 'constant'
|
237
|
+
self.raw_value = @config['query']
|
238
|
+
|
239
|
+
else
|
240
|
+
if !@config['current'].nil?
|
241
|
+
self.raw_value = @config['current']['value']
|
242
|
+
else
|
243
|
+
raise GrafanaError.new("Grafana variable with type '#{@config['type']}' and name '#{@config['name']}' could not be handled properly. Please raise a ticket.")
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
216
247
|
end
|
217
248
|
end
|
data/lib/grafana/webrequest.rb
CHANGED
@@ -50,6 +50,7 @@ module Grafana
|
|
50
50
|
@logger.debug("Requesting #{uri} with '#{@options[:body]}' and timeout '#{timeout}'")
|
51
51
|
response = @http.request(request)
|
52
52
|
@logger.debug("Received response #{response}")
|
53
|
+
@logger.debug("HTTP response body: #{response.body}") unless response.code =~ /^2.*/
|
53
54
|
|
54
55
|
response
|
55
56
|
end
|
@@ -1,5 +1,8 @@
|
|
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
|
#
|
@@ -10,7 +13,7 @@ module GrafanaReporter
|
|
10
13
|
attr_reader :variables, :result, :panel, :dashboard
|
11
14
|
|
12
15
|
def timeout
|
13
|
-
# TODO: check where value priorities should be evaluated
|
16
|
+
# TODO: PRIO check where value priorities should be evaluated
|
14
17
|
return @variables['timeout'].raw_value if @variables['timeout']
|
15
18
|
return @variables['grafana_default_timeout'].raw_value if @variables['grafana_default_timeout']
|
16
19
|
|
@@ -41,6 +44,7 @@ module GrafanaReporter
|
|
41
44
|
else
|
42
45
|
raise GrafanaReporterError, "Internal error in AbstractQuery: given object is of type #{grafana_obj.class.name}, which is not supported"
|
43
46
|
end
|
47
|
+
@logger = @grafana ? @grafana.logger : ::Logger.new($stderr, level: :info)
|
44
48
|
@variables = {}
|
45
49
|
@variables['from'] = Grafana::Variable.new(nil)
|
46
50
|
@variables['to'] = Grafana::Variable.new(nil)
|
@@ -76,7 +80,7 @@ module GrafanaReporter
|
|
76
80
|
raise DatasourceNotSupportedError.new(@datasource, self) if @datasource.is_a?(Grafana::UnsupportedDatasource)
|
77
81
|
|
78
82
|
begin
|
79
|
-
@result = @datasource.request(from: from, to: to, raw_query: raw_query, variables:
|
83
|
+
@result = @datasource.request(from: from, to: to, raw_query: raw_query, variables: @variables,
|
80
84
|
prepared_request: @grafana.prepare_request, timeout: timeout)
|
81
85
|
rescue ::Grafana::GrafanaError
|
82
86
|
# grafana errors will be directly passed through
|
@@ -89,6 +93,7 @@ module GrafanaReporter
|
|
89
93
|
end
|
90
94
|
|
91
95
|
raise DatasourceRequestInvalidReturnValueError.new(@datasource, @result) unless datasource_response_valid?
|
96
|
+
|
92
97
|
post_process
|
93
98
|
@result
|
94
99
|
end
|
@@ -139,6 +144,8 @@ module GrafanaReporter
|
|
139
144
|
#
|
140
145
|
# Multiple columns may be filtered. Therefore the column titles have to be named in the
|
141
146
|
# {Grafana::Variable#raw_value} and have to be separated by +,+ (comma).
|
147
|
+
#
|
148
|
+
# Commas can be used in a format string, but need to be escaped by using +_,+.
|
142
149
|
# @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
|
143
150
|
# @param filter_columns_variable [Grafana::Variable] column names, which shall be removed in the query result
|
144
151
|
# @return [Hash] filtered query result
|
@@ -146,8 +153,8 @@ module GrafanaReporter
|
|
146
153
|
return result unless filter_columns_variable
|
147
154
|
|
148
155
|
filter_columns = filter_columns_variable.raw_value
|
149
|
-
filter_columns.split(
|
150
|
-
pos = result[:header].index(filter_column)
|
156
|
+
filter_columns.split(/(?<!_),/).each do |filter_column|
|
157
|
+
pos = result[:header].index(filter_column.gsub("_,", ","))
|
151
158
|
|
152
159
|
unless pos.nil?
|
153
160
|
result[:header].delete_at(pos)
|
@@ -163,23 +170,33 @@ module GrafanaReporter
|
|
163
170
|
# The formatting will be applied separately for every column. Therefore the column formats have to be named
|
164
171
|
# in the {Grafana::Variable#raw_value} and have to be separated by +,+ (comma). If no value is specified for
|
165
172
|
# a column, no change will happen.
|
173
|
+
#
|
174
|
+
# It is also possible to format milliseconds as dates by specifying date formats, e.g. +date:iso+. It is
|
175
|
+
# possible to use any date format according
|
176
|
+
# {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#from-and-to}
|
177
|
+
#
|
178
|
+
# Commas can be used in a format string, but need to be escaped by using +_,+.
|
166
179
|
# @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
|
167
180
|
# @param formats [Grafana::Variable] formats, which shall be applied to the columns in the query result
|
168
181
|
# @return [Hash] formatted query result
|
169
182
|
def format_columns(result, formats)
|
170
183
|
return result unless formats
|
171
184
|
|
172
|
-
formats.text.split(
|
173
|
-
format = formats.text.split(
|
185
|
+
formats.text.split(/(?<!_),/).each_index do |i|
|
186
|
+
format = formats.text.split(/(?<!_),/)[i].gsub("_,", ",")
|
174
187
|
next if format.empty?
|
175
188
|
|
176
189
|
result[:content].map do |row|
|
177
190
|
next unless row.length > i
|
178
191
|
|
179
192
|
begin
|
180
|
-
|
193
|
+
if format =~ /^date:/
|
194
|
+
row[i] = ::Grafana::Variable.format_as_date(row[i], format.sub(/^date:/, '')) if row[i]
|
195
|
+
else
|
196
|
+
row[i] = format % row[i] if row[i]
|
197
|
+
end
|
181
198
|
rescue StandardError => e
|
182
|
-
@
|
199
|
+
@logger.error(e.message)
|
183
200
|
row[i] = e.message
|
184
201
|
end
|
185
202
|
end
|
@@ -216,7 +233,6 @@ module GrafanaReporter
|
|
216
233
|
# @param result [Hash] preformatted query result (see {Grafana::AbstractDatasource#request}.
|
217
234
|
# @param configs [Array<Grafana::Variable>] one variable for replacing values in one column
|
218
235
|
# @return [Hash] query result with replaced values
|
219
|
-
# TODO: make sure that caught errors are also visible in logger
|
220
236
|
def replace_values(result, configs)
|
221
237
|
return result if configs.empty?
|
222
238
|
|
@@ -231,8 +247,11 @@ module GrafanaReporter
|
|
231
247
|
|
232
248
|
k = arr[0]
|
233
249
|
v = arr[1]
|
234
|
-
|
235
|
-
|
250
|
+
|
251
|
+
# allow keys and values to contain escaped colons or commas
|
252
|
+
k = k.gsub(/\\([:,])/, '\1')
|
253
|
+
v = v.gsub(/\\([:,])/, '\1')
|
254
|
+
|
236
255
|
result[:content].map do |row|
|
237
256
|
(row.length - 1).downto 0 do |i|
|
238
257
|
if cols.include?(i + 1) || cols.empty?
|
@@ -242,7 +261,7 @@ module GrafanaReporter
|
|
242
261
|
begin
|
243
262
|
row[i] = row[i].to_s.gsub(/#{k}/, v) if row[i].to_s =~ /#{k}/
|
244
263
|
rescue StandardError => e
|
245
|
-
@
|
264
|
+
@logger.error(e.message)
|
246
265
|
row[i] = e.message
|
247
266
|
end
|
248
267
|
|
@@ -267,6 +286,7 @@ module GrafanaReporter
|
|
267
286
|
end
|
268
287
|
end
|
269
288
|
rescue StandardError => e
|
289
|
+
@logger.error(e.message)
|
270
290
|
row[i] = e.message
|
271
291
|
end
|
272
292
|
end
|
@@ -284,22 +304,34 @@ module GrafanaReporter
|
|
284
304
|
result
|
285
305
|
end
|
286
306
|
|
287
|
-
# Used to build a output
|
307
|
+
# Used to build a table output in a custom format.
|
288
308
|
# @param result [Hash] preformatted sql hash, (see {Grafana::AbstractDatasource#request})
|
289
309
|
# @param opts [Hash] options for the formatting:
|
290
|
-
# @option opts [Grafana::Variable] :row_divider requested row divider for the result table
|
291
|
-
# @option opts [Grafana::Variable] :column_divider requested row divider for the result table
|
292
|
-
# @option opts [
|
293
|
-
# @option opts [
|
294
|
-
# @
|
310
|
+
# @option opts [Grafana::Variable] :row_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated`
|
311
|
+
# @option opts [Grafana::Variable] :column_divider requested row divider for the result table, only to be used with table_formatter `adoc_deprecated`
|
312
|
+
# @option opts [Grafana::Variable] :include_headline specifies if table should contain headline, defaults to false
|
313
|
+
# @option opts [Grafana::Variable] :table_formatter specifies which formatter shall be used, defaults to 'csv'
|
314
|
+
# @option opts [Grafana::Variable] :transposed specifies whether the result table is transposed
|
315
|
+
# @return [String] table in custom output format
|
295
316
|
def format_table_output(result, opts)
|
296
|
-
opts = {
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
317
|
+
opts = { include_headline: Grafana::Variable.new('false'),
|
318
|
+
table_formatter: Grafana::Variable.new('csv'),
|
319
|
+
row_divider: Grafana::Variable.new('| '),
|
320
|
+
column_divider: Grafana::Variable.new(' | '),
|
321
|
+
transpose: Grafana::Variable.new('false') }.merge(opts.delete_if {|_k, v| v.nil? })
|
322
|
+
|
323
|
+
if opts[:table_formatter].raw_value == 'adoc_deprecated'
|
324
|
+
@logger.warn("You are using deprecated 'table_formatter' named 'adoc_deprecated', which will be "\
|
325
|
+
"removed in a future version. Start using 'adoc_plain' or register your own "\
|
326
|
+
"implementation of AbstractTableFormatStrategy.")
|
327
|
+
return result[:content].map do |row|
|
328
|
+
opts[:row_divider].raw_value + row.map do |item|
|
329
|
+
item.to_s.gsub('|', '\\|')
|
330
|
+
end.join(opts[:column_divider].raw_value)
|
331
|
+
end.join("\n")
|
302
332
|
end
|
333
|
+
|
334
|
+
AbstractTableFormatStrategy.get(opts[:table_formatter].raw_value).format(result, opts[:include_headline].raw_value.downcase == 'true', opts[:transpose].raw_value.downcase == 'true')
|
303
335
|
end
|
304
336
|
|
305
337
|
# Used to translate the relative date strings used by grafana, e.g. +now-5d/w+ to the
|
@@ -317,9 +349,10 @@ module GrafanaReporter
|
|
317
349
|
# @param timezone [Grafana::Variable] timezone to use, if not system timezone
|
318
350
|
# @return [String] translated date as timestamp string
|
319
351
|
def translate_date(orig_date, report_time, is_to_time, timezone = nil)
|
320
|
-
#
|
352
|
+
@logger.warn("#translate_date has been called without 'report_time' - using current time as fallback.") unless report_time
|
321
353
|
report_time ||= ::Grafana::Variable.new(Time.now.to_s)
|
322
354
|
orig_date = orig_date.raw_value if orig_date.is_a?(Grafana::Variable)
|
355
|
+
|
323
356
|
return (DateTime.parse(report_time.raw_value).to_time.to_i * 1000).to_s unless orig_date
|
324
357
|
return orig_date if orig_date =~ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/
|
325
358
|
return orig_date if orig_date =~ /^\d+$/
|
@@ -327,24 +360,24 @@ module GrafanaReporter
|
|
327
360
|
# check if a relative date is mentioned
|
328
361
|
date_spec = orig_date.clone
|
329
362
|
|
330
|
-
date_spec.
|
363
|
+
date_spec = date_spec.gsub(/^now/, '')
|
331
364
|
raise TimeRangeUnknownError, orig_date unless date_spec
|
332
365
|
|
333
366
|
date = DateTime.parse(report_time.raw_value)
|
334
|
-
# TODO: allow from_translated or similar in ADOC template
|
367
|
+
# TODO: PRIO allow from_translated or similar in ADOC template
|
335
368
|
date = date.new_offset(timezone.raw_value) if timezone
|
336
369
|
|
337
370
|
until date_spec.empty?
|
338
371
|
fit_match = date_spec.match(%r{^/(?<fit>[smhdwMy])})
|
339
372
|
if fit_match
|
340
373
|
date = fit_date(date, fit_match[:fit], is_to_time)
|
341
|
-
date_spec.
|
374
|
+
date_spec = date_spec.gsub(%r{^/#{fit_match[:fit]}}, '')
|
342
375
|
end
|
343
376
|
|
344
377
|
delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
|
345
378
|
if delta_match
|
346
379
|
date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
|
347
|
-
date_spec.
|
380
|
+
date_spec = date_spec.gsub(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/, '')
|
348
381
|
end
|
349
382
|
|
350
383
|
raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
|
@@ -387,22 +420,15 @@ module GrafanaReporter
|
|
387
420
|
def datasource_response_valid?
|
388
421
|
return false if @result.nil?
|
389
422
|
return false unless @result.is_a?(Hash)
|
390
|
-
|
391
|
-
return
|
392
|
-
return false unless @result.
|
393
|
-
return false unless @result.has_key?(:content)
|
423
|
+
return false if @result.empty?
|
424
|
+
return false unless @result.key?(:header)
|
425
|
+
return false unless @result.key?(:content)
|
394
426
|
return false unless @result[:header].is_a?(Array)
|
395
427
|
return false unless @result[:content].is_a?(Array)
|
396
428
|
|
397
429
|
true
|
398
430
|
end
|
399
431
|
|
400
|
-
# @return [Hash<String, Variable>] all grafana variables stored in this query, i.e. the variable name
|
401
|
-
# is prefixed with +var-+
|
402
|
-
def grafana_variables
|
403
|
-
@variables.select { |k, _v| k =~ /^var-.+/ }
|
404
|
-
end
|
405
|
-
|
406
432
|
def delta_date(date, delta_count, time_letter)
|
407
433
|
# substract specified time
|
408
434
|
case time_letter
|
@@ -126,7 +126,7 @@ module GrafanaReporter
|
|
126
126
|
|
127
127
|
# Is being called to start the report generation. To execute the specific report generation, this function
|
128
128
|
# calls the abstract {#build} method with the given parameters.
|
129
|
-
# @param template [String] path to the template to be used, trailing
|
129
|
+
# @param template [String] path to the template to be used, trailing extension may be omitted, whereas {#default_template_extension} will be appended
|
130
130
|
# @param destination_file_or_path [String or File] path to the destination report or file object to use
|
131
131
|
# @param custom_attributes [Hash] custom attributes, which shall be merged with priority over the configuration
|
132
132
|
# @return [void]
|
@@ -136,14 +136,16 @@ module GrafanaReporter
|
|
136
136
|
@destination_file_or_path = destination_file_or_path
|
137
137
|
@custom_attributes = custom_attributes
|
138
138
|
|
139
|
-
# automatically add extension, if a file with
|
140
|
-
@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)
|
141
141
|
raise MissingTemplateError, @template.to_s unless File.file?(@template.to_s)
|
142
142
|
|
143
143
|
notify(:on_before_create)
|
144
144
|
@start_time = Time.new
|
145
145
|
logger.info("Report started at #{@start_time}")
|
146
|
-
|
146
|
+
logger.info("You are running ruby-grafana-reporter version #{GRAFANA_REPORTER_VERSION.join('.')}.")
|
147
|
+
logger.info("A newer version is released. Check out https://github.com/divinity666/ruby-grafana-reporter/releases/latest") unless @config.latest_version_check_ok?
|
148
|
+
build
|
147
149
|
rescue MissingTemplateError => e
|
148
150
|
@logger.error(e.message)
|
149
151
|
@error = [e.message]
|
@@ -186,6 +188,18 @@ module GrafanaReporter
|
|
186
188
|
raise NotImplementedError
|
187
189
|
end
|
188
190
|
|
191
|
+
# @abstract
|
192
|
+
# @return [String] specifying the default extension of a template file
|
193
|
+
def self.default_template_extension
|
194
|
+
raise NotImplementedError
|
195
|
+
end
|
196
|
+
|
197
|
+
# @abstract
|
198
|
+
# @return [String] specifying the default extension of a rendered result file
|
199
|
+
def self.default_result_extension
|
200
|
+
raise NotImplementedError
|
201
|
+
end
|
202
|
+
|
189
203
|
private
|
190
204
|
|
191
205
|
# Called, if the report generation has died with an error.
|
@@ -207,6 +221,7 @@ module GrafanaReporter
|
|
207
221
|
def done!
|
208
222
|
return if @done
|
209
223
|
|
224
|
+
@destination_file_or_path.close if @destination_file_or_path.is_a?(File)
|
210
225
|
@done = true
|
211
226
|
@end_time = Time.new
|
212
227
|
@start_time ||= @end_time
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GrafanaReporter
|
4
|
+
# The abstract base class, which is to be implemented for different table
|
5
|
+
# output formats. By implementing this class, you e.g. can decide if a table
|
6
|
+
# will be formatted as CSV, JSON or any other format.
|
7
|
+
class AbstractTableFormatStrategy
|
8
|
+
@@subclasses = []
|
9
|
+
|
10
|
+
def self.inherited(obj)
|
11
|
+
@@subclasses << obj
|
12
|
+
end
|
13
|
+
|
14
|
+
# @param abbreviation [String] name of the requested table format strategy
|
15
|
+
# @return [AbstractTableFormatStrategy] fitting strategy instance for the given name
|
16
|
+
def self.get(abbreviation)
|
17
|
+
@@subclasses.select { |item| item.abbreviation == abbreviation }.first.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# @abstract
|
21
|
+
# @return [String] short name of the current stategy, under which it shall be accessible
|
22
|
+
def self.abbreviation
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
# Used to format a given content array to the desired output format. The default
|
27
|
+
# implementation applies the {#format_rules} to create a custom string export. If
|
28
|
+
# this is not sufficient for a desired table format, you may simply overwrite this
|
29
|
+
# function to have full freedom about the desired output.
|
30
|
+
# @param content [Hash] datasource table result
|
31
|
+
# @param include_headline [Boolean] true, if headline should be included in result
|
32
|
+
# @param transposed [Boolean] true, if result array is in transposed format
|
33
|
+
# @return [String] formatted in table format
|
34
|
+
def format(content, include_headline, transposed)
|
35
|
+
result = content[:content]
|
36
|
+
|
37
|
+
# add the headline at the correct position to the content array
|
38
|
+
if include_headline
|
39
|
+
if transposed
|
40
|
+
result.each_index do |i|
|
41
|
+
result[i] = [content[:header][i]] + result[i]
|
42
|
+
end
|
43
|
+
else
|
44
|
+
result = result.unshift(content[:header])
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# translate the content to a table
|
49
|
+
result.map do |row|
|
50
|
+
format_rules[:row_start] + row.map do |item|
|
51
|
+
value = item.to_s
|
52
|
+
if format_rules[:replace_string_or_regex]
|
53
|
+
value = value.gsub(format_rules[:replace_string_or_regex], format_rules[:replacement])
|
54
|
+
end
|
55
|
+
|
56
|
+
format_rules[:cell_start] + value + format_rules[:cell_end]
|
57
|
+
end.join(format_rules[:between_cells])
|
58
|
+
end.join(format_rules[:row_end])
|
59
|
+
end
|
60
|
+
|
61
|
+
# Formatting rules, which are applied to build the table output format.
|
62
|
+
def format_rules
|
63
|
+
{
|
64
|
+
row_start: '',
|
65
|
+
row_end: '',
|
66
|
+
cell_start: '',
|
67
|
+
between_cells: '',
|
68
|
+
cell_end: '',
|
69
|
+
replace_string_or_regex: nil,
|
70
|
+
replacement: ''
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -33,7 +33,12 @@ module GrafanaReporter
|
|
33
33
|
@result = replace_values(@result, @variables.select { |k, _v| k =~ /^replace_values_\d+/ })
|
34
34
|
@result = filter_columns(@result, @variables['filter_columns'])
|
35
35
|
|
36
|
-
@result = format_table_output(@result,
|
36
|
+
@result = format_table_output(@result,
|
37
|
+
row_divider: @variables['row_divider'],
|
38
|
+
column_divider: @variables['column_divider'],
|
39
|
+
table_formatter: @variables['table_formatter'],
|
40
|
+
include_headline: @variables['include_headline'],
|
41
|
+
transpose: @variables['transpose'])
|
37
42
|
end
|
38
43
|
end
|
39
44
|
end
|
@@ -32,7 +32,12 @@ module GrafanaReporter
|
|
32
32
|
@result = replace_values(@result, @variables.select { |k, _v| k =~ /^replace_values_\d+/ })
|
33
33
|
@result = filter_columns(@result, @variables['filter_columns'])
|
34
34
|
|
35
|
-
@result = format_table_output(@result,
|
35
|
+
@result = format_table_output(@result,
|
36
|
+
row_divider: @variables['row_divider'],
|
37
|
+
column_divider: @variables['column_divider'],
|
38
|
+
table_formatter: @variables['table_formatter'],
|
39
|
+
include_headline: @variables['include_headline'],
|
40
|
+
transpose: @variables['transpose'])
|
36
41
|
end
|
37
42
|
end
|
38
43
|
end
|
@@ -13,7 +13,6 @@ module GrafanaReporter
|
|
13
13
|
# It can be run to test the grafana connection, render a single template
|
14
14
|
# or run as a service.
|
15
15
|
class Application
|
16
|
-
|
17
16
|
# Contains the {Configuration} object of the application.
|
18
17
|
attr_accessor :config
|
19
18
|
|
@@ -140,7 +139,13 @@ module GrafanaReporter
|
|
140
139
|
|
141
140
|
when Configuration::MODE_SINGLE_RENDER
|
142
141
|
begin
|
143
|
-
config.report_class.
|
142
|
+
template_ext = config.report_class.default_template_extension
|
143
|
+
report_ext = config.report_class.default_result_extension
|
144
|
+
default_to_file = File.basename(config.template.to_s.gsub(/(?:\.#{template_ext})?$/, ".#{report_ext}"))
|
145
|
+
|
146
|
+
to_file = config.to_file
|
147
|
+
to_file = "#{config.reports_folder}#{default_to_file}" if to_file == true
|
148
|
+
config.report_class.new(config).create_report(config.template, to_file)
|
144
149
|
rescue StandardError => e
|
145
150
|
puts "#{e.message}\n#{e.backtrace.join("\n")}"
|
146
151
|
end
|