ruby-grafana-reporter 0.1.7 → 0.4.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -245
  3. data/bin/ruby-grafana-reporter +3 -2
  4. data/lib/VERSION.rb +6 -3
  5. data/lib/grafana/abstract_datasource.rb +116 -0
  6. data/lib/grafana/dashboard.rb +75 -66
  7. data/lib/grafana/errors.rb +81 -61
  8. data/lib/grafana/grafana.rb +130 -131
  9. data/lib/grafana/grafana_alerts_datasource.rb +57 -0
  10. data/lib/grafana/grafana_annotations_datasource.rb +56 -0
  11. data/lib/grafana/grafana_property_datasource.rb +25 -0
  12. data/lib/grafana/graphite_datasource.rb +44 -0
  13. data/lib/grafana/image_rendering_datasource.rb +44 -0
  14. data/lib/grafana/panel.rb +47 -39
  15. data/lib/grafana/prometheus_datasource.rb +39 -0
  16. data/lib/grafana/sql_datasource.rb +65 -0
  17. data/lib/grafana/variable.rb +218 -259
  18. data/lib/grafana/webrequest.rb +71 -0
  19. data/lib/grafana_reporter/abstract_query.rb +401 -0
  20. data/lib/grafana_reporter/abstract_report.rb +163 -109
  21. data/lib/grafana_reporter/alerts_table_query.rb +44 -0
  22. data/lib/grafana_reporter/annotations_table_query.rb +43 -0
  23. data/lib/grafana_reporter/application/application.rb +162 -229
  24. data/lib/grafana_reporter/application/errors.rb +33 -30
  25. data/lib/grafana_reporter/application/webservice.rb +242 -0
  26. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +90 -0
  27. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +89 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +76 -0
  29. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +77 -0
  30. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +72 -0
  31. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +98 -0
  32. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
  33. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +23 -0
  34. data/lib/grafana_reporter/asciidoctor/report.rb +172 -159
  35. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  36. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  37. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +92 -0
  38. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +88 -0
  39. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  40. data/lib/grafana_reporter/configuration.rb +310 -326
  41. data/lib/grafana_reporter/console_configuration_wizard.rb +319 -0
  42. data/lib/grafana_reporter/demo_report_wizard.rb +87 -0
  43. data/lib/grafana_reporter/errors.rb +81 -38
  44. data/lib/grafana_reporter/help.rb +447 -0
  45. data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
  46. data/lib/grafana_reporter/panel_image_query.rb +29 -0
  47. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  48. data/lib/grafana_reporter/query_value_query.rb +79 -0
  49. data/lib/grafana_reporter/report_webhook.rb +35 -0
  50. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +29 -27
  51. metadata +48 -60
  52. data/lib/grafana/abstract_panel_query.rb +0 -20
  53. data/lib/grafana/abstract_query.rb +0 -127
  54. data/lib/grafana/abstract_sql_query.rb +0 -42
  55. data/lib/grafana/panel_image_query.rb +0 -49
  56. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -99
  57. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
  58. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -37
  59. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -86
  60. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -86
  61. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -67
  62. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -65
  63. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -58
  64. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -75
  65. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -70
  66. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -18
  67. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -41
  68. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -202
  69. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -67
  70. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -65
  71. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -57
  72. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -32
  73. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -23
  74. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -43
  75. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -36
  76. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -309
  77. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -34
  78. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -32
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ module Asciidoctor
5
+ # Implements the hook
6
+ # include::grafana_environment[]
7
+ #
8
+ # Shows all available variables, which are accessible during this run of the asciidoctor
9
+ # grafana reporter in a asciidoctor readable form.
10
+ #
11
+ # This processor is very helpful during report template design, to find out the available
12
+ # variables, that can be accessed.
13
+ #
14
+ # == Used document parameters
15
+ # All, to be listed as the available environment.
16
+ class ShowEnvironmentIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor
17
+ include ProcessorMixin
18
+
19
+ # :nodoc:
20
+ def handles?(target)
21
+ target.start_with? 'grafana_environment'
22
+ end
23
+
24
+ # :nodoc:
25
+ def process(doc, reader, _target, _attrs)
26
+ # return if @report.cancel
27
+ @report.next_step
28
+ @report.logger.debug('Processing ShowEnvironmentIncludeProcessor')
29
+
30
+ vars = ['== Accessible Variables',
31
+ '|===']
32
+ doc.attributes.sort.each do |k, v|
33
+ vars << "| `+{#{k}}+` | #{v}"
34
+ end
35
+ vars << '|==='
36
+
37
+ reader.unshift_lines vars
38
+ end
39
+
40
+ # @see ProcessorMixin#build_demo_entry
41
+ def build_demo_entry(_panel)
42
+ 'include::grafana_environment[]'
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ module Asciidoctor
5
+ # Implements the hook
6
+ # include::grafana_help[]
7
+ #
8
+ # Shows all available options for the asciidoctor grafana reporter in a asciidoctor readable form.
9
+ #
10
+ # == Used document parameters
11
+ # None
12
+ class ShowHelpIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor
13
+ include ProcessorMixin
14
+
15
+ # :nodoc:
16
+ def handles?(target)
17
+ target.start_with? 'grafana_help'
18
+ end
19
+
20
+ # :nodoc:
21
+ def process(_doc, reader, _target, _attrs)
22
+ # return if @report.cancel
23
+ @report.next_step
24
+ @report.logger.debug('Processing ShowHelpIncludeProcessor')
25
+
26
+ reader.unshift_lines GrafanaReporter::Help.new.asciidoctor.split("\n")
27
+ end
28
+
29
+ # @see ProcessorMixin#build_demo_entry
30
+ def build_demo_entry(_panel)
31
+ 'include::grafana_help[]'
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ module Asciidoctor
5
+ # Implements the hook
6
+ # include::grafana_sql_table:<datasource_id>[<options>]
7
+ #
8
+ # Returns the results of the SQL query as a asciidoctor table.
9
+ #
10
+ # == Used document parameters
11
+ # +grafana_default_instance+ - name of grafana instance, 'default' if not specified
12
+ #
13
+ # +from+ - 'from' time for the sql query
14
+ #
15
+ # +to+ - 'to' time for the sql query
16
+ #
17
+ # All other variables starting with +var-+ will be used to replace grafana templating strings
18
+ # in the given SQL query.
19
+ #
20
+ # == Supported options
21
+ # +sql+ - sql statement (*mandatory*)
22
+ #
23
+ # +instance+ - name of grafana instance, 'default' if not specified
24
+ #
25
+ # +from+ - 'from' time for the sql query
26
+ #
27
+ # +to+ - 'to' time for the sql query
28
+ #
29
+ # +format+ - see {QueryMixin#format_columns}
30
+ #
31
+ # +replace_values+ - see {QueryMixin#replace_values}
32
+ #
33
+ # +filter_columns+ - see {QueryMixin#filter_columns}
34
+ class SqlTableIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor
35
+ include ProcessorMixin
36
+
37
+ # :nodoc:
38
+ def handles?(target)
39
+ target.start_with? 'grafana_sql_table:'
40
+ end
41
+
42
+ # :nodoc:
43
+ def process(doc, reader, target, attrs)
44
+ return if @report.cancel
45
+
46
+ @report.next_step
47
+ instance = attrs['instance'] || doc.attr('grafana_default_instance') || 'default'
48
+ attrs['result_type'] = 'sql_table'
49
+ @report.logger.debug("Processing SqlTableIncludeProcessor (instance: #{instance},"\
50
+ " datasource: #{target.split(':')[1]}, sql: #{attrs['sql']})")
51
+
52
+ begin
53
+ # catch properly if datasource could not be identified
54
+ query = QueryValueQuery.new(@report.grafana(instance))
55
+ query.datasource = @report.grafana(instance).datasource_by_id(target.split(':')[1].to_i)
56
+ query.raw_query = attrs['sql']
57
+ query.merge_hash_variables(doc.attributes, attrs)
58
+ @report.logger.debug("from: #{query.from}, to: #{query.to}")
59
+
60
+ reader.unshift_lines query.execute
61
+ rescue GrafanaReporterError => e
62
+ @report.logger.error(e.message)
63
+ reader.unshift_line "|#{e.message}"
64
+ rescue StandardError => e
65
+ @report.logger.fatal(e.message)
66
+ reader.unshift_line "|#{e.message}"
67
+ end
68
+
69
+ reader
70
+ end
71
+
72
+ # @see ProcessorMixin#build_demo_entry
73
+ def build_demo_entry(panel)
74
+ return nil unless panel
75
+ return nil unless panel.model['type'].include?('table')
76
+
77
+ ref_id = nil
78
+ panel.model['targets'].each do |item|
79
+ if !item['hide'] && !panel.query(item['refId']).to_s.empty?
80
+ ref_id = item['refId']
81
+ break
82
+ end
83
+ end
84
+ return nil unless ref_id
85
+
86
+ "|===\ninclude::grafana_sql_table:#{panel.dashboard.grafana.datasource_by_name(panel.model['datasource']).id}"\
87
+ "[sql=\"#{panel.query(ref_id).gsub(/"/, '\"').gsub("\n", ' ').gsub(/\\/, '\\\\')}\",filter_columns=\"time\","\
88
+ "dashboard=\"#{panel.dashboard.id}\",from=\"now-1h\",to=\"now\"]\n|==="
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ module Asciidoctor
5
+ # Implements the hook
6
+ # grafana_sql_value:<datasource_id>[<options>]
7
+ #
8
+ # Returns the first value of the resulting SQL query.
9
+ #
10
+ # == Used document parameters
11
+ # +grafana_default_instance+ - name of grafana instance, 'default' if not specified
12
+ #
13
+ # +from+ - 'from' time for the sql query
14
+ #
15
+ # +to+ - 'to' time for the sql query
16
+ #
17
+ # All other variables starting with +var-+ will be used to replace grafana templating strings
18
+ # in the given SQL query.
19
+ #
20
+ # == Supported options
21
+ # +sql+ - sql statement (*mandatory*)
22
+ #
23
+ # +instance+ - name of grafana instance, 'default' if not specified
24
+ #
25
+ # +from+ - 'from' time for the sql query
26
+ #
27
+ # +to+ - 'to' time for the sql query
28
+ #
29
+ # +format+ - see {QueryMixin#format_columns}
30
+ #
31
+ # +replace_values+ - see {QueryMixin#replace_values}
32
+ #
33
+ # +filter_columns+ - see {QueryMixin#filter_columns}
34
+ class SqlValueInlineMacro < ::Asciidoctor::Extensions::InlineMacroProcessor
35
+ include ProcessorMixin
36
+ use_dsl
37
+
38
+ named :grafana_sql_value
39
+
40
+ # @see GrafanaReporter::Asciidoctor::SqlFirstValueQuery
41
+ def process(parent, target, attrs)
42
+ return if @report.cancel
43
+
44
+ @report.next_step
45
+ instance = attrs['instance'] || parent.document.attr('grafana_default_instance') || 'default'
46
+ attrs['result_type'] = 'sql_value'
47
+ @report.logger.debug("Processing SqlValueInlineMacro (instance: #{instance}, datasource: #{target},"\
48
+ " sql: #{attrs['sql']})")
49
+
50
+ begin
51
+ # catch properly if datasource could not be identified
52
+ query = QueryValueQuery.new(@report.grafana(instance))
53
+ query.datasource = @report.grafana(instance).datasource_by_id(target)
54
+ query.raw_query = attrs['sql']
55
+ query.merge_hash_variables(parent.document.attributes, attrs)
56
+ @report.logger.debug("from: #{query.from}, to: #{query.to}")
57
+
58
+ create_inline(parent, :quoted, query.execute)
59
+ rescue GrafanaReporterError => e
60
+ @report.logger.error(e.message)
61
+ create_inline(parent, :quoted, e.message)
62
+ rescue StandardError => e
63
+ @report.logger.fatal(e.message)
64
+ create_inline(parent, :quoted, e.message)
65
+ end
66
+ end
67
+
68
+ # @see ProcessorMixin#build_demo_entry
69
+ def build_demo_entry(panel)
70
+ return nil unless panel
71
+ return nil unless panel.model['type'] == 'singlestat'
72
+
73
+ ref_id = nil
74
+ panel.model['targets'].each do |item|
75
+ if !item['hide'] && !panel.query(item['refId']).to_s.empty?
76
+ ref_id = item['refId']
77
+ break
78
+ end
79
+ end
80
+ return nil unless ref_id
81
+
82
+ "grafana_sql_value:#{panel.dashboard.grafana.datasource_by_name(panel.model['datasource']).id}"\
83
+ "[sql=\"#{panel.query(ref_id).gsub(/"/, '\"').gsub("\n", ' ').gsub(/\\/, '\\\\')}\",from=\"now-1h\","\
84
+ 'to="now"]'
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'processor_mixin'
4
+
5
+ module GrafanaReporter
6
+ module Asciidoctor
7
+ # Implements the hook
8
+ # include::grafana_value_as_variable[<options>]
9
+ #
10
+ # Returns an attribute definition in asciidoctor format. This is needed if you want to refer to values of
11
+ # a grafana query within a variable in asciidoctor. As this works without this function for the
12
+ # `IncludeProcessor`s values, it will not work for all the other processors.
13
+ #
14
+ # This method is just a proxy for all other hooks and will forward parameters accordingly.
15
+ #
16
+ # Example:
17
+ #
18
+ # include:grafana_value_as_variable[call="grafana_sql_value:1",variable_name="my_variable",sql="SELECT 'looks good'",<any_other_option>]
19
+ #
20
+ # This will call the {SqlValueInlineMacro} with `datasource_id` set to `1` and store the result in the
21
+ # variable. The resulting asciidoctor variable definition will be created as:
22
+ #
23
+ # :my_variable: looks good
24
+ #
25
+ # and can be refered to in your document easily as
26
+ #
27
+ # {my_variable}
28
+ #
29
+ # == Supported options
30
+ # +call+ - regular call to the reporter hook (*mandatory*)
31
+ #
32
+ # +variable_name+ - name of the variable, to which the result shall be assigned (*mandatory*)
33
+ class ValueAsVariableIncludeProcessor < ::Asciidoctor::Extensions::IncludeProcessor
34
+ include ProcessorMixin
35
+
36
+ # :nodoc:
37
+ def handles?(target)
38
+ target.start_with? 'grafana_value_as_variable'
39
+ end
40
+
41
+ # :nodoc:
42
+ def process(doc, reader, target, attrs)
43
+ return if @report.cancel
44
+
45
+ # increase step for this processor as well as it is also counted in the step counter
46
+ @report.next_step
47
+
48
+ call_attr = attrs.delete('call')
49
+ call, target = call_attr.split(':') if call_attr
50
+ attribute = attrs.delete('variable_name')
51
+ @report.logger.debug("Processing ValueAsVariableIncludeProcessor (call: #{call}, target: #{target},"\
52
+ " variable_name: #{attribute}, attrs: #{attrs})")
53
+ if !call || !attribute
54
+ @report.logger.error('ValueAsVariableIncludeProcessor: Missing mandatory attribute \'call\' or '\
55
+ '\'variable_name\'.')
56
+ # increase counter, as error occured and no sub call is being processed
57
+ @report.next_step
58
+ return reader
59
+ end
60
+
61
+ # TODO: remove dirty hack to allow the document as parameter for other processors
62
+ def doc.document
63
+ self
64
+ end
65
+
66
+ # TODO: properly show error messages also in document
67
+ ext = doc.extensions.find_inline_macro_extension(call) if doc.extensions.inline_macros?
68
+ if !ext
69
+ @report.logger.error('ValueAsVariableIncludeProcessor: Could not find inline macro extension for '\
70
+ "'#{call}'.")
71
+ # increase counter, as error occured and no sub call is being processed
72
+ @report.next_step
73
+ else
74
+ @report.logger.debug('ValueAsVariableIncludeProcessor: Calling sub-method.')
75
+ item = ext.process_method.call(doc, target, attrs)
76
+ if !item.text.to_s.empty?
77
+ result = ":#{attribute}: #{item.text}"
78
+ @report.logger.debug("ValueAsVariableIncludeProcessor: Adding '#{result}' to document.")
79
+ reader.unshift_line(result)
80
+ else
81
+ @report.logger.debug("ValueAsVariableIncludeProcessor: Not adding variable '#{attribute}'"\
82
+ ' as query result was empty.')
83
+ end
84
+ end
85
+
86
+ reader
87
+ end
88
+ end
89
+ end
90
+ end
@@ -1,326 +1,310 @@
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 #{$0} 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
1
+ # frozen_string_literal: true
2
+
3
+ # In this namespace all objects needed for the grafana reporter are collected.
4
+ module GrafanaReporter
5
+ # Used to store the whole settings, which are necessary to run the reporter.
6
+ # It can read configuration files, but might also be configured programmatically.
7
+ #
8
+ # This class also contains a function {#validate}, which ensures that the
9
+ # provided settings are set properly.
10
+ #
11
+ # Using this class is embedded in the {Application::Application#configure_and_run}.
12
+ #
13
+ class Configuration
14
+ # @return [AbstractReport] specific report class, which should be used.
15
+ attr_accessor :report_class
16
+
17
+ # Returned by {#mode} if only a connection test shall be executed.
18
+ MODE_CONNECTION_TEST = 'test'
19
+ # Returned by {#mode} if only one configured report shall be rendered.
20
+ MODE_SINGLE_RENDER = 'single-render'
21
+ # Returned by {#mode} if the default webservice shall be started.
22
+ MODE_SERVICE = 'webservice'
23
+
24
+ # Used to access the configuration hash. To make sure, that the configuration is
25
+ # valid, call {#validate}.
26
+ attr_reader :config
27
+
28
+ def initialize
29
+ @config = {}
30
+ @logger = ::Logger.new($stderr, level: :info)
31
+ end
32
+
33
+ attr_accessor :logger
34
+
35
+ # Used to overwrite the current configuration.
36
+ def config=(new_config)
37
+ @config = new_config
38
+ update_configuration
39
+ end
40
+
41
+ # @return [String] mode, in which the reporting shall be executed. One of {MODE_CONNECTION_TEST},
42
+ # {MODE_SINGLE_RENDER} and {MODE_SERVICE}.
43
+ def mode
44
+ if (get_config('grafana-reporter:run-mode') != MODE_CONNECTION_TEST) &&
45
+ (get_config('grafana-reporter:run-mode') != MODE_SINGLE_RENDER)
46
+ return MODE_SERVICE
47
+ end
48
+
49
+ get_config('grafana-reporter:run-mode')
50
+ end
51
+
52
+ # @return [String] full path of configured report template. Only needed in {MODE_SINGLE_RENDER}.
53
+ def template
54
+ return nil if get_config('default-document-attributes:var-template').nil?
55
+
56
+ "#{templates_folder}#{get_config('default-document-attributes:var-template')}.adoc"
57
+ end
58
+
59
+ # @return [String] destination filename for the report in {MODE_SINGLE_RENDER}.
60
+ def to_file
61
+ return get_config('to_file') || true if mode == MODE_SINGLE_RENDER
62
+
63
+ get_config('to_file')
64
+ end
65
+
66
+ # @return [Array<String>] names of the configured grafana_instances.
67
+ def grafana_instances
68
+ instances = get_config('grafana')
69
+ instances.keys
70
+ end
71
+
72
+ # @param instance [String] grafana instance name, for which the value shall be retrieved.
73
+ # @return [String] configured 'host' for the requested grafana instance.
74
+ def grafana_host(instance = 'default')
75
+ host = get_config("grafana:#{instance}:host")
76
+ raise GrafanaInstanceWithoutHostError, instance if host.nil?
77
+
78
+ host
79
+ end
80
+
81
+ # @param instance [String] grafana instance name, for which the value shall be retrieved.
82
+ # @return [String] configured 'api_key' for the requested grafana instance.
83
+ def grafana_api_key(instance = 'default')
84
+ get_config("grafana:#{instance}:api_key")
85
+ end
86
+
87
+ # @return [String] configured folder, in which the report templates are stored including trailing slash.
88
+ # By default: current folder.
89
+ def templates_folder
90
+ result = get_config('grafana-reporter:templates-folder') || '.'
91
+ return result.sub(%r{/*$}, '/') unless result.empty?
92
+
93
+ result
94
+ end
95
+
96
+ # Returns configured folder, in which temporary images during report generation
97
+ # shall be stored including trailing slash. Folder has to be a subfolder of
98
+ # {#templates_folder}. By default: current folder.
99
+ # @return [String] configured folder, in which temporary images shall be stored.
100
+ def images_folder
101
+ img_path = templates_folder
102
+ img_path = if img_path.empty?
103
+ get_config('default-document-attributes:imagesdir').to_s
104
+ else
105
+ img_path + get_config('default-document-attributes:imagesdir').to_s
106
+ end
107
+ img_path.empty? ? './' : img_path.sub(%r{/*$}, '/')
108
+ end
109
+
110
+ # @return [String] name of grafana instance, against which a test shall be executed
111
+ def test_instance
112
+ get_config('grafana-reporter:test-instance')
113
+ end
114
+
115
+ # @return [String] configured folder, in which the reports shall be stored including trailing slash.
116
+ # By default: current folder.
117
+ def reports_folder
118
+ result = get_config('grafana-reporter:reports-folder') || '.'
119
+ return result.sub(%r{/*$}, '/') unless result.empty?
120
+
121
+ result
122
+ end
123
+
124
+ # @return [Integer] how many hours a generated report shall be retained, before it shall be deleted.
125
+ # By default: 24.
126
+ def report_retention
127
+ get_config('grafana-reporter:report-retention') || 24
128
+ end
129
+
130
+ # @return [Integer] port, on which the webserver shall run. By default: 8815.
131
+ def webserver_port
132
+ get_config('grafana-reporter:webservice-port') || 8815
133
+ end
134
+
135
+ # The configuration made with the setting 'default-document-attributes' will
136
+ # be passed 1:1 to the asciidoctor report service. It can be used to preconfigure
137
+ # whatever is essential for the needed report renderings.
138
+ # @return [Hash] configured document attributes
139
+ def default_document_attributes
140
+ get_config('default-document-attributes') || {}
141
+ end
142
+
143
+ # This function shall be called, before the configuration object is used in the
144
+ # {Application::Application#run}. It ensures, that everything is setup properly
145
+ # and all necessary folders exist. Appropriate errors are raised in case of errors.
146
+ # @param explicit [Boolean] true, if validation shall expect explicit (wizard) configuration file
147
+ # @return [void]
148
+ def validate(explicit = false)
149
+ check_deprecation
150
+ validate_schema(schema(explicit), @config)
151
+
152
+ # check if set folders exist
153
+ raise FolderDoesNotExistError.new(reports_folder, 'reports-folder') unless File.directory?(reports_folder)
154
+ raise FolderDoesNotExistError.new(templates_folder, 'templates-folder') unless File.directory?(templates_folder)
155
+ raise FolderDoesNotExistError.new(images_folder, 'images-folder') unless File.directory?(images_folder)
156
+ end
157
+
158
+ # Can be used to configure or overwrite single parameters.
159
+ #
160
+ # @param path [String] path of the paramter to set, e.g. +grafana-reporter:webservice-port+
161
+ # @param value [Object] value to set
162
+ def set_param(path, value)
163
+ return if path.nil?
164
+
165
+ levels = path.split(':')
166
+ last_level = levels.pop
167
+
168
+ cur_pos = @config
169
+ levels.each do |subpath|
170
+ cur_pos[subpath] = {} unless cur_pos[subpath]
171
+ cur_pos = cur_pos[subpath]
172
+ end
173
+
174
+ cur_pos[last_level] = value
175
+ update_configuration
176
+ end
177
+
178
+ # Merge the given configuration object settings with the current config, i.e. overwrite and add all
179
+ # settings from the given config, but keep the not specified configs from the current object.
180
+ #
181
+ # param other_config [Configuration] other configuration object
182
+ def merge!(other_config)
183
+ config.merge!(other_config.config) { |_key, v1, v2| v1.is_a?(Hash) && v2.is_a?(Hash) ? v1.merge(v2) : v2 }
184
+ update_configuration
185
+ end
186
+
187
+ private
188
+
189
+ def check_deprecation
190
+ return if report_class
191
+
192
+ logger.warn('DEPRECATION WARNING: Your configuration explicitly needs to specify the '\
193
+ '\'grafana-reporter:report-class\' value. Currently this defaults to '\
194
+ '\'GrafanaReporter::Asciidoctor::Report\'. You can get rid of this warning, if you '\
195
+ 'explicitly set this configuration in your configuration file. Setting this default will be '\
196
+ 'removed in a future version.')
197
+ set_param('grafana-reporter:report-class', 'GrafanaReporter::Asciidoctor::Report')
198
+ end
199
+
200
+ def update_configuration
201
+ debug_level = get_config('grafana-reporter:debug-level')
202
+ rep_class = get_config('grafana-reporter:report-class')
203
+
204
+ @logger.level = Object.const_get("::Logger::Severity::#{debug_level}") if debug_level =~ /DEBUG|INFO|WARN|
205
+ ERROR|FATAL|UNKNOWN/x
206
+ self.report_class = Object.const_get(rep_class) if rep_class
207
+ ::Grafana::WebRequest.ssl_cert = get_config('grafana-reporter:ssl-cert')
208
+
209
+ # register callbacks
210
+ callbacks = get_config('grafana-reporter:callbacks')
211
+ return unless callbacks
212
+
213
+ callbacks.each do |url, event|
214
+ AbstractReport.add_event_listener(event.to_sym, ReportWebhook.new(url))
215
+ end
216
+ end
217
+
218
+ def get_config(path)
219
+ return if path.nil?
220
+
221
+ cur_pos = @config
222
+ path.split(':').each do |subpath|
223
+ cur_pos = cur_pos[subpath] if cur_pos
224
+ end
225
+ cur_pos
226
+ end
227
+
228
+ def validate_schema(schema, subject)
229
+ return nil if subject.nil?
230
+
231
+ schema.each do |key, config|
232
+ type, min_occurence, next_level = config
233
+
234
+ validate_schema(next_level, subject[key]) if next_level
235
+
236
+ if key.nil?
237
+ # apply to all on this level
238
+ raise ConfigurationError, "Unhandled configuration data type '#{subject.class}'." unless subject.is_a?(Hash)
239
+
240
+ if subject.length < min_occurence
241
+ raise ConfigurationDoesNotMatchSchemaError.new(key, 'occur', min_occurence, subject.length)
242
+ end
243
+
244
+ subject.each do |k, _v|
245
+ sub_scheme = {}
246
+ sub_scheme[k] = schema[nil]
247
+ validate_schema(sub_scheme, subject)
248
+ end
249
+
250
+ # apply to single item
251
+ elsif subject.is_a?(Hash)
252
+ if !subject.key?(key) && min_occurence.positive?
253
+ raise ConfigurationDoesNotMatchSchemaError.new(key, 'occur', min_occurence, 0)
254
+ end
255
+ if !subject[key].is_a?(type) && subject.key?(key)
256
+ raise ConfigurationDoesNotMatchSchemaError.new(key, 'be a', type, subject[key].class)
257
+ end
258
+
259
+ else
260
+ raise ConfigurationError, "Unhandled configuration data type '#{subject.class}'."
261
+ end
262
+ end
263
+
264
+ # validate also if subject has further configurations, which are not known by the reporter
265
+ subject.each do |item, _subitems|
266
+ schema_config = schema[item] || schema[nil]
267
+ if schema_config.nil?
268
+ logger.warn("Item '#{item}' in configuration is unknown to the reporter and will be ignored")
269
+ end
270
+ end
271
+ end
272
+
273
+ def schema(explicit)
274
+ {
275
+ 'grafana' =>
276
+ [
277
+ Hash, 1,
278
+ {
279
+ nil =>
280
+ [
281
+ Hash, 1,
282
+ {
283
+ 'host' => [String, 1],
284
+ 'api_key' => [String, 0]
285
+ }
286
+ ]
287
+ }
288
+ ],
289
+ 'default-document-attributes' => [Hash, explicit ? 1 : 0],
290
+ 'to_file' => [String, 0],
291
+ 'grafana-reporter' =>
292
+ [
293
+ Hash, 1,
294
+ {
295
+ 'debug-level' => [String, 0],
296
+ 'run-mode' => [String, 0],
297
+ 'test-instance' => [String, 0],
298
+ 'templates-folder' => [String, explicit ? 1 : 0],
299
+ 'report-class' => [String, 1],
300
+ 'reports-folder' => [String, explicit ? 1 : 0],
301
+ 'report-retention' => [Integer, explicit ? 1 : 0],
302
+ 'ssl-cert' => [String, 0],
303
+ 'webservice-port' => [Integer, explicit ? 1 : 0],
304
+ 'callbacks' => [Hash, 0, { nil => [String, 1] }]
305
+ }
306
+ ]
307
+ }
308
+ end
309
+ end
310
+ end