ruby-grafana-reporter 0.1.7 → 0.2.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 +166 -339
- data/bin/ruby-grafana-reporter +5 -4
- data/lib/VERSION.rb +5 -3
- data/lib/grafana/abstract_panel_query.rb +22 -20
- data/lib/grafana/abstract_query.rb +132 -127
- data/lib/grafana/abstract_sql_query.rb +51 -42
- data/lib/grafana/dashboard.rb +77 -66
- data/lib/grafana/errors.rb +66 -61
- data/lib/grafana/grafana.rb +130 -131
- data/lib/grafana/panel.rb +41 -39
- data/lib/grafana/panel_image_query.rb +52 -49
- data/lib/grafana/variable.rb +217 -259
- data/lib/grafana_reporter/abstract_report.rb +112 -109
- data/lib/grafana_reporter/application/application.rb +404 -229
- data/lib/grafana_reporter/application/errors.rb +33 -30
- data/lib/grafana_reporter/application/webservice.rb +231 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +104 -99
- data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +99 -96
- data/lib/grafana_reporter/asciidoctor/errors.rb +40 -37
- data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +92 -86
- data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +91 -86
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +69 -67
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +68 -65
- data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +61 -58
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +78 -75
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +73 -70
- data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +20 -18
- data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +43 -41
- data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +70 -67
- data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +66 -65
- data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +61 -57
- data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +34 -32
- data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +25 -23
- data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +44 -43
- data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +38 -36
- data/lib/grafana_reporter/asciidoctor/query_mixin.rb +310 -309
- data/lib/grafana_reporter/asciidoctor/report.rb +177 -159
- data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +37 -34
- data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +39 -32
- data/lib/grafana_reporter/configuration.rb +257 -326
- data/lib/grafana_reporter/errors.rb +48 -38
- data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
- data/lib/ruby-grafana-reporter.rb +29 -27
- metadata +10 -23
@@ -1,326 +1,257 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
# @return [String]
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
end
|
102
|
-
|
103
|
-
#
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
#
|
123
|
-
#
|
124
|
-
def
|
125
|
-
get_config('
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
#
|
132
|
-
#
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
end
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
raise ConfigurationError, "Unhandled configuration data type '#{subject.class}'."
|
259
|
-
end
|
260
|
-
else
|
261
|
-
# apply to single item
|
262
|
-
if subject.is_a?(Hash)
|
263
|
-
if !subject.key?(key) && (min_occurence > 0)
|
264
|
-
raise ConfigurationDoesNotMatchSchemaError.new(key, 'occur', min_occurence, 0)
|
265
|
-
end
|
266
|
-
if !subject[key].is_a?(type) && subject.key?(key)
|
267
|
-
raise ConfigurationDoesNotMatchSchemaError.new(key, 'be a', type, subject[key].class)
|
268
|
-
end
|
269
|
-
|
270
|
-
elsif subject.is_a?(Array)
|
271
|
-
if (subject.length < key) && (min_occurence > subject.length)
|
272
|
-
raise ConfigurationDoesNotMatchSchemaError.new(key, 'occur', min_occurence, subject.length)
|
273
|
-
end
|
274
|
-
if !subject[key].is_a?(type) && (subject.length >= key)
|
275
|
-
raise ConfigurationDoesNotMatchSchemaError.new(key, 'be a', type, subject[key].class)
|
276
|
-
end
|
277
|
-
|
278
|
-
else
|
279
|
-
raise ConfigurationError, "Unhandled configuration data type '#{subject.class}'."
|
280
|
-
end
|
281
|
-
end
|
282
|
-
end
|
283
|
-
|
284
|
-
# validate also if subject has further configurations, which are not known by the reporter
|
285
|
-
subject.each do |item, subitems|
|
286
|
-
schema_config = schema[item] || schema[nil]
|
287
|
-
if schema_config.nil?
|
288
|
-
logger.warn("Item '#{item}' in configuration is unknown to the reporter and will be ignored")
|
289
|
-
end
|
290
|
-
end
|
291
|
-
end
|
292
|
-
|
293
|
-
def schema
|
294
|
-
{
|
295
|
-
'grafana' =>
|
296
|
-
[
|
297
|
-
Hash, 1,
|
298
|
-
{
|
299
|
-
nil =>
|
300
|
-
[
|
301
|
-
Hash, 1,
|
302
|
-
{
|
303
|
-
'host' => [String, 1],
|
304
|
-
'api_key' => [String, 0],
|
305
|
-
'datasources' => [Hash, 0, { nil => [Integer, 1] }]
|
306
|
-
}
|
307
|
-
]
|
308
|
-
}
|
309
|
-
],
|
310
|
-
'default-document-attributes' => [Hash, 0],
|
311
|
-
'grafana-reporter' =>
|
312
|
-
[
|
313
|
-
Hash, 0,
|
314
|
-
{
|
315
|
-
'run-mode' => [String, 0],
|
316
|
-
'test-instance' => [String, 0],
|
317
|
-
'templates-folder' => [String, 0],
|
318
|
-
'reports-folder' => [String, 0],
|
319
|
-
'report-retention' => [Integer, 0],
|
320
|
-
'webservice-port' => [Integer, 0]
|
321
|
-
}
|
322
|
-
]
|
323
|
-
}
|
324
|
-
end
|
325
|
-
end
|
326
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# In this namespace all objects needed for the grafana reporter are collected.
|
4
|
+
module GrafanaReporter
|
5
|
+
# Used to store the whole settings, which are necessary to run the reporter.
|
6
|
+
# It can read configuration files, but might also be configured programmatically.
|
7
|
+
#
|
8
|
+
# This class also contains a function {#validate}, which ensures that the
|
9
|
+
# provided settings are set properly.
|
10
|
+
#
|
11
|
+
# Using this class is embedded in the {Application::Application#configure_and_run}.
|
12
|
+
#
|
13
|
+
class Configuration
|
14
|
+
# @return [AbstractReport] specific report class, which should be used.
|
15
|
+
attr_accessor :report_class
|
16
|
+
|
17
|
+
# Returned by {#mode} if only a connection test shall be executed.
|
18
|
+
MODE_CONNECTION_TEST = 'test'
|
19
|
+
# Returned by {#mode} if only one configured report shall be rendered.
|
20
|
+
MODE_SINGLE_RENDER = 'single-render'
|
21
|
+
# Returned by {#mode} if the default webservice shall be started.
|
22
|
+
MODE_SERVICE = 'webservice'
|
23
|
+
|
24
|
+
# Used to access the configuration hash. To make sure, that the configuration is
|
25
|
+
# valid, call {#validate}.
|
26
|
+
#
|
27
|
+
# NOTE: This function overwrites all existing configurations
|
28
|
+
attr_accessor :config
|
29
|
+
|
30
|
+
def initialize
|
31
|
+
@config = {}
|
32
|
+
@logger = ::Logger.new($stderr, level: :unknown)
|
33
|
+
# TODO: set report class somewhere else, but make it known here
|
34
|
+
self.report_class = Asciidoctor::Report
|
35
|
+
end
|
36
|
+
|
37
|
+
attr_accessor :logger
|
38
|
+
|
39
|
+
# @return [String] mode, in which the reporting shall be executed. One of {MODE_CONNECTION_TEST},
|
40
|
+
# {MODE_SINGLE_RENDER} and {MODE_SERVICE}.
|
41
|
+
def mode
|
42
|
+
if (get_config('grafana-reporter:run-mode') != MODE_CONNECTION_TEST) &&
|
43
|
+
(get_config('grafana-reporter:run-mode') != MODE_SINGLE_RENDER)
|
44
|
+
return MODE_SERVICE
|
45
|
+
end
|
46
|
+
|
47
|
+
get_config('grafana-reporter:run-mode')
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [String] full path of configured report template. Only needed in {MODE_SINGLE_RENDER}.
|
51
|
+
def template
|
52
|
+
return nil if get_config('default-document-attributes:var-template').nil?
|
53
|
+
"#{templates_folder}#{get_config('default-document-attributes:var-template')}.adoc"
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [String] destination filename for the report in {MODE_SINGLE_RENDER}.
|
57
|
+
def to_file
|
58
|
+
return get_config('to_file') || true if mode == MODE_SINGLE_RENDER
|
59
|
+
|
60
|
+
get_config('to_file')
|
61
|
+
end
|
62
|
+
|
63
|
+
# @return [Array<String>] names of the configured grafana_instances.
|
64
|
+
def grafana_instances
|
65
|
+
instances = get_config('grafana')
|
66
|
+
instances.keys
|
67
|
+
end
|
68
|
+
|
69
|
+
# @param instance [String] grafana instance name, for which the value shall be retrieved.
|
70
|
+
# @return [String] configured 'host' for the requested grafana instance.
|
71
|
+
def grafana_host(instance = 'default')
|
72
|
+
host = get_config("grafana:#{instance}:host")
|
73
|
+
raise GrafanaInstanceWithoutHostError, instance if host.nil?
|
74
|
+
|
75
|
+
host
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param instance [String] grafana instance name, for which the value shall be retrieved.
|
79
|
+
# @return [String] configured 'api_key' for the requested grafana instance.
|
80
|
+
def grafana_api_key(instance = 'default')
|
81
|
+
get_config("grafana:#{instance}:api_key")
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param instance [String] grafana instance name, for which the value shall be retrieved.
|
85
|
+
# @return [Hash<String,Integer>] configured datasources for the requested grafana instance. Name as key,
|
86
|
+
# ID as value.
|
87
|
+
def grafana_datasources(instance = 'default')
|
88
|
+
hash = get_config("grafana:#{instance}:datasources")
|
89
|
+
return nil if hash.nil?
|
90
|
+
|
91
|
+
hash.map { |k, v| [k, v] }.to_h
|
92
|
+
end
|
93
|
+
|
94
|
+
# @return [String] configured folder, in which the report templates are stored including trailing slash.
|
95
|
+
# By default: current folder.
|
96
|
+
def templates_folder
|
97
|
+
result = get_config('grafana-reporter:templates-folder') || '.'
|
98
|
+
return result.sub(%r{/*$}, '/') unless result.empty?
|
99
|
+
|
100
|
+
result
|
101
|
+
end
|
102
|
+
|
103
|
+
# Returns configured folder, in which temporary images during report generation
|
104
|
+
# shall be stored including trailing slash. Folder has to be a subfolder of
|
105
|
+
# {#templates_folder}. By default: current folder.
|
106
|
+
# @return [String] configured folder, in which temporary images shall be stored.
|
107
|
+
def images_folder
|
108
|
+
img_path = templates_folder
|
109
|
+
img_path = if img_path.empty?
|
110
|
+
get_config('default-document-attributes:imagesdir').to_s
|
111
|
+
else
|
112
|
+
img_path + get_config('default-document-attributes:imagesdir').to_s
|
113
|
+
end
|
114
|
+
img_path.empty? ? './' : img_path.sub(%r{/*$}, '/')
|
115
|
+
end
|
116
|
+
|
117
|
+
# @return [String] name of grafana instance, against which a test shall be executed
|
118
|
+
def test_instance
|
119
|
+
get_config('grafana-reporter:test-instance')
|
120
|
+
end
|
121
|
+
|
122
|
+
# @return [String] configured folder, in which the reports shall be stored including trailing slash.
|
123
|
+
# By default: current folder.
|
124
|
+
def reports_folder
|
125
|
+
result = get_config('grafana-reporter:reports-folder') || '.'
|
126
|
+
return result.sub(%r{/*$}, '/') unless result.empty?
|
127
|
+
|
128
|
+
result
|
129
|
+
end
|
130
|
+
|
131
|
+
# @return [Integer] how many hours a generated report shall be retained, before it shall be deleted.
|
132
|
+
# By default: 24.
|
133
|
+
def report_retention
|
134
|
+
get_config('grafana-reporter:report-retention') || 24
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [Integer] port, on which the webserver shall run. By default: 8815.
|
138
|
+
def webserver_port
|
139
|
+
get_config('grafana-reporter:webservice-port') || 8815
|
140
|
+
end
|
141
|
+
|
142
|
+
# The configuration made with the setting 'default-document-attributes' will
|
143
|
+
# be passed 1:1 to the asciidoctor report service. It can be used to preconfigure
|
144
|
+
# whatever is essential for the needed report renderings.
|
145
|
+
# @return [Hash] configured document attributes
|
146
|
+
def default_document_attributes
|
147
|
+
get_config('default-document-attributes') || {}
|
148
|
+
end
|
149
|
+
|
150
|
+
# This function shall be called, before the configuration object is used in the
|
151
|
+
# {Application::Application#run}. It ensures, that everything is setup properly
|
152
|
+
# and all necessary folders exist. Appropriate errors are raised in case of errors.
|
153
|
+
# @return [void]
|
154
|
+
def validate
|
155
|
+
validate_schema(schema, @config)
|
156
|
+
|
157
|
+
# check if set folders exist
|
158
|
+
raise FolderDoesNotExistError.new(reports_folder, 'reports-folder') unless File.directory?(reports_folder)
|
159
|
+
raise FolderDoesNotExistError.new(templates_folder, 'templates-folder') unless File.directory?(templates_folder)
|
160
|
+
raise FolderDoesNotExistError.new(images_folder, 'images-folder') unless File.directory?(images_folder)
|
161
|
+
end
|
162
|
+
|
163
|
+
private
|
164
|
+
|
165
|
+
def get_config(path)
|
166
|
+
return if path.nil?
|
167
|
+
|
168
|
+
cur_pos = @config
|
169
|
+
path.split(':').each do |subpath|
|
170
|
+
cur_pos = cur_pos[subpath] if cur_pos
|
171
|
+
end
|
172
|
+
cur_pos
|
173
|
+
end
|
174
|
+
|
175
|
+
def validate_schema(schema, subject)
|
176
|
+
return nil if subject.nil?
|
177
|
+
|
178
|
+
schema.each do |key, config|
|
179
|
+
type, min_occurence, next_level = config
|
180
|
+
|
181
|
+
validate_schema(next_level, subject[key]) if next_level
|
182
|
+
|
183
|
+
if key.nil?
|
184
|
+
# apply to all on this level
|
185
|
+
case
|
186
|
+
when subject.is_a?(Hash)
|
187
|
+
if subject.length < min_occurence
|
188
|
+
raise ConfigurationDoesNotMatchSchemaError.new(key, 'occur', min_occurence, subject.length)
|
189
|
+
end
|
190
|
+
|
191
|
+
subject.each do |k, _v|
|
192
|
+
sub_scheme = {}
|
193
|
+
sub_scheme[k] = schema[nil]
|
194
|
+
validate_schema(sub_scheme, subject)
|
195
|
+
end
|
196
|
+
|
197
|
+
else
|
198
|
+
raise ConfigurationError, "Unhandled configuration data type '#{subject.class}'."
|
199
|
+
end
|
200
|
+
|
201
|
+
# apply to single item
|
202
|
+
elsif subject.is_a?(Hash)
|
203
|
+
if !subject.key?(key) && min_occurence.positive?
|
204
|
+
raise ConfigurationDoesNotMatchSchemaError.new(key, 'occur', min_occurence, 0)
|
205
|
+
end
|
206
|
+
if !subject[key].is_a?(type) && subject.key?(key)
|
207
|
+
raise ConfigurationDoesNotMatchSchemaError.new(key, 'be a', type, subject[key].class)
|
208
|
+
end
|
209
|
+
|
210
|
+
else
|
211
|
+
raise ConfigurationError, "Unhandled configuration data type '#{subject.class}'."
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# validate also if subject has further configurations, which are not known by the reporter
|
216
|
+
subject.each do |item, _subitems|
|
217
|
+
schema_config = schema[item] || schema[nil]
|
218
|
+
if schema_config.nil?
|
219
|
+
logger.warn("Item '#{item}' in configuration is unknown to the reporter and will be ignored")
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def schema
|
225
|
+
{
|
226
|
+
'grafana' =>
|
227
|
+
[
|
228
|
+
Hash, 1,
|
229
|
+
{
|
230
|
+
nil =>
|
231
|
+
[
|
232
|
+
Hash, 1,
|
233
|
+
{
|
234
|
+
'host' => [String, 1],
|
235
|
+
'api_key' => [String, 0],
|
236
|
+
'datasources' => [Hash, 0, { nil => [Integer, 1] }]
|
237
|
+
}
|
238
|
+
]
|
239
|
+
}
|
240
|
+
],
|
241
|
+
'default-document-attributes' => [Hash, 0],
|
242
|
+
'grafana-reporter' =>
|
243
|
+
[
|
244
|
+
Hash, 0,
|
245
|
+
{
|
246
|
+
'run-mode' => [String, 0],
|
247
|
+
'test-instance' => [String, 0],
|
248
|
+
'templates-folder' => [String, 0],
|
249
|
+
'reports-folder' => [String, 0],
|
250
|
+
'report-retention' => [Integer, 0],
|
251
|
+
'webservice-port' => [Integer, 0]
|
252
|
+
}
|
253
|
+
]
|
254
|
+
}
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|