ruby-grafana-reporter 0.2.2 → 0.4.3

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +126 -88
  3. data/bin/ruby-grafana-reporter +2 -2
  4. data/lib/VERSION.rb +3 -2
  5. data/lib/grafana/abstract_datasource.rb +146 -0
  6. data/lib/grafana/dashboard.rb +1 -3
  7. data/lib/grafana/errors.rb +18 -3
  8. data/lib/grafana/grafana.rb +64 -66
  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 +30 -0
  12. data/lib/grafana/graphite_datasource.rb +72 -0
  13. data/lib/grafana/image_rendering_datasource.rb +44 -0
  14. data/lib/grafana/influxdb_datasource.rb +70 -0
  15. data/lib/grafana/panel.rb +9 -3
  16. data/lib/grafana/prometheus_datasource.rb +67 -0
  17. data/lib/grafana/sql_datasource.rb +78 -0
  18. data/lib/grafana/unsupported_datasource.rb +7 -0
  19. data/lib/grafana/variable.rb +1 -1
  20. data/lib/grafana/webrequest.rb +71 -0
  21. data/lib/grafana_reporter/abstract_query.rb +460 -0
  22. data/lib/grafana_reporter/abstract_report.rb +139 -18
  23. data/lib/grafana_reporter/alerts_table_query.rb +39 -0
  24. data/lib/grafana_reporter/annotations_table_query.rb +38 -0
  25. data/lib/grafana_reporter/application/application.rb +34 -286
  26. data/lib/grafana_reporter/application/webservice.rb +50 -15
  27. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +91 -0
  28. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +90 -0
  29. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +74 -0
  30. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +76 -0
  31. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +70 -0
  32. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +95 -0
  33. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +90 -0
  34. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +49 -0
  35. data/lib/grafana_reporter/asciidoctor/report.rb +32 -76
  36. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  37. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  38. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +90 -0
  39. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +86 -0
  40. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  41. data/lib/grafana_reporter/configuration.rb +59 -52
  42. data/lib/grafana_reporter/console_configuration_wizard.rb +311 -0
  43. data/lib/grafana_reporter/demo_report_wizard.rb +105 -0
  44. data/lib/grafana_reporter/erb/report.rb +30 -0
  45. data/lib/grafana_reporter/erb/report_jail.rb +21 -0
  46. data/lib/grafana_reporter/errors.rb +55 -0
  47. data/lib/grafana_reporter/help.rb +443 -0
  48. data/lib/grafana_reporter/logger/{two_way_logger.rb → two_way_delegate_logger.rb} +1 -1
  49. data/lib/grafana_reporter/panel_image_query.rb +25 -0
  50. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  51. data/lib/grafana_reporter/query_value_query.rb +61 -0
  52. data/lib/grafana_reporter/report_webhook.rb +35 -0
  53. data/lib/ruby_grafana_extension.rb +8 -0
  54. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +1 -0
  55. metadata +47 -39
  56. data/lib/grafana/abstract_panel_query.rb +0 -22
  57. data/lib/grafana/abstract_query.rb +0 -132
  58. data/lib/grafana/abstract_sql_query.rb +0 -51
  59. data/lib/grafana/panel_image_query.rb +0 -52
  60. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -104
  61. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -99
  62. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -40
  63. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -92
  64. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -91
  65. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -69
  66. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -68
  67. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -61
  68. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -78
  69. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -73
  70. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -20
  71. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -43
  72. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -30
  73. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -70
  74. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -66
  75. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -86
  76. data/lib/grafana_reporter/asciidoctor/help.rb +0 -435
  77. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -34
  78. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -26
  79. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -44
  80. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -38
  81. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -301
  82. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -42
  83. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -44
@@ -0,0 +1,311 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ # This class provides a console configuration wizard, to reduce the manual efforts that have
5
+ # to be spent for that action and to reduce mistakes as good as possible.
6
+ class ConsoleConfigurationWizard
7
+ # Provides a command line configuration wizard for setting up the necessary configuration
8
+ # file.
9
+ # TODO: refactor class
10
+ def start_wizard(config_file, console_config)
11
+ action = overwrite_or_use_config_file(config_file)
12
+ return if action == 'abort'
13
+
14
+ config = create_config_wizard(config_file, console_config) if action == 'overwrite'
15
+ config ||= Configuration.new
16
+
17
+ begin
18
+ config.config = YAML.load_file(config_file)
19
+ rescue StandardError => e
20
+ raise ConfigurationError, "Could not read config file '#{config_file}' (Error: #{e.message})\n"\
21
+ "Source:\n#{File.read(config_file)}"
22
+ end
23
+
24
+ begin
25
+ config.validate(true)
26
+ puts 'Configuration file validated successfully.'
27
+ rescue ConfigurationError => e
28
+ raise e
29
+ end
30
+
31
+ demo_report = create_demo_report(config)
32
+
33
+ demo_report ||= '<<your_report_name>>'
34
+ config_param = config_file == Configuration::DEFAULT_CONFIG_FILE_NAME ? '' : " -c #{config_file}"
35
+ program_call = "#{Gem.ruby} #{$PROGRAM_NAME}"
36
+ program_call = ENV['OCRA_EXECUTABLE'].gsub("#{Dir.pwd}/".gsub('/', '\\'), '') if ENV['OCRA_EXECUTABLE']
37
+
38
+ puts
39
+ puts 'Now everything is setup properly. Create your reports as required in the templates '\
40
+ 'folder and run the reporter either standalone with e.g. the following command:'
41
+ puts
42
+ puts " #{program_call}#{config_param} -t #{demo_report} -o demo_report_with_help.pdf"
43
+ puts
44
+ puts 'or run it as a service using the following command:'
45
+ puts
46
+ puts " #{program_call}#{config_param}"
47
+ puts
48
+ puts "Open 'http://localhost:#{config.webserver_port}/render?var-template=#{demo_report}' in a webbrowser to"\
49
+ ' test your configuration.'
50
+ end
51
+
52
+ private
53
+
54
+ def create_config_wizard(config_file, console_config)
55
+ config = Configuration.new
56
+
57
+ puts 'This wizard will guide you through an initial configuration for'\
58
+ ' the ruby-grafana-reporter. The configuration file will be created'\
59
+ ' in the current folder. Please make sure to specify necessary paths'\
60
+ ' either with a relative or an absolute path properly.'
61
+ puts
62
+ puts "Wizard is creating configuration file '#{config_file}'."
63
+ puts
64
+ port = ui_config_port
65
+ grafana = ui_config_grafana(console_config)
66
+ templates = ui_config_templates_folder
67
+ reports = ui_config_reports_folder
68
+ images = ui_config_images_folder(templates)
69
+ retention = ui_config_retention
70
+
71
+ config_yaml = %(# This configuration has been built with the configuration wizard.
72
+
73
+ #{grafana}
74
+
75
+ grafana-reporter:
76
+ report-class: GrafanaReporter::Asciidoctor::Report
77
+ templates-folder: #{templates}
78
+ reports-folder: #{reports}
79
+ report-retention: #{retention}
80
+ webservice-port: #{port}
81
+ # you may want to configure the following webhook callbacks to get informed on certain events
82
+ # callbacks:
83
+ # all:
84
+ # - <<your_callback_url>>
85
+ # - ...
86
+ # on_before_create:
87
+ # - <<your_callback_url>>
88
+ # - ...
89
+ # on_after_cancel:
90
+ # - <<your_callback_url>>
91
+ # - ...
92
+ # on_after_finish:
93
+ # - <<your_callback_url>>
94
+ # - ...
95
+
96
+ default-document-attributes:
97
+ imagesdir: #{images}
98
+ # feel free to add here additional asciidoctor document attributes which are applied to all your templates
99
+ )
100
+
101
+ begin
102
+ File.write(config_file, config_yaml, mode: 'w')
103
+ puts 'Configuration file successfully created.'
104
+ rescue StandardError => e
105
+ raise e
106
+ end
107
+
108
+ config
109
+ end
110
+
111
+ def create_demo_report(config)
112
+ unless Dir.exist?(config.templates_folder)
113
+ puts "Skip creation of DEMO template, as folder '#{config.templates_folder}' does not exist."
114
+ return nil
115
+ end
116
+
117
+ create = user_input('Shall I create a demo report for your new configuration file? Please note '\
118
+ 'that this report might contain confidential information, depending on the '\
119
+ 'confidentiality of the information stored in your dashboard.', 'yN')
120
+ return nil unless create =~ /^(?:y|Y)$/
121
+
122
+ demo_report = 'demo_report'
123
+ demo_report_file = "#{config.templates_folder}#{demo_report}.adoc"
124
+
125
+ # ask to overwrite file
126
+ if File.exist?(demo_report_file)
127
+ input = user_input("Demo template '#{demo_report_file}' does already exist. Do you want to "\
128
+ 'overwrite it?', 'yN')
129
+
130
+ case input
131
+ when /^(?:y|Y)$/
132
+ puts 'Overwriting existing DEMO template.'
133
+
134
+ else
135
+ puts 'Skip creation of DEMO template.'
136
+ return demo_report
137
+ end
138
+ end
139
+
140
+ grafana = ::Grafana::Grafana.new(config.grafana_host, config.grafana_api_key)
141
+ demo_report_content = DemoReportWizard.new(config.report_class.demo_report_classes).build(grafana)
142
+
143
+ begin
144
+ File.write(demo_report_file, demo_report_content, mode: 'w')
145
+ puts "DEMO template '#{demo_report_file}' successfully created."
146
+ rescue StandardError => e
147
+ puts e.message
148
+ return nil
149
+ end
150
+
151
+ demo_report
152
+ end
153
+
154
+ def ui_config_grafana(config)
155
+ valid = false
156
+ url = nil
157
+ api_key = nil
158
+ until valid
159
+ url ||= user_input('Specify grafana host', 'http://localhost:3000')
160
+ print "Testing connection to '#{url}' #{api_key ? '_with_' : '_without_'} API key..."
161
+ begin
162
+ res = Grafana::Grafana.new(url,
163
+ api_key,
164
+ logger: config.logger).test_connection
165
+ rescue StandardError => e
166
+ puts
167
+ puts e.message
168
+ end
169
+ puts 'done.'
170
+
171
+ case res
172
+ when 'Admin'
173
+ tmp = user_input('Access to grafana is permitted as Admin, which is a potential security risk.'\
174
+ ' Do you want to use another [a]pi key, [r]e-enter url key or [i]gnore?', 'aRi')
175
+
176
+ case tmp
177
+ when /(?:i|I)$/
178
+ valid = true
179
+
180
+ when /(?:a|A)$/
181
+ print 'Enter API key: '
182
+ api_key = gets.strip
183
+
184
+ else
185
+ url = nil
186
+ api_key = nil
187
+
188
+ end
189
+
190
+ when 'NON-Admin'
191
+ print 'Access to grafana is permitted as NON-Admin.'
192
+ valid = true
193
+
194
+ else
195
+ tmp = user_input("Grafana could not be accessed at '#{url}'. Do you want to use an [a]pi key,"\
196
+ ' [r]e-enter url, or [i]gnore and proceed?', 'aRi')
197
+
198
+ case tmp
199
+ when /(?:i|I)$/
200
+ valid = true
201
+
202
+ when /(?:a|A)$/
203
+ print 'Enter API key: '
204
+ api_key = gets.strip
205
+
206
+ else
207
+ url = nil
208
+ api_key = nil
209
+
210
+ end
211
+
212
+ end
213
+ end
214
+ %(grafana:
215
+ default:
216
+ host: #{url}#{api_key ? "\n api_key: #{api_key}" : ''}
217
+ )
218
+ end
219
+
220
+ def ui_config_port
221
+ input = nil
222
+ until input
223
+ input = user_input('Specify port on which reporter shall run', '8815')
224
+ input = nil unless input =~ /[0-9]+/
225
+ end
226
+ input
227
+ end
228
+
229
+ def ui_config_templates_folder
230
+ input = nil
231
+ until input
232
+ input = user_input('Specify path where templates shall be stored', './templates')
233
+ input = nil unless validate_config_folder(input)
234
+ end
235
+ input
236
+ end
237
+
238
+ def ui_config_reports_folder
239
+ input = nil
240
+ until input
241
+ input = user_input('Specify path where created reports shall be stored', './reports')
242
+ input = nil unless validate_config_folder(input)
243
+ end
244
+ input
245
+ end
246
+
247
+ def ui_config_images_folder(parent)
248
+ input = nil
249
+ until input
250
+ input = user_input('Specify path where rendered images shall be stored (relative to templates folder)',
251
+ './images')
252
+ input = nil unless validate_config_folder(File.join(parent, input))
253
+ end
254
+ input
255
+ end
256
+
257
+ def ui_config_retention
258
+ input = nil
259
+ until input
260
+ input = user_input('Specify report retention duration in hours', '24')
261
+ input = nil unless input =~ /[0-9]+/
262
+ end
263
+ input
264
+ end
265
+
266
+ def user_input(text, default)
267
+ print "#{text} [#{default}]: "
268
+ input = gets.gsub(/\n$/, '')
269
+ input = default if input.empty?
270
+ input
271
+ end
272
+
273
+ def validate_config_folder(folder)
274
+ return true if Dir.exist?(folder)
275
+
276
+ print "Directory '#{folder} does not exist: [c]reate, [r]e-enter path or [i]gnore? [cRi]: "
277
+ case gets
278
+ when /^(?:c|C)$/
279
+ begin
280
+ Dir.mkdir(folder)
281
+ puts "Directory '#{folder}' successfully created."
282
+ return true
283
+ rescue StandardError => e
284
+ puts "WARN: Directory '#{folder}' could not be created. Please create it manually."
285
+ puts e.message
286
+ end
287
+
288
+ when /^(?:i|I)$/
289
+ puts "WARN: Directory '#{folder}' does not exist. Please create manually."
290
+ return true
291
+ end
292
+
293
+ false
294
+ end
295
+
296
+ def overwrite_or_use_config_file(config_file)
297
+ return 'overwrite' unless File.exist?(config_file)
298
+
299
+ input = nil
300
+ until input
301
+ input = user_input("Configuration file '#{config_file}' already exists. Do you want to [o]verwrite it, "\
302
+ 'use it to for [d]emo report creation only, or [a]bort?', 'odA')
303
+ end
304
+
305
+ return 'demo_report' if input =~ /^(?:d|D)$/
306
+ return 'abort' if input =~ /^(?:A|a|odA)$/
307
+
308
+ 'overwrite'
309
+ end
310
+ end
311
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ # This class is used to build a demo report based on a real grafana instance. Therefore
5
+ # it checks available grafana dashboards and panels and returns a final template file as
6
+ # string, which can then be used as a template.
7
+ class DemoReportWizard
8
+ # @param query_classes [Array] class objects, for which a demo report shall be created
9
+ def initialize(query_classes)
10
+ @query_classes = query_classes
11
+ end
12
+
13
+ # Invokes the build process for the given +grafana+ object. Progress is printed to
14
+ # STDOUT.
15
+ # @param grafana [Grafana] grafana instance, for which the demo report shall be built
16
+ # @return [String] demo template as string
17
+ def build(grafana)
18
+ results = {}
19
+
20
+ grafana.dashboard_ids.sample(15).each do |dashboard_id|
21
+ print "Evaluating dashboard '#{dashboard_id}' for building a demo report..."
22
+ dashboard = grafana.dashboard(dashboard_id)
23
+
24
+ results = evaluate_dashboard(dashboard, @query_classes - results.keys).merge(results)
25
+
26
+ puts "done - #{(@query_classes - results.keys).length} examples to go"
27
+ break if (@query_classes - results.keys).empty?
28
+ end
29
+
30
+ if grafana.dashboard_ids.length > 15 && !(@query_classes - results.keys).empty?
31
+ puts 'Aborting evaluating further dashboards after 15 samples.'
32
+ end
33
+
34
+ unless (@query_classes - results.keys).empty?
35
+ puts "For #{(@query_classes - results.keys).length} reporter functionalities no appropriate "\
36
+ 'examples could be found in the configured grafana instance.'
37
+ end
38
+
39
+ format_results(default_result(@query_classes - results.keys).merge(results))
40
+ end
41
+
42
+ private
43
+
44
+ def default_result(query_classes)
45
+ results = {}
46
+
47
+ query_classes.each do |query_class|
48
+ results[query_class] = "No example found for #{query_class.name} in the dashboards."
49
+ end
50
+
51
+ results
52
+ end
53
+
54
+ def evaluate_dashboard(dashboard, query_classes)
55
+ results = {}
56
+
57
+ dashboard.panels.shuffle.each do |panel|
58
+ begin
59
+ next if panel.datasource.is_a?(Grafana::UnsupportedDatasource)
60
+ rescue Grafana::DatasourceDoesNotExistError
61
+ next
62
+ end
63
+
64
+ query_classes.each do |query_class|
65
+ unless query_class.public_instance_methods.include?(:build_demo_entry)
66
+ results[query_class] = "Method 'build_demo_entry' not implemented for #{query_class.name}"
67
+ next
68
+ end
69
+
70
+ begin
71
+ result = query_class.new.build_demo_entry(panel)
72
+ results[query_class] = result if result
73
+ rescue Grafana::DatasourceDoesNotExistError
74
+ # properly catch DatasourceDoesNotExist errors here, as they don't lead to a real issue
75
+ # during demo report creation
76
+ # This may e.g. happen if a panel asks e.g. for datasource '-- Dashboard --' which is
77
+ # currently not allowed
78
+ rescue StandardError => e
79
+ puts "#{e.message}\n#{e.backtrace.join("\n")}"
80
+ end
81
+ end
82
+ end
83
+
84
+ results
85
+ end
86
+
87
+ # TODO: move this method to Asciidoctor::Report
88
+ def format_results(raw_results)
89
+ results = ['= Demo report',
90
+ "Created by `+ruby-grafana-reporter+` version #{GRAFANA_REPORTER_VERSION.join('.')}",
91
+ '== Examples']
92
+
93
+ raw_results.each do |k, v|
94
+ results += if v =~ /^[A-Z]/
95
+ ["=== #{k.to_s.gsub(/.*::/, '')}", v.to_s]
96
+ else
97
+ ["=== #{k.to_s.gsub(/.*::/, '')}", 'Sample call:', " #{v.gsub(/\n/, "\n ")}",
98
+ 'Result:', v.to_s]
99
+ end
100
+ end
101
+
102
+ results.join("\n\n")
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module GrafanaReporter
6
+ module ERB
7
+ # Implementation of a specific {AbstractReport}. It is used to
8
+ # build reports specifically for erb templates.
9
+ class Report < ::GrafanaReporter::AbstractReport
10
+ # Starts to create an asciidoctor report. It utilizes all extensions in the {GrafanaReporter::Asciidoctor}
11
+ # namespace to realize the conversion.
12
+ # @see AbstractReport#build
13
+ def build(template, destination_file_or_path, custom_attributes)
14
+ attrs = @config.default_document_attributes.merge(@custom_attributes)
15
+ logger.debug("Document attributes: #{attrs}")
16
+
17
+ # TODO: if path is true, a default filename has to be generated. check if this should be a general function instead
18
+ File.write(path, ::ERB.new(File.read(template)).result(ReportJail.new(self, attrs).bind))
19
+
20
+ # TODO: check if closing output file is correct here, or maybe can be moved to AbstractReport.done!
21
+ @destination_file_or_path.close if @destination_file_or_path.is_a?(File)
22
+ end
23
+
24
+ # @see AbstractReport#demo_report_classes
25
+ def self.demo_report_classes
26
+ []
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GrafanaReporter
4
+ module ERB
5
+ # An instance of this class is used as binding for the ERB execution, i.e.
6
+ # this class contains everything known within the ERB template
7
+ class ReportJail
8
+ attr_reader :report, :attributes
9
+
10
+ def initialize(report, attributes)
11
+ @report = report
12
+ @attributes = attributes
13
+ end
14
+
15
+ # @return binding to this object
16
+ def bind
17
+ binding
18
+ end
19
+ end
20
+ end
21
+ end