ruby-grafana-reporter 0.1.6 → 0.3.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/LICENSE +0 -0
- data/README.md +95 -173
- data/bin/ruby-grafana-reporter +5 -0
- 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 +133 -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 +158 -229
- data/lib/grafana_reporter/application/errors.rb +33 -30
- data/lib/grafana_reporter/application/webservice.rb +230 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +101 -99
- data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +96 -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/show_help_include_processor.rb +30 -202
- 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 +88 -57
- data/lib/grafana_reporter/asciidoctor/help.rb +435 -0
- data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +36 -32
- data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +28 -23
- data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +44 -43
- data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +40 -36
- data/lib/grafana_reporter/asciidoctor/query_mixin.rb +312 -309
- data/lib/grafana_reporter/asciidoctor/report.rb +179 -159
- data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +42 -34
- data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +44 -32
- data/lib/grafana_reporter/configuration.rb +304 -326
- data/lib/grafana_reporter/console_configuration_wizard.rb +269 -0
- 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 +32 -27
- metadata +116 -16
@@ -0,0 +1,269 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GrafanaReporter
|
4
|
+
class ConsoleConfigurationWizard
|
5
|
+
# Provides a command line configuration wizard for setting up the necessary configuration
|
6
|
+
# file.
|
7
|
+
# TODO: refactor class
|
8
|
+
def start_wizard(config_file, console_config)
|
9
|
+
config = Configuration.new
|
10
|
+
|
11
|
+
return unless overwrite_file(config_file)
|
12
|
+
|
13
|
+
puts 'This wizard will guide you through an initial configuration for'\
|
14
|
+
' the ruby-grafana-reporter. The configuration file will be created'\
|
15
|
+
' in the current folder. Please make sure to specify necessary paths'\
|
16
|
+
' either with a relative or an absolute path properly.'
|
17
|
+
puts
|
18
|
+
puts "Wizard is creating configuration file '#{config_file}'."
|
19
|
+
puts
|
20
|
+
port = ui_config_port
|
21
|
+
grafana = ui_config_grafana(console_config)
|
22
|
+
templates = ui_config_templates_folder
|
23
|
+
reports = ui_config_reports_folder
|
24
|
+
images = ui_config_images_folder(templates)
|
25
|
+
retention = ui_config_retention
|
26
|
+
|
27
|
+
config_yaml = %(# This configuration has been built with the configuration wizard.
|
28
|
+
|
29
|
+
#{grafana}
|
30
|
+
|
31
|
+
grafana-reporter:
|
32
|
+
report-class: GrafanaReporter::Asciidoctor::Report
|
33
|
+
templates-folder: #{templates}
|
34
|
+
reports-folder: #{reports}
|
35
|
+
report-retention: #{retention}
|
36
|
+
webservice-port: #{port}
|
37
|
+
|
38
|
+
default-document-attributes:
|
39
|
+
imagesdir: #{images}
|
40
|
+
# feel free to add here additional asciidoctor document attributes which are applied to all your templates
|
41
|
+
)
|
42
|
+
|
43
|
+
begin
|
44
|
+
File.write(config_file, config_yaml, mode: 'w')
|
45
|
+
puts 'Configuration file successfully created.'
|
46
|
+
rescue StandardError => e
|
47
|
+
raise e
|
48
|
+
end
|
49
|
+
|
50
|
+
begin
|
51
|
+
config.config = YAML.load_file(config_file)
|
52
|
+
rescue StandardError => e
|
53
|
+
raise ConfigurationError, "Could not read config file '#{config_file}' (Error: #{e.message})\n"\
|
54
|
+
"Source:\n#{File.read(config_file)}"
|
55
|
+
end
|
56
|
+
|
57
|
+
begin
|
58
|
+
config.validate(true)
|
59
|
+
puts 'Configuration file validated successfully.'
|
60
|
+
rescue ConfigurationError => e
|
61
|
+
raise e
|
62
|
+
end
|
63
|
+
|
64
|
+
demo_report = create_demo_report(config)
|
65
|
+
|
66
|
+
demo_report ||= '<<your_report_name>>'
|
67
|
+
config_param = config_file == Application::Application::CONFIG_FILE ? '' : " -c #{config_file}"
|
68
|
+
program_call = "#{Gem.ruby} #{$PROGRAM_NAME}"
|
69
|
+
program_call = ENV['OCRA_EXECUTABLE'].gsub("#{Dir.pwd}/".gsub('/', '\\'), '') if ENV['OCRA_EXECUTABLE']
|
70
|
+
|
71
|
+
puts
|
72
|
+
puts 'Now everything is setup properly. Create your reports as required in the templates '\
|
73
|
+
'folder and run the reporter either standalone with e.g. the following command:'
|
74
|
+
puts
|
75
|
+
puts " #{program_call}#{config_param} -t #{demo_report} -o demo_report_with_help.pdf"
|
76
|
+
puts
|
77
|
+
puts 'or run it as a service using the following command:'
|
78
|
+
puts
|
79
|
+
puts " #{program_call}#{config_param}"
|
80
|
+
puts
|
81
|
+
puts "Open 'http://localhost:#{config.webserver_port}/render?var-template=#{demo_report}' in a webbrowser to"\
|
82
|
+
' test your configuration.'
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def create_demo_report(config)
|
88
|
+
unless Dir.exist?(config.templates_folder)
|
89
|
+
puts "Skip creation of DEMO template, as folder '#{config.templates_folder}' does not exist."
|
90
|
+
return nil
|
91
|
+
end
|
92
|
+
|
93
|
+
demo_report = 'demo_report'
|
94
|
+
demo_report_file = "#{config.templates_folder}#{demo_report}.adoc"
|
95
|
+
|
96
|
+
# TODO: add question to overwrite file
|
97
|
+
if File.exist?(demo_report_file)
|
98
|
+
puts "Skip creation of DEMO template, as file '#{demo_report_file}' already exists."
|
99
|
+
return demo_report
|
100
|
+
end
|
101
|
+
|
102
|
+
demo_report = %(= First Grafana Report Template
|
103
|
+
|
104
|
+
include::grafana_help[]
|
105
|
+
|
106
|
+
include::grafana_environment[])
|
107
|
+
begin
|
108
|
+
File.write(demo_report_file, demo_report, mode: 'w')
|
109
|
+
puts "DEMO template '#{demo_report_file}' successfully created."
|
110
|
+
rescue StandardError => e
|
111
|
+
puts e.message
|
112
|
+
return nil
|
113
|
+
end
|
114
|
+
|
115
|
+
demo_report
|
116
|
+
end
|
117
|
+
|
118
|
+
def ui_config_grafana(config)
|
119
|
+
valid = false
|
120
|
+
url = nil
|
121
|
+
api_key = nil
|
122
|
+
until valid
|
123
|
+
url ||= user_input('Specify grafana host', 'http://localhost:3000')
|
124
|
+
print "Testing connection to '#{url}' #{api_key ? '_with_' : '_without_'} API key..."
|
125
|
+
begin
|
126
|
+
# TODO: how to handle if ssl access if not working properly?
|
127
|
+
res = Grafana::Grafana.new(url,
|
128
|
+
api_key,
|
129
|
+
logger: config.logger, ssl_cert: config.ssl_cert).test_connection
|
130
|
+
rescue StandardError => e
|
131
|
+
puts
|
132
|
+
puts e.message
|
133
|
+
end
|
134
|
+
puts 'done.'
|
135
|
+
|
136
|
+
case res
|
137
|
+
when 'Admin'
|
138
|
+
valid = true
|
139
|
+
|
140
|
+
when 'NON-Admin'
|
141
|
+
print 'Access to grafana is permitted as NON-Admin. Do you want to use an [a]pi key,'\
|
142
|
+
' [r]e-enter api key or [i]gnore? [aRi]: '
|
143
|
+
|
144
|
+
case gets
|
145
|
+
when /(?:i|I)$/
|
146
|
+
valid = true
|
147
|
+
|
148
|
+
# TODO: what is difference between 'a' and 'r'?
|
149
|
+
when /(?:a|A)$/
|
150
|
+
print 'Enter API key: '
|
151
|
+
api_key = gets.sub(/\n$/, '')
|
152
|
+
|
153
|
+
when /(?:r|R|adRi)$/
|
154
|
+
api_key = nil
|
155
|
+
|
156
|
+
end
|
157
|
+
|
158
|
+
# TODO: ask to enter API key, if grafana cannot be accessed without that
|
159
|
+
else
|
160
|
+
print "Grafana could not be accessed at '#{url}'. Do you want do [r]e-enter url, or"\
|
161
|
+
' [i]gnore and proceed? [Ri]: '
|
162
|
+
|
163
|
+
case gets
|
164
|
+
when /(?:i|I)$/
|
165
|
+
valid = true
|
166
|
+
|
167
|
+
else
|
168
|
+
url = nil
|
169
|
+
api_key = nil
|
170
|
+
|
171
|
+
end
|
172
|
+
|
173
|
+
end
|
174
|
+
end
|
175
|
+
%(grafana:
|
176
|
+
default:
|
177
|
+
host: #{url}#{api_key ? "\n api_key: #{api_key}" : ''}}
|
178
|
+
)
|
179
|
+
end
|
180
|
+
|
181
|
+
def ui_config_port
|
182
|
+
input = nil
|
183
|
+
until input
|
184
|
+
input = user_input('Specify port on which reporter shall run', '8815')
|
185
|
+
input = nil unless input =~ /[0-9]+/
|
186
|
+
end
|
187
|
+
input
|
188
|
+
end
|
189
|
+
|
190
|
+
def ui_config_templates_folder
|
191
|
+
input = nil
|
192
|
+
until input
|
193
|
+
input = user_input('Specify path where templates shall be stored', './templates')
|
194
|
+
input = nil unless validate_config_folder(input)
|
195
|
+
end
|
196
|
+
input
|
197
|
+
end
|
198
|
+
|
199
|
+
def ui_config_reports_folder
|
200
|
+
input = nil
|
201
|
+
until input
|
202
|
+
input = user_input('Specify path where created reports shall be stored', './reports')
|
203
|
+
input = nil unless validate_config_folder(input)
|
204
|
+
end
|
205
|
+
input
|
206
|
+
end
|
207
|
+
|
208
|
+
def ui_config_images_folder(parent)
|
209
|
+
input = nil
|
210
|
+
until input
|
211
|
+
input = user_input('Specify path where rendered images shall be stored (relative to templates folder)',
|
212
|
+
'./images')
|
213
|
+
input = nil unless validate_config_folder(File.join(parent, input))
|
214
|
+
end
|
215
|
+
input
|
216
|
+
end
|
217
|
+
|
218
|
+
def ui_config_retention
|
219
|
+
input = nil
|
220
|
+
until input
|
221
|
+
input = user_input('Specify report retention duration in hours', '24')
|
222
|
+
input = nil unless input =~ /[0-9]+/
|
223
|
+
end
|
224
|
+
input
|
225
|
+
end
|
226
|
+
|
227
|
+
def user_input(text, default)
|
228
|
+
print "#{text} [#{default}]: "
|
229
|
+
input = gets.gsub(/\n$/, '')
|
230
|
+
input = default if input.empty?
|
231
|
+
input
|
232
|
+
end
|
233
|
+
|
234
|
+
def validate_config_folder(folder)
|
235
|
+
return true if Dir.exist?(folder)
|
236
|
+
|
237
|
+
print "Directory '#{folder} does not exist: [c]reate, [r]e-enter path or [i]gnore? [cRi]: "
|
238
|
+
case gets
|
239
|
+
when /^(?:c|C)$/
|
240
|
+
begin
|
241
|
+
Dir.mkdir(folder)
|
242
|
+
puts "Directory '#{folder}' successfully created."
|
243
|
+
return true
|
244
|
+
rescue StandardError => e
|
245
|
+
puts "WARN: Directory '#{folder}' could not be created. Please create it manually."
|
246
|
+
puts e.message
|
247
|
+
end
|
248
|
+
|
249
|
+
when /^(?:i|I)$/
|
250
|
+
puts "WARN: Directory '#{folder}' does not exist. Please create manually."
|
251
|
+
return true
|
252
|
+
end
|
253
|
+
|
254
|
+
false
|
255
|
+
end
|
256
|
+
|
257
|
+
def overwrite_file(config_file)
|
258
|
+
return true unless File.exist?(config_file)
|
259
|
+
|
260
|
+
input = nil
|
261
|
+
until input
|
262
|
+
input = user_input("Configuration file '#{config_file}' already exists. Do you want to overwrite it?", 'yN')
|
263
|
+
return false if input =~ /^(?:n|N|yN)$/
|
264
|
+
end
|
265
|
+
|
266
|
+
true
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
@@ -1,38 +1,48 @@
|
|
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
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GrafanaReporter
|
4
|
+
# General error of the reporter. All other errors will inherit from this class.
|
5
|
+
class GrafanaReporterError < StandardError
|
6
|
+
def initialize(message)
|
7
|
+
super("GrafanaReporterError: #{message}")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Thrown, if the requested grafana instance does not have the mandatory 'host'
|
12
|
+
# setting configured.
|
13
|
+
class GrafanaInstanceWithoutHostError < GrafanaReporterError
|
14
|
+
def initialize(instance)
|
15
|
+
super("Grafana instance '#{instance}' has been configured without mandatory 'host' setting.")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# General configuration error. All configuration errors inherit from this class.
|
20
|
+
class ConfigurationError < GrafanaReporterError
|
21
|
+
def initialize(message)
|
22
|
+
super("Configuration error: #{message}")
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Thrown if a non existing template has been specified.
|
27
|
+
class MissingTemplateError < ConfigurationError
|
28
|
+
def initialize(template)
|
29
|
+
super("Given report template '#{template}' is not a valid template.")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Thrown, if a configured path does not exist.
|
34
|
+
class FolderDoesNotExistError < ConfigurationError
|
35
|
+
def initialize(folder, config_item)
|
36
|
+
super("#{config_item} '#{folder}' does not exist.")
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Thrown if the configuration does not match the expected schema.
|
41
|
+
# Details about how to fix that are provided in the message.
|
42
|
+
class ConfigurationDoesNotMatchSchemaError < ConfigurationError
|
43
|
+
def initialize(item, verb, expected, currently)
|
44
|
+
super("Configuration file does not match schema definition. Expected '#{item}' to #{verb} '#{expected}',"\
|
45
|
+
"but was '#{currently}'.")
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -1,52 +1,58 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
module
|
5
|
-
|
6
|
-
# This logger enables a special use case, so that one and the same log
|
7
|
-
# will automatically be send to two different logger destinations.
|
8
|
-
#
|
9
|
-
# One destination is the set {#additional_logger=} which respects the
|
10
|
-
# configured severity. The other destination is an internal logger, which
|
11
|
-
# will always log all messages in mode Logger::Severity::Debug. All messages
|
12
|
-
# of the internal logger can easily be retrieved, by using the
|
13
|
-
# {#internal_messages} method.
|
14
|
-
#
|
15
|
-
# Except the {#level=} setting, all calls to the logger will immediately
|
16
|
-
# be delegated to the internal logger and the configured {#additional_logger=}.
|
17
|
-
# By having this behavior, the class can be used wherever the standard Logger
|
18
|
-
# can also be used.
|
19
|
-
class TwoWayDelegateLogger
|
20
|
-
def initialize
|
21
|
-
@internal_messages = StringIO.new
|
22
|
-
@internal_logger = ::Logger.new(@internal_messages)
|
23
|
-
@internal_logger.level = ::Logger::Severity::DEBUG
|
24
|
-
@additional_logger = ::Logger.new(nil)
|
25
|
-
end
|
26
|
-
|
27
|
-
# Sets the severity level of the additional logger to the given severity.
|
28
|
-
# @param severity one of {Logger::Severity}
|
29
|
-
def level=(severity)
|
30
|
-
@additional_logger.level = severity
|
31
|
-
end
|
32
|
-
|
33
|
-
# @return [String] all messages of the internal logger.
|
34
|
-
def internal_messages
|
35
|
-
@internal_messages.string
|
36
|
-
end
|
37
|
-
|
38
|
-
# Used to set the additional logger in this class to an already existing
|
39
|
-
# logger.
|
40
|
-
# @param logger [Logger] sets the additional logger to the given value.
|
41
|
-
def additional_logger=(logger)
|
42
|
-
@additional_logger = logger || ::Logger.new(nil)
|
43
|
-
end
|
44
|
-
|
45
|
-
# Delegates all not configured calls to the internal and the additional logger.
|
46
|
-
def method_missing(method, *args)
|
47
|
-
@internal_logger.send(method, *args)
|
48
|
-
@additional_logger.send(method, *args)
|
49
|
-
end
|
50
|
-
|
51
|
-
|
52
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GrafanaReporter
|
4
|
+
# This module contains special extensions for use in the reporter.
|
5
|
+
module Logger
|
6
|
+
# This logger enables a special use case, so that one and the same log
|
7
|
+
# will automatically be send to two different logger destinations.
|
8
|
+
#
|
9
|
+
# One destination is the set {#additional_logger=} which respects the
|
10
|
+
# configured severity. The other destination is an internal logger, which
|
11
|
+
# will always log all messages in mode Logger::Severity::Debug. All messages
|
12
|
+
# of the internal logger can easily be retrieved, by using the
|
13
|
+
# {#internal_messages} method.
|
14
|
+
#
|
15
|
+
# Except the {#level=} setting, all calls to the logger will immediately
|
16
|
+
# be delegated to the internal logger and the configured {#additional_logger=}.
|
17
|
+
# By having this behavior, the class can be used wherever the standard Logger
|
18
|
+
# can also be used.
|
19
|
+
class TwoWayDelegateLogger
|
20
|
+
def initialize
|
21
|
+
@internal_messages = StringIO.new
|
22
|
+
@internal_logger = ::Logger.new(@internal_messages)
|
23
|
+
@internal_logger.level = ::Logger::Severity::DEBUG
|
24
|
+
@additional_logger = ::Logger.new(nil)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Sets the severity level of the additional logger to the given severity.
|
28
|
+
# @param severity one of {Logger::Severity}
|
29
|
+
def level=(severity)
|
30
|
+
@additional_logger.level = severity
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String] all messages of the internal logger.
|
34
|
+
def internal_messages
|
35
|
+
@internal_messages.string
|
36
|
+
end
|
37
|
+
|
38
|
+
# Used to set the additional logger in this class to an already existing
|
39
|
+
# logger.
|
40
|
+
# @param logger [Logger] sets the additional logger to the given value.
|
41
|
+
def additional_logger=(logger)
|
42
|
+
@additional_logger = logger || ::Logger.new(nil)
|
43
|
+
end
|
44
|
+
|
45
|
+
# Delegates all not configured calls to the internal and the additional logger.
|
46
|
+
def method_missing(method, *args)
|
47
|
+
@internal_logger.send(method, *args)
|
48
|
+
@additional_logger.send(method, *args)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Registers all methods to which the internal logger responds.
|
52
|
+
def respond_to_missing?(method, *_args)
|
53
|
+
super
|
54
|
+
@internal_logger.respond_to?(method)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|