ruby-grafana-reporter 0.4.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|