ruby-grafana-reporter 0.1.6

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 +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +248 -0
  4. data/lib/VERSION.rb +3 -0
  5. data/lib/grafana/abstract_panel_query.rb +20 -0
  6. data/lib/grafana/abstract_query.rb +127 -0
  7. data/lib/grafana/abstract_sql_query.rb +42 -0
  8. data/lib/grafana/dashboard.rb +66 -0
  9. data/lib/grafana/errors.rb +61 -0
  10. data/lib/grafana/grafana.rb +131 -0
  11. data/lib/grafana/panel.rb +39 -0
  12. data/lib/grafana/panel_image_query.rb +49 -0
  13. data/lib/grafana/variable.rb +259 -0
  14. data/lib/grafana_reporter/abstract_report.rb +109 -0
  15. data/lib/grafana_reporter/application/application.rb +229 -0
  16. data/lib/grafana_reporter/application/errors.rb +30 -0
  17. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +99 -0
  18. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +96 -0
  19. data/lib/grafana_reporter/asciidoctor/errors.rb +37 -0
  20. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +86 -0
  21. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +86 -0
  22. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +67 -0
  23. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +65 -0
  24. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +58 -0
  25. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +75 -0
  26. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +70 -0
  27. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +18 -0
  28. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +41 -0
  29. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +202 -0
  30. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +67 -0
  31. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +65 -0
  32. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +57 -0
  33. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +32 -0
  34. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +23 -0
  35. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +43 -0
  36. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +36 -0
  37. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +309 -0
  38. data/lib/grafana_reporter/asciidoctor/report.rb +159 -0
  39. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +34 -0
  40. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +32 -0
  41. data/lib/grafana_reporter/configuration.rb +326 -0
  42. data/lib/grafana_reporter/errors.rb +38 -0
  43. data/lib/grafana_reporter/logger/two_way_logger.rb +52 -0
  44. data/lib/ruby-grafana-reporter.rb +27 -0
  45. metadata +88 -0
@@ -0,0 +1,159 @@
1
+ module GrafanaReporter
2
+ module Asciidoctor
3
+ # Implementation of a specific {AbstractReport}. It is used to
4
+ # build reports specifically for asciidoctor results.
5
+ class Report < GrafanaReporter::AbstractReport
6
+ # (see AbstractReport#initialize)
7
+ def initialize(config, template, destination_file_or_path = nil, custom_attributes = {})
8
+ super
9
+ @current_pos = 0
10
+ @image_files = []
11
+ @grafana_instances = {}
12
+ end
13
+
14
+ # Starts to create an asciidoctor report. It utilizes all {Extensions} to
15
+ # realize the conversion.
16
+ # @see AbstractReport#create_report
17
+ # @return [void]
18
+ def create_report
19
+ @start_time = Time.now
20
+ attrs = {'convert-backend' => 'pdf'}.merge(@config.default_document_attributes.merge(@custom_attributes))
21
+ attrs['grafana-report-timestamp'] = @start_time.to_s
22
+ logger.info('Report started at ' + @start_time.to_s)
23
+ logger.debug('Document attributes: ' + attrs.to_s)
24
+
25
+ initialize_step_counter
26
+
27
+ # register necessary extensions for the current report
28
+ ::Asciidoctor::LoggerManager.logger = logger
29
+
30
+ registry = ::Asciidoctor::Extensions::Registry.new
31
+ #TODO dynamically register macros, which is also needed when supporting custom macros
32
+ registry.inline_macro Extensions::PanelImageInlineMacro.new.current_report(self)
33
+ registry.inline_macro Extensions::PanelQueryValueInlineMacro.new.current_report(self)
34
+ registry.inline_macro Extensions::PanelPropertyInlineMacro.new.current_report(self)
35
+ registry.inline_macro Extensions::SqlValueInlineMacro.new.current_report(self)
36
+ registry.block_macro Extensions::PanelImageBlockMacro.new.current_report(self)
37
+ registry.include_processor Extensions::ValueAsVariableIncludeProcessor.new.current_report(self)
38
+ registry.include_processor Extensions::PanelQueryTableIncludeProcessor.new.current_report(self)
39
+ registry.include_processor Extensions::SqlTableIncludeProcessor.new.current_report(self)
40
+ registry.include_processor Extensions::ShowEnvironmentIncludeProcessor.new.current_report(self)
41
+ registry.include_processor Extensions::ShowHelpIncludeProcessor.new.current_report(self)
42
+ registry.include_processor Extensions::AnnotationsTableIncludeProcessor.new.current_report(self)
43
+ registry.include_processor Extensions::AlertsTableIncludeProcessor.new.current_report(self)
44
+
45
+ ::Asciidoctor.convert_file(@template, extension_registry: registry, backend: attrs['convert-backend'], to_file: path, attributes: attrs, header_footer: true)
46
+
47
+ @destination_file_or_path.close if @destination_file_or_path.is_a?(File)
48
+
49
+ # store report including als images as ZIP file, if the result is not a PDF
50
+ # TODO add tests for zipping results
51
+ if attrs['convert-backend'] != 'pdf'
52
+ dest_path = @destination_file_or_path
53
+ dest_path = @destination_file_or_path.path if @destination_file_or_path.is_a?(File)
54
+
55
+ # build zip file
56
+ zip_file = Tempfile.new("gf_zip")
57
+ Zip::File.open(zip_file.path, Zip::File::CREATE) do |zipfile|
58
+ # add report file
59
+ zipfile.get_output_stream(dest_path.gsub(@config.reports_folder, '') + ".#{attrs['convert-backend']}") { |f| f.puts File.read(dest_path) }
60
+
61
+ # add image files
62
+ @image_files.each do |file|
63
+ zipfile.get_output_stream(file.path.gsub(@config.images_folder, '')) { |f| f.puts File.read(file.path) }
64
+ end
65
+ end
66
+
67
+ # replace original file with zip file
68
+ zip_file.rewind
69
+ begin
70
+ File.write(dest_path, zip_file.read)
71
+ rescue => e
72
+ logger.fatal("Could not overwrite report file '#{dest_path}' with ZIP file. (#{e.message}).")
73
+ end
74
+
75
+ # cleanup temporary zip file
76
+ zip_file.close
77
+ zip_file.unlink
78
+ end
79
+
80
+ clean_image_files
81
+ @end_time = Time.now
82
+ logger.info('Report finished after ' + (@end_time - @start_time).to_s + ' seconds.')
83
+ @done = true
84
+ rescue StandardError => e
85
+ # catch all errors during execution
86
+ died_with_error(e)
87
+ raise e
88
+ end
89
+
90
+ # @see AbstractReport#progress
91
+ # @return [Float] number between 0 and 1 reflecting the current progress.
92
+ def progress
93
+ return 0 if @total_steps.to_i.zero?
94
+
95
+ @current_pos.to_f / @total_steps
96
+ end
97
+
98
+ # @param instance [String] requested grafana instance
99
+ # @return [Grafana::Grafana] the requested grafana instance.
100
+ def grafana(instance)
101
+ unless @grafana_instances[instance]
102
+ @grafana_instances[instance] = ::Grafana::Grafana.new(@config.grafana_host(instance), @config.grafana_api_key(instance), logger: @logger, datasources: @config.grafana_datasources(instance))
103
+ end
104
+ @grafana_instances[instance]
105
+ end
106
+
107
+ # Increments the progress.
108
+ # @return [Integer] number of the current progress position.
109
+ def next_step
110
+ @current_pos += 1
111
+ @current_pos
112
+ end
113
+
114
+ # Called to save a temporary image file. After the final generation of the
115
+ # report, these temporary files will automatically be removed.
116
+ # @param img_data [String] image file raw data, which shall be saved
117
+ # @return [String] path to the temporary file.
118
+ def save_image_file(img_data)
119
+ file = Tempfile.new(['gf_image_', '.png'], @config.images_folder.to_s)
120
+ file.write(img_data)
121
+ path = file.path.gsub(/#{@config.images_folder}/, '')
122
+
123
+ @image_files << file
124
+ file.close
125
+
126
+ path
127
+ end
128
+
129
+ # Called, if the report generation has died with an error.
130
+ # @param e [StandardError] occured error
131
+ # @return [void]
132
+ def died_with_error(e)
133
+ @error = [e.message] << [e.backtrace]
134
+ @end_time = Time.now
135
+ @done = true
136
+ end
137
+
138
+ private
139
+
140
+ def clean_image_files
141
+ @image_files.each(&:unlink)
142
+ @image_files = []
143
+ end
144
+
145
+ def initialize_step_counter
146
+ @total_steps = 0
147
+ File.readlines(@template).each do |line|
148
+ begin
149
+ @total_steps += line.gsub(%r{//.*}, '').scan(/(?:grafana_panel_image|grafana_panel_query_value|grafana_panel_query_table|grafana_sql_value|grafana_sql_table|grafana_environment|grafana_help|grafana_panel_property|grafana_annotations|grafana_alerts|grafana_value_as_variable)/).length
150
+ rescue => e
151
+ logger.error("Could not process line '#{line}' (Error: #{e.message})")
152
+ raise e
153
+ end
154
+ end
155
+ logger.debug("Template #{@template} contains #{@total_steps.to_s} calls of grafana reporter functions.")
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,34 @@
1
+ module GrafanaReporter
2
+ module Asciidoctor
3
+ # This class is being used to execute a SQL query against a grafana datasource.
4
+ # Only the first result in the first column will be returned as a single value.
5
+ class SqlFirstValueQuery < Grafana::AbstractSqlQuery
6
+ include QueryMixin
7
+
8
+ # Executes {QueryMixin#format_columns}, {QueryMixin#replace_values} and
9
+ # {QueryMixin#filter_columns} on the query results.
10
+ #
11
+ # Finally only the first value in the first row and the first column of
12
+ # will be returned.
13
+ # @return [void]
14
+ def post_process
15
+ results = preformat_sql_result(@result.body)
16
+ results = format_columns(results, @variables['format'])
17
+ results = replace_values(results, @variables.select { |k, _v| k =~ /^replace_values_\d+/ })
18
+ results = filter_columns(results, @variables['filter_columns'])
19
+ if @variables['filter_column']
20
+ @report.logger.warn("DEPRECATED: Call of no longer supported function 'filter_column' has been found. Rename to 'filter_columns'")
21
+ results = filter_columns(results, @variables['filter_column'])
22
+ end
23
+
24
+ unless results[:content].empty?
25
+ unless results[:content][0].empty?
26
+ @result = results[:content][0][0]
27
+ return
28
+ end
29
+ end
30
+ @result = ''
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,32 @@
1
+ module GrafanaReporter
2
+ module Asciidoctor
3
+ # This class is being used to execute a SQL query against a grafana datasource.
4
+ # The results will be formatted as as asciidoctor table.
5
+ class SqlTableQuery < Grafana::AbstractSqlQuery
6
+ include QueryMixin
7
+
8
+ # Executes {QueryMixin#format_columns}, {QueryMixin#replace_values} and
9
+ # {QueryMixin#filter_columns} on the query results.
10
+ #
11
+ # Finally the results are formatted as a asciidoctor table.
12
+ # @return [void]
13
+ def post_process
14
+ results = preformat_sql_result(@result.body)
15
+ results = format_columns(results, @variables['format'])
16
+ results = replace_values(results, @variables.select { |k, _v| k =~ /^replace_values_\d+/ })
17
+ results = filter_columns(results, @variables['filter_columns'])
18
+ if @variables['filter_column']
19
+ @report.logger.warn("DEPRECATED: Call of no longer supported function 'filter_column' has been found. Rename to 'filter_columns'")
20
+ results = filter_columns(results, @variables['filter_column'])
21
+ end
22
+ results = transpose(results, @variables['transpose'])
23
+ row_divider = '| '
24
+ row_divider = @variables['row_divider'].raw_value if @variables['row_divider'].is_a?(Grafana::Variable)
25
+ column_divider = ' | '
26
+ column_divider = @variables['column_divider'].raw_value if @variables['column_divider'].is_a?(Grafana::Variable)
27
+
28
+ @result = results[:content].map { |row| row_divider + row.map { |item| column_divider == ' | ' ? item.to_s.gsub('|', '\\|') : item.to_s }.join(column_divider) }
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,326 @@
1
+ # In this namespace all objects needed for the grafana reporter are collected.
2
+ module GrafanaReporter
3
+ # Used to store the whole settings, which are necessary to run the reporter.
4
+ # It can read configuration files, but might also be configured programmatically.
5
+ #
6
+ # This class also contains a function {#validate}, which ensures that the
7
+ # provided settings are set properly.
8
+ #
9
+ # Using this class is embedded in the {Application::Application#configure_and_run}.
10
+ #
11
+ # TODO add config example
12
+ class Configuration
13
+ # @return [AbstractReport] specific report class, which should be used.
14
+ attr_accessor :report_class
15
+
16
+ # Returned by {#mode} if only a connection test shall be executed.
17
+ MODE_CONNECTION_TEST = 'test'
18
+ # Returned by {#mode} if only one configured report shall be rendered.
19
+ MODE_SINGLE_RENDER = 'single-render'
20
+ # Returned by {#mode} if the default webservice shall be started.
21
+ MODE_SERVICE = 'webservice'
22
+
23
+ def initialize
24
+ @config = {}
25
+ @logger = ::Logger.new(STDERR, level: :unknown)
26
+ # TODO: set report class somewhere else, but make it known here
27
+ self.report_class = Asciidoctor::Report
28
+ end
29
+
30
+ attr_reader :logger
31
+
32
+ # @return [String] mode, in which the reporting shall be executed. One of {MODE_CONNECTION_TEST}, {MODE_SINGLE_RENDER} and {MODE_SERVICE}.
33
+ def mode
34
+ return MODE_SERVICE if get_config('grafana-reporter:run-mode') != MODE_CONNECTION_TEST and get_config('grafana-reporter:run-mode') != MODE_SINGLE_RENDER
35
+
36
+ return get_config('grafana-reporter:run-mode')
37
+ end
38
+
39
+ # @return [String] configured report template. Only needed in {MODE_SINGLE_RENDER}.
40
+ def template
41
+ get_config('default-document-attributes:var-template')
42
+ end
43
+
44
+ # @return [String] destination filename for the report in {MODE_SINGLE_RENDER}.
45
+ def to_file
46
+ return get_config('to_file') || true if mode == MODE_SINGLE_RENDER
47
+
48
+ get_config('to_file')
49
+ end
50
+
51
+ # @return [Array<String>] names of the configured grafana_instances.
52
+ def grafana_instances
53
+ instances = get_config('grafana')
54
+ instances.keys
55
+ end
56
+
57
+ # @param instance [String] grafana instance name, for which the value shall be retrieved.
58
+ # @return [String] configured 'host' for the requested grafana instance.
59
+ def grafana_host(instance = 'default')
60
+ host = get_config("grafana:#{instance}:host")
61
+ raise GrafanaInstanceWithoutHostError, instance if host.nil?
62
+
63
+ host
64
+ end
65
+
66
+ # @param instance [String] grafana instance name, for which the value shall be retrieved.
67
+ # @return [String] configured 'api_key' for the requested grafana instance.
68
+ def grafana_api_key(instance = 'default')
69
+ get_config("grafana:#{instance}:api_key")
70
+ end
71
+
72
+ # @param instance [String] grafana instance name, for which the value shall be retrieved.
73
+ # @return [Hash<String,Integer>] configured datasources for the requested grafana instance. Name as key, ID as value.
74
+ def grafana_datasources(instance = 'default')
75
+ hash = get_config("grafana:#{instance}:datasources")
76
+ return nil if hash.nil?
77
+
78
+ hash.map { |k, v| [k, v] }.to_h
79
+ end
80
+
81
+ # @return [String] configured folder, in which the report templates are stored including trailing slash. By default: current folder.
82
+ def templates_folder
83
+ result = get_config('grafana-reporter:templates-folder') || '.'
84
+ result.sub!(%r{[/]*$}, '/') unless result.empty?
85
+ result
86
+ end
87
+
88
+ # Returns configured folder, in which temporary images during report generation
89
+ # shall be stored including trailing slash. Folder has to be a subfolder of
90
+ # {#templates_folder}. By default: current folder.
91
+ # @return [String] configured folder, in which temporary images shall be stored.
92
+ def images_folder
93
+ img_path = templates_folder
94
+ img_path = img_path.empty? ? get_config('default-document-attributes:imagesdir').to_s : img_path + get_config('default-document-attributes:imagesdir').to_s
95
+ img_path.empty? ? './' : img_path.sub(%r{[/]*$}, '/')
96
+ end
97
+
98
+ # @return [String] name of grafana instance, against which a test shall be executed
99
+ def test_instance
100
+ get_config('grafana-reporter:test-instance')
101
+ end
102
+
103
+ # @return [String] configured folder, in which the reports shall be stored including trailing slash. By default: current folder.
104
+ def reports_folder
105
+ result = get_config('grafana-reporter:reports-folder') || '.'
106
+ result.sub!(%r{[/]*$}, '/') unless result.empty?
107
+ result
108
+ end
109
+
110
+ # @return [Integer] how many hours a generated report shall be retained, before it shall be deleted. By default: 24.
111
+ def report_retention
112
+ get_config('grafana-reporter:report-retention') || 24
113
+ end
114
+
115
+ # @return [Integer] port, on which the webserver shall run. By default: 8815.
116
+ def webserver_port
117
+ get_config('grafana-reporter:webservice-port') || 8815
118
+ end
119
+
120
+ # The configuration made with the setting 'default-document-attributes' will
121
+ # be passed 1:1 to the asciidoctor report service. It can be used to preconfigure
122
+ # whatever is essential for the needed report renderings.
123
+ # @return [Hash] configured document attributes
124
+ def default_document_attributes
125
+ get_config('default-document-attributes') || {}
126
+ end
127
+
128
+ # Used to load the configuration of a file or a manually created Hash to this
129
+ # object. To make sure, that the configuration is valid, call {#validate}.
130
+ #
131
+ # NOTE: This function overwrites all existing configurations
132
+ # @param hash [Hash] configuration settings
133
+ # @return [void]
134
+ def load_config(hash)
135
+ @config = hash
136
+ end
137
+
138
+ # Used to do the configuration by a command line call. Therefore also help will
139
+ # be shown, in case no parameter has been given.
140
+ # @param params [Array<String>] command line parameters, mainly ARGV can be used.
141
+ # @return [Integer] 0 if everything is fine, -1 if execution shall be aborted.
142
+ def configure_by_command_line(params = [])
143
+ params << '--help' if params.empty?
144
+
145
+ parser = OptionParser.new do |opts|
146
+ opts.banner = 'Usage: ruby ruby-grafana-reporter.rb CONFIG_FILE [options]'
147
+
148
+ opts.on('-d', '--debug LEVEL', 'Specify detail level: FATAL, ERROR, WARN, INFO, DEBUG.') do |level|
149
+ @logger.level = Object.const_get("::Logger::Severity::#{level}") if level =~ /(?:FATAL|ERROR|WARN|INFO|DEBUG)/
150
+ end
151
+
152
+ opts.on('--test GRAFANA_INSTANCE', 'test current configuration against given GRAFANA_INSTANCE') do |instance|
153
+ if get_config('grafana-reporter')
154
+ @config['grafana-reporter']['run-mode'] = 'test'
155
+ else
156
+ @config.merge!({'grafana-reporter' => {'run-mode' => 'test'} })
157
+ end
158
+ @config['grafana-reporter']['test-instance'] = instance
159
+ end
160
+
161
+ opts.on('-t', '--template TEMPLATE', 'Render a single ASCIIDOC template to PDF and exit') do |template|
162
+ if get_config('grafana-reporter')
163
+ @config['grafana-reporter']['run-mode'] = 'single-render'
164
+ else
165
+ @config.merge!({'grafana-reporter' => {'run-mode' => 'single-render'} })
166
+ end
167
+ @config['default-document-attributes']['var-template'] = template
168
+ end
169
+
170
+ opts.on('-o', '--output FILE', 'Output filename if only a single file is rendered') do |file|
171
+ @config.merge!({ 'to_file' => file })
172
+ end
173
+
174
+ opts.on('-v', '--version', 'Version information') do
175
+ puts GRAFANA_REPORTER_VERSION.join('.')
176
+ return -1
177
+ end
178
+
179
+ opts.on('-h', '--help', 'Show this message') do
180
+ puts opts
181
+ return -1
182
+ end
183
+ end
184
+
185
+ unless params.empty?
186
+ if File.exist?(params[0])
187
+ config_file = params.slice!(0)
188
+ begin
189
+ load_config(YAML.load_file(config_file))
190
+ rescue StandardError => e
191
+ raise ConfigurationError, "Could not read CONFIG_FILE '#{config_file}' (Error: #{e.message})"
192
+ end
193
+ end
194
+ end
195
+ parser.parse!(params)
196
+
197
+ 0
198
+ end
199
+
200
+ # This function shall be called, before the configuration object is used in the
201
+ # {Application::Application#run}. It ensures, that everything is setup properly
202
+ # and all necessary folders exist. Appropriate errors are raised in case of errors.
203
+ # @return [void]
204
+ def validate
205
+ validate_schema(schema, @config)
206
+
207
+ # check if set folders exist
208
+ raise FolderDoesNotExistError.new(reports_folder, 'reports-folder') unless File.directory?(reports_folder)
209
+ raise FolderDoesNotExistError.new(templates - folder, 'templates-folder') unless File.directory?(templates_folder)
210
+ raise FolderDoesNotExistError.new(images - folder, 'images-folder') unless File.directory?(images_folder)
211
+ end
212
+
213
+ private
214
+
215
+ def get_config(path)
216
+ return if path.nil?
217
+
218
+ cur_pos = @config
219
+ path.split(':').each do |subpath|
220
+ cur_pos = cur_pos[subpath] if cur_pos
221
+ end
222
+ cur_pos
223
+ end
224
+
225
+ def validate_schema(schema, subject)
226
+ return nil if subject.nil?
227
+
228
+ schema.each do |key, config|
229
+ type, min_occurence, next_level = config
230
+
231
+ validate_schema(next_level, subject[key]) if next_level
232
+
233
+ if key.nil?
234
+ # apply to all on this level
235
+ if subject.is_a?(Hash)
236
+ if subject.length < min_occurence
237
+ raise ConfigurationDoesNotMatchSchemaError.new(key, 'occur', min_occurence, subject.length)
238
+ end
239
+
240
+ subject.each do |k, _v|
241
+ sub_scheme = {}
242
+ sub_scheme[k] = schema[nil]
243
+ validate_schema(sub_scheme, subject)
244
+ end
245
+
246
+ elsif subject.is_a?(Array)
247
+ if subject.length < min_occurence
248
+ raise ConfigurationDoesNotMatchSchemaError.new(key, 'occur', min_occurence, subject.length)
249
+ end
250
+
251
+ subject.each_index do |i|
252
+ sub_scheme = {}
253
+ sub_scheme[i] = schema[nil]
254
+ validate_schema(sub_scheme, subject)
255
+ end
256
+
257
+ else
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