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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +0 -0
  3. data/README.md +95 -173
  4. data/bin/ruby-grafana-reporter +5 -0
  5. data/lib/VERSION.rb +5 -3
  6. data/lib/grafana/abstract_panel_query.rb +22 -20
  7. data/lib/grafana/abstract_query.rb +132 -127
  8. data/lib/grafana/abstract_sql_query.rb +51 -42
  9. data/lib/grafana/dashboard.rb +77 -66
  10. data/lib/grafana/errors.rb +66 -61
  11. data/lib/grafana/grafana.rb +133 -131
  12. data/lib/grafana/panel.rb +41 -39
  13. data/lib/grafana/panel_image_query.rb +52 -49
  14. data/lib/grafana/variable.rb +217 -259
  15. data/lib/grafana_reporter/abstract_report.rb +112 -109
  16. data/lib/grafana_reporter/application/application.rb +158 -229
  17. data/lib/grafana_reporter/application/errors.rb +33 -30
  18. data/lib/grafana_reporter/application/webservice.rb +230 -0
  19. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +101 -99
  20. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +96 -96
  21. data/lib/grafana_reporter/asciidoctor/errors.rb +40 -37
  22. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +92 -86
  23. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +91 -86
  24. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +69 -67
  25. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +68 -65
  26. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +61 -58
  27. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +78 -75
  28. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +73 -70
  29. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +20 -18
  30. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +43 -41
  31. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +30 -202
  32. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +70 -67
  33. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +66 -65
  34. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +88 -57
  35. data/lib/grafana_reporter/asciidoctor/help.rb +435 -0
  36. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +36 -32
  37. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +28 -23
  38. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +44 -43
  39. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +40 -36
  40. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +312 -309
  41. data/lib/grafana_reporter/asciidoctor/report.rb +179 -159
  42. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +42 -34
  43. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +44 -32
  44. data/lib/grafana_reporter/configuration.rb +304 -326
  45. data/lib/grafana_reporter/console_configuration_wizard.rb +269 -0
  46. data/lib/grafana_reporter/errors.rb +48 -38
  47. data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
  48. data/lib/ruby-grafana-reporter.rb +32 -27
  49. 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
- module GrafanaReporter
2
- # General error of the reporter. All other errors will inherit from this class.
3
- class GrafanaReporterError < StandardError
4
- def initialize(message)
5
- super('GrafanaReporterError: ' + message.to_s)
6
- end
7
- end
8
-
9
- # Thrown, if the requested grafana instance does not have the mandatory 'host'
10
- # setting configured.
11
- class GrafanaInstanceWithoutHostError < GrafanaReporterError
12
- def initialize(instance)
13
- super("Grafana instance '#{instance}' has been configured without mandatory 'host' setting.")
14
- end
15
- end
16
-
17
- # General configuration error. All configuration errors inherit from this class.
18
- class ConfigurationError < GrafanaReporterError
19
- def initialize(message)
20
- super("Configuration error: #{message}")
21
- end
22
- end
23
-
24
- # Thrown, if a configured path does not exist.
25
- class FolderDoesNotExistError < ConfigurationError
26
- def initialize(folder, config_item)
27
- super("#{config_item} '#{folder}' does not exist.")
28
- end
29
- end
30
-
31
- # Thrown if the configuration does not match the expected schema.
32
- # Details about how to fix that are provided in the message.
33
- class ConfigurationDoesNotMatchSchemaError < ConfigurationError
34
- def initialize(item, verb, expected, currently)
35
- super("Configuration file does not match schema definition. Expected '#{item}' to #{verb} '#{expected}', but was '#{currently}'.")
36
- end
37
- end
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
- module GrafanaReporter
2
-
3
- # This module contains special extensions for use in the reporter.
4
- module Logger
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
- end
51
- end
52
- end
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