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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +166 -339
  3. data/bin/ruby-grafana-reporter +5 -4
  4. data/lib/VERSION.rb +5 -3
  5. data/lib/grafana/abstract_panel_query.rb +22 -20
  6. data/lib/grafana/abstract_query.rb +132 -127
  7. data/lib/grafana/abstract_sql_query.rb +51 -42
  8. data/lib/grafana/dashboard.rb +77 -66
  9. data/lib/grafana/errors.rb +66 -61
  10. data/lib/grafana/grafana.rb +130 -131
  11. data/lib/grafana/panel.rb +41 -39
  12. data/lib/grafana/panel_image_query.rb +52 -49
  13. data/lib/grafana/variable.rb +217 -259
  14. data/lib/grafana_reporter/abstract_report.rb +112 -109
  15. data/lib/grafana_reporter/application/application.rb +404 -229
  16. data/lib/grafana_reporter/application/errors.rb +33 -30
  17. data/lib/grafana_reporter/application/webservice.rb +231 -0
  18. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +104 -99
  19. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +99 -96
  20. data/lib/grafana_reporter/asciidoctor/errors.rb +40 -37
  21. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +92 -86
  22. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +91 -86
  23. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +69 -67
  24. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +68 -65
  25. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +61 -58
  26. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +78 -75
  27. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +73 -70
  28. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +20 -18
  29. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +43 -41
  30. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +70 -67
  31. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +66 -65
  32. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +61 -57
  33. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +34 -32
  34. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +25 -23
  35. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +44 -43
  36. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +38 -36
  37. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +310 -309
  38. data/lib/grafana_reporter/asciidoctor/report.rb +177 -159
  39. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +37 -34
  40. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +39 -32
  41. data/lib/grafana_reporter/configuration.rb +257 -326
  42. data/lib/grafana_reporter/errors.rb +48 -38
  43. data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
  44. data/lib/ruby-grafana-reporter.rb +29 -27
  45. metadata +10 -23
@@ -1,109 +1,112 @@
1
- module GrafanaReporter
2
- # @abstract Override {#create_report} and {#progress}.
3
- #
4
- # This class is used to build a report on basis of a given configuration and
5
- # template.
6
- #
7
- # Objects of this class are also stored in {Application::Application}, unless
8
- # the retention time is over.
9
- class AbstractReport
10
- # @return [String] path to the template
11
- attr_reader :template
12
-
13
- # @return [Time] time, when the report generation started
14
- attr_reader :start_time
15
-
16
- # @return [Time] time, when the report generation ended
17
- attr_reader :end_time
18
-
19
- # @return [Logger] logger object used during report generation
20
- attr_reader :logger
21
-
22
- # @return [Boolean] true, if the report is or shall be cancelled
23
- attr_reader :cancel
24
-
25
- # @return [Boolen] true, if the report generation is finished (successfull or not)
26
- attr_reader :done
27
-
28
- # @param config [Configuration] configuration object
29
- # @param template [String] path to the template to be used
30
- # @param destination_file_or_path [String or File] path to the destination report or file object to use
31
- # @param custom_attributes [Hash] custom attributes, which shall be merged with priority over the configuration
32
- def initialize(config, template, destination_file_or_path = nil, custom_attributes = {})
33
- @config = config
34
- @logger = Logger::TwoWayDelegateLogger.new
35
- @logger.additional_logger = @config.logger
36
- @done = false
37
- @template = template
38
- @destination_file_or_path = destination_file_or_path
39
- @custom_attributes = custom_attributes
40
- @start_time = nil
41
- @end_time = nil
42
- @cancel = false
43
- raise MissingTemplateError, @template.to_s unless File.exist?(@template.to_s)
44
- end
45
-
46
- # Call to request cancelling the report generation.
47
- # @return [void]
48
- def cancel!
49
- @cancel = true
50
- logger.info('Cancelling report generation invoked.')
51
- end
52
-
53
- # @return [String] path to the report destination file
54
- def path
55
- @destination_file_or_path.respond_to?(:path) ? @destination_file_or_path.path : @destination_file_or_path
56
- end
57
-
58
- # Deletes the report file object.
59
- # @return [void]
60
- def delete_file
61
- if @destination_file_or_path.is_a?(Tempfile)
62
- @destination_file_or_path.unlink
63
- elsif @destination_file_or_path.is_a?(File)
64
- @destination_file_or_path.delete
65
- end
66
- @destination_file_or_path = nil
67
- end
68
-
69
- # @return [Float] time in seconds, that the report generation took
70
- def execution_time
71
- return nil if start_time.nil?
72
- return end_time - start_time unless end_time.nil?
73
-
74
- Time.now - start_time
75
- end
76
-
77
- # @return [Array] error messages during report generation.
78
- def error
79
- @error || []
80
- end
81
-
82
- # @return [String] status of the report, one of 'in progress', 'cancelled', 'died' or 'finished'.
83
- def status
84
- return 'cancelled' if done && cancel
85
- return 'finished' if done && error.empty?
86
- return 'died' if done && !error.empty?
87
-
88
- 'in progress'
89
- end
90
-
91
- # @return [String] string containing all messages ([Logger::Severity::DEBUG]) of the logger during report generation.
92
- def full_log
93
- logger.internal_messages
94
- end
95
-
96
- # @abstract
97
- # Is being called to start the report generation.
98
- # @return [void]
99
- def create_report
100
- raise NotImplementedError
101
- end
102
-
103
- # @abstract
104
- # @return [Integer] number between 0 and 100, representing the current progress of the report creation.
105
- def progress
106
- raise NotImplementedError
107
- end
108
- end
109
- end
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ # @abstract Override {#create_report} and {#progress}.
5
+ #
6
+ # This class is used to build a report on basis of a given configuration and
7
+ # template.
8
+ #
9
+ # Objects of this class are also stored in {Application::Application}, unless
10
+ # the retention time is over.
11
+ class AbstractReport
12
+ # @return [String] path to the template
13
+ attr_reader :template
14
+
15
+ # @return [Time] time, when the report generation started
16
+ attr_reader :start_time
17
+
18
+ # @return [Time] time, when the report generation ended
19
+ attr_reader :end_time
20
+
21
+ # @return [Logger] logger object used during report generation
22
+ attr_reader :logger
23
+
24
+ # @return [Boolean] true, if the report is or shall be cancelled
25
+ attr_reader :cancel
26
+
27
+ # @return [Boolen] true, if the report generation is finished (successfull or not)
28
+ attr_reader :done
29
+
30
+ # @param config [Configuration] configuration object
31
+ # @param template [String] path to the template to be used
32
+ # @param destination_file_or_path [String or File] path to the destination report or file object to use
33
+ # @param custom_attributes [Hash] custom attributes, which shall be merged with priority over the configuration
34
+ def initialize(config, template, destination_file_or_path = nil, custom_attributes = {})
35
+ @config = config
36
+ @logger = Logger::TwoWayDelegateLogger.new
37
+ @logger.additional_logger = @config.logger
38
+ @done = false
39
+ @template = template
40
+ @destination_file_or_path = destination_file_or_path
41
+ @custom_attributes = custom_attributes
42
+ @start_time = nil
43
+ @end_time = nil
44
+ @cancel = false
45
+ raise MissingTemplateError, @template.to_s unless File.exist?(@template.to_s)
46
+ end
47
+
48
+ # Call to request cancelling the report generation.
49
+ # @return [void]
50
+ def cancel!
51
+ @cancel = true
52
+ logger.info('Cancelling report generation invoked.')
53
+ end
54
+
55
+ # @return [String] path to the report destination file
56
+ def path
57
+ @destination_file_or_path.respond_to?(:path) ? @destination_file_or_path.path : @destination_file_or_path
58
+ end
59
+
60
+ # Deletes the report file object.
61
+ # @return [void]
62
+ def delete_file
63
+ if @destination_file_or_path.is_a?(Tempfile)
64
+ @destination_file_or_path.unlink
65
+ elsif @destination_file_or_path.is_a?(File)
66
+ @destination_file_or_path.delete
67
+ end
68
+ @destination_file_or_path = nil
69
+ end
70
+
71
+ # @return [Float] time in seconds, that the report generation took
72
+ def execution_time
73
+ return nil if start_time.nil?
74
+ return end_time - start_time unless end_time.nil?
75
+
76
+ Time.now - start_time
77
+ end
78
+
79
+ # @return [Array] error messages during report generation.
80
+ def error
81
+ @error || []
82
+ end
83
+
84
+ # @return [String] status of the report, one of 'in progress', 'cancelled', 'died' or 'finished'.
85
+ def status
86
+ return 'cancelled' if done && cancel
87
+ return 'finished' if done && error.empty?
88
+ return 'died' if done && !error.empty?
89
+
90
+ 'in progress'
91
+ end
92
+
93
+ # @return [String] string containing all messages ([Logger::Severity::DEBUG]) of the logger during report
94
+ # generation.
95
+ def full_log
96
+ logger.internal_messages
97
+ end
98
+
99
+ # @abstract
100
+ # Is being called to start the report generation.
101
+ # @return [void]
102
+ def create_report
103
+ raise NotImplementedError
104
+ end
105
+
106
+ # @abstract
107
+ # @return [Integer] number between 0 and 100, representing the current progress of the report creation.
108
+ def progress
109
+ raise NotImplementedError
110
+ end
111
+ end
112
+ end
@@ -1,229 +1,404 @@
1
- module GrafanaReporter
2
- # This module contains all classes, which are used by the grafana reporter
3
- # application. The application is a set of classes, which allows to run the
4
- # reporter in several ways.
5
- #
6
- # If you intend to use the reporter functionality, without the application,
7
- # it might be helpful to not use the classes from here.
8
- module Application
9
- # This class contains the main application to run the grafana reporter.
10
- #
11
- # It can be run to test the grafana connection, render a single template
12
- # or run as a service.
13
- class Application
14
- def initialize
15
- @logger = ::Logger.new(STDERR, level: :unknown)
16
- @reports = []
17
- end
18
-
19
- # Can be used to set a {Configuration} object to the application.
20
- #
21
- # This is mainly helpful in testing the application or in an
22
- # integrated use.
23
- # @param config {Configuration} configuration to be used by the application
24
- # @return [void]
25
- def config=(config)
26
- @logger = config.logger || @logger
27
- @config = config
28
- end
29
-
30
- # This is the main method, which is called, if the application is
31
- # run in standalone mode.
32
- # @param params [Array] normally the ARGV command line parameters
33
- # @return [Integer] see {#run}
34
- def configure_and_run(params = [])
35
- config = GrafanaReporter::Configuration.new
36
- config.logger.level = ::Logger::Severity::INFO
37
- result = config.configure_by_command_line(params)
38
- return result if result != 0
39
-
40
- self.config = config
41
- run
42
- end
43
-
44
- # Runs the application with the current set {Configuration} object.
45
- # @return [Integer] value smaller than 0, if error. 0 if successfull
46
- def run
47
- begin
48
- @config.validate
49
- rescue ConfigurationError => e
50
- puts e.message
51
- return -2
52
- end
53
-
54
- case @config.mode
55
- when Configuration::MODE_CONNECTION_TEST
56
- res = Grafana::Grafana.new(@config.grafana_host(@config.test_instance), @config.grafana_api_key(@config.test_instance), logger: @logger).test_connection
57
- puts res
58
-
59
- when Configuration::MODE_SINGLE_RENDER
60
- @config.report_class.new(@config, @config.template, @config.to_file).create_report
61
-
62
- when Configuration::MODE_SERVICE
63
- run_webserver
64
- end
65
- 0
66
- end
67
-
68
- private
69
-
70
- def clean_outdated_temporary_reports
71
- clean_time = Time.now - 60 * 60 * @config.report_retention
72
- @reports.select { |report| report.done && clean_time > report.end_time }.each do |report|
73
- @reports.delete(report).delete_file
74
- end
75
- end
76
-
77
- def run_webserver
78
- # start webserver
79
- server = TCPServer.new(@config.webserver_port)
80
- @logger.info("Server listening on port #{@config.webserver_port}...")
81
-
82
- @progress_reporter = Thread.new {}
83
-
84
- loop do
85
- # step 1) accept incoming connection
86
- socket = server.accept
87
-
88
- # step 2) print the request headers (separated by a blank line e.g. \r\n)
89
- request = ''
90
- line = ''
91
- begin
92
- until line == "\r\n"
93
- line = socket.readline
94
- request += line
95
- end
96
- rescue EOFError => e
97
- @logger.debug("Webserver EOFError: #{e.message}")
98
- end
99
-
100
- begin
101
- response = handle_request(request)
102
- socket.write response
103
- rescue WebserviceUnknownPathError => e
104
- @logger.debug(e.message)
105
- socket.write http_response(404, '', e.message)
106
- rescue MissingTemplateError => e
107
- @logger.error(e.message)
108
- socket.write http_response(400, 'Bad Request', e.message)
109
- rescue WebserviceGeneralRenderingError => e
110
- @logger.fatal(e.message)
111
- socket.write http_response(400, 'Bad Request', e.message)
112
- rescue StandardError => e
113
- @logger.fatal(e.message)
114
- socket.write http_response(400, 'Bad Request', e.message)
115
- ensure
116
- socket.close
117
- end
118
-
119
- unless @progress_reporter.alive?
120
- @progress_reporter = Thread.new do
121
- running_reports = @reports.reject(&:done)
122
- until running_reports.empty?
123
- @logger.info("#{running_reports.length} report(s) in progress: #{running_reports.map { |report| (report.progress * 100).to_i.to_s + '% (running ' + report.execution_time.to_i.to_s + ' secs)' }.join(', ')}") unless running_reports.empty?
124
- sleep 5
125
- running_reports = @reports.reject(&:done)
126
- end
127
- # puts "no more running reports - stopping to report progress"
128
- end
129
- end
130
-
131
- clean_outdated_temporary_reports
132
- end
133
- end
134
-
135
- def handle_request(request)
136
- raise WebserviceUnknownPathError, request.split("\r\n")[0] if request.nil?
137
- raise WebserviceUnknownPathError, request.split("\r\n")[0] if request.split("\r\n")[0].nil?
138
-
139
- query_string = request.split("\r\n")[0].gsub(%r{(?:[^\?]+[\?])(.*)(?: HTTP/.*)$}, '\1')
140
- query_parameters = CGI.parse(query_string)
141
-
142
- @logger.debug("Received request: #{request.split("\r\n")[0]}")
143
- @logger.debug('query_parameters: ' + query_parameters.to_s)
144
-
145
- # read URL parameters
146
- attrs = {}
147
- query_parameters.each do |k, v|
148
- attrs[k] = v.length == 1 ? v[0] : v
149
- end
150
-
151
- if request.split("\r\n")[0] =~ %r{^GET /render[\? ]}
152
- # build report
153
- template_file = @config.templates_folder.to_s + attrs['var-template'].to_s + '.adoc'
154
-
155
- file = Tempfile.new('gf_pdf_', @config.reports_folder)
156
- begin
157
- FileUtils.chmod('+r', file.path)
158
- rescue StandardError => e
159
- @logger.debug("File permissions could not be set for #{file.path}: #{e.message}")
160
- end
161
-
162
- report = @config.report_class.new(@config, template_file, file, attrs)
163
- Thread.new do
164
- report.create_report
165
- end
166
- @reports << report
167
-
168
- return http_response(302, 'Found', nil, Location: "/view_report?report_id=#{report.object_id}")
169
-
170
- elsif request.split("\r\n")[0] =~ %r{^GET /overview[\? ]}
171
- # show overview for current reports
172
- return get_reports_status_as_html(@reports)
173
-
174
- elsif request.split("\r\n")[0] =~ %r{^GET /view_report[\? ]}
175
- # view report if already available, or show status view
176
- report = @reports.select { |r| r.object_id.to_s == attrs['report_id'].to_s }.first
177
- raise WebserviceGeneralRenderingError, 'view_report has been called without valid id' if report.nil?
178
-
179
- # show report status
180
- return get_reports_status_as_html([report]) if !report.done || !report.error.empty?
181
-
182
- # provide report
183
- @logger.debug("Returning PDF report at #{report.path}")
184
- content = File.read(report.path)
185
- return http_response(200, 'OK', content, "Content-Type": 'application/pdf') if content.start_with?("%PDF")
186
- # TODO properly provide file as zip
187
- return http_response(200, 'OK', content, "Content-Type": 'application/octet-stream', "Content-Disposition": "attachment; filename=report.zip")
188
-
189
- elsif request.split("\r\n")[0] =~ %r{^GET /cancel_report[\? ]}
190
- # view report if already available, or show status view
191
- report = @reports.select { |r| r.object_id.to_s == attrs['report_id'].to_s }.first
192
- raise WebserviceGeneralRenderingError, 'cancel_report has been called without valid id' if report.nil?
193
-
194
- report.cancel! unless report.done
195
-
196
- # redirect to view_report page
197
- return http_response(302, 'Found', nil, Location: "/view_report?report_id=#{report.object_id}")
198
-
199
- elsif request.split("\r\n")[0] =~ %r{^GET /view_log[\? ]}
200
- # view report if already available, or show status view
201
- report = @reports.select { |r| r.object_id.to_s == attrs['report_id'].to_s }.first
202
- raise WebserviceGeneralRenderingError, 'view_log has been called without valid id' if report.nil?
203
-
204
- content = report.full_log
205
-
206
- return http_response(200, 'OK', content, "Content-Type": 'text/plain')
207
- end
208
-
209
- raise WebserviceUnknownPathError, request.split("\r\n")[0]
210
- end
211
-
212
- def get_reports_status_as_html(reports)
213
- i = reports.length
214
-
215
- content = '<html><head></head><body><table><thead><th>#</th><th>Start Time</th><th>End Time</th><th>Template</th><th>Execution time</th><th>Status</th><th>Error</th><th>Action</th></thead>' +
216
- reports.reverse.map do |report|
217
- "<tr><td>#{(i -= 1)}</td><td>#{report.start_time}</td><td>#{report.end_time}</td><td>#{report.template}</td><td>#{report.execution_time.to_i} secs</td><td>#{report.status} (#{(report.progress * 100).to_i}%)</td><td>#{report.error.join('<br>')}</td><td>#{!report.done && !report.cancel ? "<a href=\"/cancel_report?report_id=#{report.object_id}\">Cancel</a>&nbsp;" : ''}#{(report.status == 'finished') || (report.status == 'cancelled') ? "<a href=\"/view_report?report_id=#{report.object_id}\">View</a>&nbsp;" : '&nbsp;'}<a href=\"/view_log?report_id=#{report.object_id}\">Log</a></td></tr>"
218
- end.join('') +
219
- '</table></body></html>'
220
-
221
- http_response(200, 'OK', content, "Content-Type": 'text/html')
222
- end
223
-
224
- def http_response(code, text, body, opts = {})
225
- "HTTP/1.1 #{code} #{text}\r\n#{opts.map { |k, v| "#{k}: #{v}" }.join("\r\n")}#{body ? "\r\nContent-Length: #{body.to_s.bytesize}" : ''}\r\n\r\n#{body}"
226
- end
227
- end
228
- end
229
- end
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ # This module contains all classes, which are used by the grafana reporter
5
+ # application. The application is a set of classes, which allows to run the
6
+ # reporter in several ways.
7
+ #
8
+ # If you intend to use the reporter functionality, without the application,
9
+ # it might be helpful to not use the classes from here.
10
+ module Application
11
+ # This class contains the main application to run the grafana reporter.
12
+ #
13
+ # It can be run to test the grafana connection, render a single template
14
+ # or run as a service.
15
+ class Application
16
+ # Default file name for grafana reporter configuration file
17
+ CONFIG_FILE = 'grafana_reporter.config'
18
+
19
+ def initialize
20
+ @logger = ::Logger.new($stdout, level: :unknown)
21
+ end
22
+
23
+ # Contains the {Configuration} object of the application.
24
+ attr_accessor :config
25
+
26
+ # This is the main method, which is called, if the application is
27
+ # run in standalone mode.
28
+ # @param params [Array<String>] command line parameters, mainly ARGV can be used.
29
+ # @return [Integer] 0 if everything is fine, -1 if execution aborted.
30
+ def configure_and_run(params = [])
31
+ config_file = CONFIG_FILE
32
+ cli_config = {}
33
+ cli_config ['grafana-reporter'] = {}
34
+ cli_config ['default-document-attributes'] = {}
35
+
36
+ parser = OptionParser.new do |opts|
37
+ opts.banner = "Usage: ruby #{$PROGRAM_NAME} [options]"
38
+
39
+ opts.on('-c', '--config CONFIG_FILE_NAME', 'Specify custom configuration file,'\
40
+ " instead of #{CONFIG_FILE}.") do |file_name|
41
+ config_file = file_name
42
+ end
43
+
44
+ opts.on('-d', '--debug LEVEL', 'Specify detail level: FATAL, ERROR, WARN, INFO, DEBUG.') do |level|
45
+ if level =~ /(?:FATAL|ERROR|WARN|INFO|DEBUG)/
46
+ @logger.level = Object.const_get("::Logger::Severity::#{level}")
47
+ end
48
+ end
49
+
50
+ opts.on('-o', '--output FILE', 'Output filename if only a single file is rendered') do |file|
51
+ cli_config['to_file'] = file
52
+ end
53
+
54
+ opts.on('-s', '--set VARIABLE,VALUE', Array, 'Set a variable value, which will be passed to the rendering') do |list|
55
+ raise ParameterValueError.new(list.length) unless list.length == 2
56
+ cli_config['default-document-attributes'][list[0]] = list[1]
57
+ end
58
+
59
+ opts.on('--test GRAFANA_INSTANCE', 'test current configuration against given GRAFANA_INSTANCE') do |instance|
60
+ cli_config['grafana-reporter']['run-mode'] = 'test'
61
+ cli_config['grafana-reporter']['test-instance'] = instance
62
+ end
63
+
64
+ opts.on('-t', '--template TEMPLATE', 'Render a single ASCIIDOC template to PDF and exit') do |template|
65
+ cli_config['grafana-reporter']['run-mode'] = 'single-render'
66
+ cli_config['default-document-attributes']['var-template'] = template
67
+ end
68
+
69
+ opts.on('-w', '--wizard', 'Configuration wizard to prepare environment for the reporter.') do
70
+ return config_wizard
71
+ end
72
+
73
+ opts.on('-v', '--version', 'Version information') do
74
+ puts GRAFANA_REPORTER_VERSION.join('.')
75
+ return -1
76
+ end
77
+
78
+ opts.on('-h', '--help', 'Show this message') do
79
+ puts opts
80
+ return -1
81
+ end
82
+ end
83
+
84
+ begin
85
+ parser.parse!(params)
86
+ rescue ApplicationError => e
87
+ puts e.message
88
+ return -1
89
+ end
90
+
91
+ # abort if config file does not exist
92
+ unless File.exist?(config_file)
93
+ puts "Config file '#{config_file}' does not exist. Consider calling the configuration wizard"\
94
+ ' with option \'-w\' or use \'-h\' to see help message. Aborting.'
95
+ return -1
96
+ end
97
+
98
+ # read config file
99
+ @config = GrafanaReporter::Configuration.new
100
+ config.logger = @logger
101
+ config_hash = nil
102
+ begin
103
+ config_hash = YAML.load_file(config_file)
104
+ rescue StandardError => e
105
+ raise ConfigurationError, "Could not read config file '#{config_file}' (Error: #{e.message})"
106
+ end
107
+
108
+ # merge command line configuration with read config file
109
+ config_hash.merge!(cli_config) { |_key, v1, v2| Hash === v1 && Hash === v2 ? v1.merge(v2) : v2 }
110
+ config.config = config_hash
111
+
112
+ run
113
+ end
114
+
115
+ # Runs the application with the current set {Configuration} object.
116
+ # @return [Integer] value smaller than 0, if error. 0 if successfull
117
+ def run
118
+ begin
119
+ config.validate
120
+ rescue ConfigurationError => e
121
+ puts e.message
122
+ return -2
123
+ end
124
+
125
+ case config.mode
126
+ when Configuration::MODE_CONNECTION_TEST
127
+ res = Grafana::Grafana.new(config.grafana_host(config.test_instance),
128
+ config.grafana_api_key(config.test_instance),
129
+ logger: config.logger).test_connection
130
+ puts res
131
+
132
+ when Configuration::MODE_SINGLE_RENDER
133
+ begin
134
+ config.report_class.new(config, config.template, config.to_file).create_report
135
+ rescue StandardError => e
136
+ puts e.message
137
+ end
138
+
139
+ when Configuration::MODE_SERVICE
140
+ Webservice.new(config).run
141
+ end
142
+
143
+ 0
144
+ end
145
+
146
+ private
147
+
148
+ # Provides a command line configuration wizard for setting up the necessary configuration
149
+ # file.
150
+ def config_wizard
151
+ if File.exist?(CONFIG_FILE)
152
+ input = nil
153
+ until input
154
+ input = user_input("Configuration file '#{CONFIG_FILE}' already exists. Do you want to overwrite it?", 'yN')
155
+ return if input =~ /^(?:n|N|yN)$/
156
+ end
157
+ end
158
+
159
+ puts 'This wizard will guide you through an initial configuration for'\
160
+ ' the ruby-grafana-reporter. The configuration file will be created'\
161
+ ' in the current folder. Please make sure to specify necessary paths'\
162
+ ' either with a relative or an absolute path properly.'
163
+ puts
164
+ port = ui_config_port
165
+ grafana = ui_config_grafana
166
+ templates = ui_config_templates_folder
167
+ reports = ui_config_reports_folder
168
+ images = ui_config_images_folder(templates)
169
+ retention = ui_config_retention
170
+
171
+ config_yaml = %(# This configuration has been built with the configuration wizard.
172
+
173
+ #{grafana}
174
+
175
+ grafana-reporter:
176
+ templates-folder: #{templates}
177
+ reports-folder: #{reports}
178
+ report-retention: #{retention}
179
+ webservice-port: #{port}
180
+
181
+ default-document-attributes:
182
+ imagesdir: #{images}
183
+ # feel free to add here additional asciidoctor document attributes which are applied to all your templates
184
+ )
185
+
186
+ begin
187
+ File.write(CONFIG_FILE, config_yaml, mode: 'w')
188
+ puts 'Configuration file successfully created.'
189
+ rescue StandardError => e
190
+ raise e
191
+ end
192
+
193
+ config = Configuration.new
194
+ begin
195
+ config.config = YAML.load_file(CONFIG_FILE)
196
+ puts 'Configuration file validated successfully.'
197
+ rescue StandardError => e
198
+ raise ConfigurationError, "Could not read config file '#{CONFIG_FILE}' (Error: #{e.message})\n"\
199
+ "Source:\n#{File.read(CONFIG_FILE)}"
200
+ end
201
+
202
+ # create a demo report
203
+ unless Dir.exist?(config.templates_folder)
204
+ puts "Skip creation of DEMO template, as folder '#{config.templates_folder}' does not exist."
205
+ return
206
+ end
207
+ demo_report = %(= First Grafana Report Template
208
+
209
+ include::grafana_help[]
210
+
211
+ include::grafana_environment[])
212
+
213
+ demo_report_file = "#{config.templates_folder}demo_report.adoc"
214
+ if File.exist?(demo_report_file)
215
+ puts "Skip creation of DEMO template, as file '#{demo_report_file}' already exists."
216
+ else
217
+ begin
218
+ File.write(demo_report_file, demo_report, mode: 'w')
219
+ puts "DEMO template '#{demo_report_file}' successfully created."
220
+ rescue StandardError => e
221
+ raise e
222
+ end
223
+ end
224
+
225
+ puts
226
+ puts 'Now everything is setup properly. Run the grafana reporter without any command to start the service.'
227
+ puts
228
+ puts ' ruby-grafana-reporter'
229
+ puts
230
+ puts "Open 'http://localhost:#{config.webserver_port}/render?var-template=demo_report' in a webbrowser to"
231
+ puts 'verify your configuration.'
232
+ end
233
+
234
+ def ui_config_grafana
235
+ valid = false
236
+ url = nil
237
+ api_key = nil
238
+ datasources = ''
239
+ until valid
240
+ url ||= user_input('Specify grafana host', 'http://localhost:3000')
241
+ print "Testing connection to '#{url}' #{api_key ? '_with_' : '_without_'} API key..."
242
+ begin
243
+ res = Grafana::Grafana.new(url,
244
+ api_key,
245
+ logger: @logger).test_connection
246
+ rescue StandardError => e
247
+ puts
248
+ puts e.message
249
+ end
250
+ puts 'done.'
251
+
252
+ case res
253
+ when 'Admin'
254
+ valid = true
255
+
256
+ when 'NON-Admin'
257
+ print 'Access to grafana is permitted as NON-Admin. Do you want to use an [a]pi key,'\
258
+ ' configure [d]atasource manually, [r]e-enter api key or [i]gnore? [adRi]: '
259
+
260
+ case gets
261
+ when /(?:i|I)$/
262
+ valid = true
263
+
264
+ when /(?:a|A)$/
265
+ print 'Enter API key: '
266
+ api_key = gets.sub(/\n$/, '')
267
+
268
+ when /(?:r|R|adRi)$/
269
+ api_key = nil
270
+
271
+ when /(?:d|D)$/
272
+ valid = true
273
+ datasources = ui_config_datasources
274
+
275
+ end
276
+
277
+ else
278
+ print "Grafana could not be accessed at '#{url}'. Do you want do [r]e-enter url, or"\
279
+ ' [i]gnore and proceed? [Ri]: '
280
+
281
+ case gets
282
+ when /(?:i|I)$/
283
+ valid = true
284
+
285
+ else
286
+ url = nil
287
+ api_key = nil
288
+
289
+ end
290
+
291
+ end
292
+ end
293
+ %(grafana:
294
+ default:
295
+ host: #{url}#{api_key ? "\n api_key: #{api_key}" : ''}#{datasources ? "\n#{datasources}" : ''}
296
+ )
297
+ end
298
+
299
+ def ui_config_datasources
300
+ finished = false
301
+ datasources = []
302
+ until finished
303
+ item = {}
304
+ print "Datasource ###{datasources.length + 1}) Enter datasource name as configured in grafana: "
305
+ item[:ds_name] = gets.sub(/\n$/, '')
306
+ print "Datasource ###{datasources.length + 1}) Enter datasource id: "
307
+ item[:ds_id] = gets.sub(/\n$/, '')
308
+
309
+ puts
310
+ selection = user_input("Datasource name: '#{item[:ds_name]}', Datasource id: '#{item[:ds_id]}'."\
311
+ ' [A]ccept, [r]etry or [c]ancel?', 'Arc')
312
+
313
+ case selection
314
+ when /(?:Arc|A|a)$/
315
+ datasources << item
316
+ another = user_input('Add [a]nother datasource or [d]one?', 'aD')
317
+ finished = true if another =~ /(?:d|D)$/
318
+
319
+ when /(?:c|C)$/
320
+ finished = true
321
+
322
+ end
323
+ end
324
+ " datasources:\n#{datasources.collect { |el| " #{el[:ds_name]}: #{el[:ds_id]}" }.join('\n')}"
325
+ end
326
+
327
+ def ui_config_port
328
+ input = nil
329
+ until input
330
+ input = user_input('Specify port on which reporter shall run', '8815')
331
+ input = nil unless input =~ /[0-9]+/
332
+ end
333
+ input
334
+ end
335
+
336
+ def ui_config_templates_folder
337
+ input = nil
338
+ until input
339
+ input = user_input('Specify path where templates shall be stored', './templates')
340
+ input = nil unless validate_config_folder(input)
341
+ end
342
+ input
343
+ end
344
+
345
+ def ui_config_reports_folder
346
+ input = nil
347
+ until input
348
+ input = user_input('Specify path where created reports shall be stored', './reports')
349
+ input = nil unless validate_config_folder(input)
350
+ end
351
+ input
352
+ end
353
+
354
+ def ui_config_images_folder(parent)
355
+ input = nil
356
+ until input
357
+ input = user_input('Specify path where rendered images shall be stored (relative to templates folder)',
358
+ './images')
359
+ input = nil unless validate_config_folder(File.join(parent, input))
360
+ end
361
+ input
362
+ end
363
+
364
+ def ui_config_retention
365
+ input = nil
366
+ until input
367
+ input = user_input('Specify report retention duration in hours', '24')
368
+ input = nil unless input =~ /[0-9]+/
369
+ end
370
+ input
371
+ end
372
+
373
+ def user_input(text, default)
374
+ print "#{text} [#{default}]: "
375
+ input = gets.gsub(/\n$/, '')
376
+ input = default if input.empty?
377
+ input
378
+ end
379
+
380
+ def validate_config_folder(folder)
381
+ return true if Dir.exist?(folder)
382
+
383
+ print "Directory '#{folder} does not exist: [c]reate, [r]e-enter path or [i]gnore? [cRi]: "
384
+ case gets
385
+ when /^(?:c|C)$/
386
+ begin
387
+ Dir.mkdir(folder)
388
+ puts "Directory '#{folder}' successfully created."
389
+ return true
390
+ rescue StandardError => e
391
+ puts "WARN: Directory '#{folder}' could not be created. Please create it manually."
392
+ puts e.message
393
+ end
394
+
395
+ when /^(?:i|I)$/
396
+ puts "WARN: Directory '#{folder}' does not exist. Please create manually."
397
+ return true
398
+ end
399
+
400
+ false
401
+ end
402
+ end
403
+ end
404
+ end