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.
- checksums.yaml +7 -0
- data/LICENSE +20 -0
- data/README.md +248 -0
- data/lib/VERSION.rb +3 -0
- data/lib/grafana/abstract_panel_query.rb +20 -0
- data/lib/grafana/abstract_query.rb +127 -0
- data/lib/grafana/abstract_sql_query.rb +42 -0
- data/lib/grafana/dashboard.rb +66 -0
- data/lib/grafana/errors.rb +61 -0
- data/lib/grafana/grafana.rb +131 -0
- data/lib/grafana/panel.rb +39 -0
- data/lib/grafana/panel_image_query.rb +49 -0
- data/lib/grafana/variable.rb +259 -0
- data/lib/grafana_reporter/abstract_report.rb +109 -0
- data/lib/grafana_reporter/application/application.rb +229 -0
- data/lib/grafana_reporter/application/errors.rb +30 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +99 -0
- data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +96 -0
- data/lib/grafana_reporter/asciidoctor/errors.rb +37 -0
- data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +86 -0
- data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +86 -0
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +67 -0
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +65 -0
- data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +58 -0
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +75 -0
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +70 -0
- data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +18 -0
- data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +41 -0
- data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +202 -0
- data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +67 -0
- data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +65 -0
- data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +57 -0
- data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +32 -0
- data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +23 -0
- data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +43 -0
- data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +36 -0
- data/lib/grafana_reporter/asciidoctor/query_mixin.rb +309 -0
- data/lib/grafana_reporter/asciidoctor/report.rb +159 -0
- data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +34 -0
- data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +32 -0
- data/lib/grafana_reporter/configuration.rb +326 -0
- data/lib/grafana_reporter/errors.rb +38 -0
- data/lib/grafana_reporter/logger/two_way_logger.rb +52 -0
- data/lib/ruby-grafana-reporter.rb +27 -0
- 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
|