ruby-grafana-reporter 0.3.0 → 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 +81 -71
  3. data/bin/ruby-grafana-reporter +5 -5
  4. data/lib/VERSION.rb +3 -2
  5. data/lib/grafana/abstract_datasource.rb +116 -0
  6. data/lib/grafana/dashboard.rb +1 -3
  7. data/lib/grafana/errors.rb +15 -0
  8. data/lib/grafana/grafana.rb +53 -56
  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 +9 -3
  15. data/lib/grafana/prometheus_datasource.rb +39 -0
  16. data/lib/grafana/sql_datasource.rb +65 -0
  17. data/lib/grafana/variable.rb +1 -0
  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 +54 -3
  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 +12 -8
  24. data/lib/grafana_reporter/application/webservice.rb +18 -6
  25. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +90 -0
  26. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +89 -0
  27. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +76 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +77 -0
  29. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +72 -0
  30. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +98 -0
  31. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
  32. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +23 -0
  33. data/lib/grafana_reporter/asciidoctor/report.rb +24 -31
  34. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  35. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  36. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +92 -0
  37. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +88 -0
  38. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  39. data/lib/grafana_reporter/configuration.rb +12 -6
  40. data/lib/grafana_reporter/console_configuration_wizard.rb +115 -65
  41. data/lib/grafana_reporter/demo_report_wizard.rb +87 -0
  42. data/lib/grafana_reporter/errors.rb +33 -0
  43. data/lib/grafana_reporter/help.rb +447 -0
  44. data/lib/grafana_reporter/logger/two_way_logger.rb +1 -1
  45. data/lib/grafana_reporter/panel_image_query.rb +29 -0
  46. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  47. data/lib/grafana_reporter/query_value_query.rb +79 -0
  48. data/lib/grafana_reporter/report_webhook.rb +35 -0
  49. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +0 -3
  50. metadata +37 -35
  51. data/lib/grafana/abstract_panel_query.rb +0 -22
  52. data/lib/grafana/abstract_query.rb +0 -132
  53. data/lib/grafana/abstract_sql_query.rb +0 -51
  54. data/lib/grafana/panel_image_query.rb +0 -52
  55. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -101
  56. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
  57. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -40
  58. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -92
  59. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -91
  60. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -69
  61. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -68
  62. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -61
  63. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -78
  64. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -73
  65. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -20
  66. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -43
  67. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -30
  68. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -70
  69. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -66
  70. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -88
  71. data/lib/grafana_reporter/asciidoctor/help.rb +0 -435
  72. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -36
  73. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -28
  74. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -44
  75. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -40
  76. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -312
  77. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -42
  78. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -44
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the datasource interface to grafana alerts.
5
+ class GrafanaAlertsDatasource < AbstractDatasource
6
+ # +:raw_query+ needs to contain a Hash with the following structure:
7
+ #
8
+ # {
9
+ # dashboardId: Dashboard ID as String or nil
10
+ # panelId: Panel ID as String or nil
11
+ # columns:
12
+ # limit:
13
+ # query:
14
+ # state:
15
+ # folderId:
16
+ # dashboardQuery:
17
+ # dashboardTag:
18
+ # }
19
+ # @see AbstractDatasource#request
20
+ def request(query_description)
21
+ webrequest = query_description[:prepared_request]
22
+ webrequest.relative_url = "/api/alerts#{url_parameters(query_description)}"
23
+
24
+ result = webrequest.execute(query_description[:timeout])
25
+
26
+ json = JSON.parse(result.body)
27
+
28
+ content = []
29
+ begin
30
+ json.each { |item| content << item.fetch_values(*query_description[:raw_query]['columns'].split(',')) }
31
+ rescue KeyError => e
32
+ raise MalformedAttributeContentError.new(e.message, 'columns', query_description[:raw_query]['columns'])
33
+ end
34
+
35
+ result = {}
36
+ result[:header] = [query_description[:raw_query]['columns'].split(',')]
37
+ result[:content] = content
38
+
39
+ result
40
+ end
41
+
42
+ private
43
+
44
+ def url_parameters(query_desc)
45
+ url_vars = {}
46
+ url_vars.merge!(query_desc[:raw_query].select do |k, _v|
47
+ k =~ /^(?:limit|dashboardId|panelId|query|state|folderId|dashboardQuery|dashboardTag)/
48
+ end)
49
+ url_vars['from'] = query_desc[:from] if query_desc[:from]
50
+ url_vars['to'] = query_desc[:to] if query_desc[:to]
51
+ url_params = URI.encode_www_form(url_vars.map { |k, v| [k, v.to_s] })
52
+ return '' if url_params.empty?
53
+
54
+ "?#{url_params}"
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the datasource interface to grafana annotations.
5
+ class GrafanaAnnotationsDatasource < AbstractDatasource
6
+ # +:raw_query+ needs to contain a Hash with the following structure:
7
+ #
8
+ # {
9
+ # dashboardId: Dashboard ID as String or nil
10
+ # panelId: Panel ID as String or nil
11
+ # columns:
12
+ # limit:
13
+ # alertId:
14
+ # userId:
15
+ # type:
16
+ # tags:
17
+ # }
18
+ # @see AbstractDatasource#request
19
+ def request(query_description)
20
+ webrequest = query_description[:prepared_request]
21
+ webrequest.relative_url = "/api/annotations#{url_parameters(query_description)}"
22
+
23
+ result = webrequest.execute(query_description[:timeout])
24
+
25
+ json = JSON.parse(result.body)
26
+
27
+ content = []
28
+ begin
29
+ json.each { |item| content << item.fetch_values(*query_description[:raw_query]['columns'].split(',')) }
30
+ rescue KeyError => e
31
+ raise MalformedAttributeContentError.new(e.message, 'columns', query_description[:raw_query]['columns'])
32
+ end
33
+
34
+ result = {}
35
+ result[:header] = [query_description[:raw_query]['columns'].split(',')]
36
+ result[:content] = content
37
+
38
+ result
39
+ end
40
+
41
+ private
42
+
43
+ def url_parameters(query_desc)
44
+ url_vars = {}
45
+ url_vars.merge!(query_desc[:raw_query].select do |k, _v|
46
+ k =~ /^(?:limit|alertId|dashboardId|panelId|userId|type|tags)/
47
+ end)
48
+ url_vars['from'] = query_desc[:from] if query_desc[:from]
49
+ url_vars['to'] = query_desc[:to] if query_desc[:to]
50
+ url_params = URI.encode_www_form(url_vars.map { |k, v| [k, v.to_s] })
51
+ return '' if url_params.empty?
52
+
53
+ "?#{url_params}"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the datasource interface to grafana model properties.
5
+ class GrafanaPropertyDatasource < AbstractDatasource
6
+ # +:raw_query+ needs to contain a Hash with the following structure:
7
+ #
8
+ # {
9
+ # property_name: Name of the queried property as String
10
+ # panel: {Panel} object to query
11
+ # }
12
+ # @see AbstractDatasource#request
13
+ def request(query_description)
14
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
15
+
16
+ panel = query_description[:raw_query][:panel]
17
+ property_name = query_description[:raw_query][:property_name]
18
+
19
+ {
20
+ header: [query_description[:raw_query][:property_name]],
21
+ content: [replace_variables(panel.field(property_name), query_description[:variables])]
22
+ }
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the interface to graphite datasources.
5
+ class GraphiteDatasource < AbstractDatasource
6
+ # +:raw_query+ needs to contain a Graphite query as String
7
+ # @see AbstractDatasource#request
8
+ def request(query_description)
9
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
10
+
11
+ request = {
12
+ body: URI.encode_www_form('from': DateTime.strptime(query_description[:from], '%Q').strftime('%H:%M_%Y%m%d'),
13
+ 'until': DateTime.strptime(query_description[:to], '%Q').strftime('%H:%M_%Y%m%d'),
14
+ 'format': 'json',
15
+ 'target': replace_variables(query_description[:raw_query], query_description[:variables])),
16
+ content_type: 'application/x-www-form-urlencoded',
17
+ request: Net::HTTP::Post
18
+ }
19
+
20
+ webrequest = query_description[:prepared_request]
21
+ webrequest.relative_url = "/api/datasources/proxy/#{id}/render"
22
+ webrequest.options.merge!(request)
23
+
24
+ result = webrequest.execute(query_description[:timeout])
25
+ preformat_response(result.body)
26
+ end
27
+
28
+ # @see AbstractDatasource#raw_query_from_panel_model
29
+ def raw_query_from_panel_model(panel_query_target)
30
+ panel_query_target['target']
31
+ end
32
+
33
+ private
34
+
35
+ # @see AbstractDatasource#preformat_response
36
+ def preformat_response(response_body)
37
+ # TODO: support multiple metrics as return types
38
+ {
39
+ header: %w[value time],
40
+ content: JSON.parse(response_body).first['datapoints']
41
+ }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the interface to image rendering datasources.
5
+ class ImageRenderingDatasource < AbstractDatasource
6
+ # +:raw_query+ needs to contain a Hash with the following structure:
7
+ #
8
+ # {
9
+ # panel: {Panel} which shall be rendered
10
+ # }
11
+ # @see AbstractDatasource#request
12
+ def request(query_description)
13
+ webrequest = query_description[:prepared_request]
14
+ webrequest.relative_url = query_description[:raw_query][:panel].render_url + url_params(query_description)
15
+ webrequest.options.merge!({ accept: 'image/png' })
16
+
17
+ result = webrequest.execute
18
+
19
+ { header: ['image'], content: [result.body] }
20
+ end
21
+
22
+ private
23
+
24
+ def url_params(query_desc)
25
+ url_vars = query_desc[:variables].select { |k, _v| k =~ /^(?:timeout|height|width|theme|fullscreen|var-.+)$/ }
26
+ url_vars = default_vars.merge(url_vars)
27
+ url_vars['from'] = Variable.new(query_desc[:from])
28
+ url_vars['to'] = Variable.new(query_desc[:to])
29
+ result = URI.encode_www_form(url_vars.map { |k, v| [k, v.raw_value.to_s] })
30
+
31
+ return '' if result.empty?
32
+
33
+ "&#{result}"
34
+ end
35
+
36
+ def default_vars
37
+ {
38
+ 'fullscreen' => Variable.new(true),
39
+ 'theme' => Variable.new('light'),
40
+ 'timeout' => Variable.new(60)
41
+ }
42
+ end
43
+ end
44
+ end
data/lib/grafana/panel.rb CHANGED
@@ -5,6 +5,7 @@ module Grafana
5
5
  class Panel
6
6
  # @return [Dashboard] parent {Dashboard} object
7
7
  attr_reader :dashboard
8
+ attr_reader :model
8
9
 
9
10
  # @param model [Hash] converted JSON Hash of the panel
10
11
  # @param dashboard [Dashboard] parent {Dashboard} object
@@ -25,12 +26,17 @@ module Grafana
25
26
  @model['id']
26
27
  end
27
28
 
28
- # @return [String] SQL query string for the requested query letter
29
+ # @return [Datasource] datasource object specified for the current panel
30
+ def datasource
31
+ dashboard.grafana.datasource_by_name(@model['datasource'])
32
+ end
33
+
34
+ # @return [String] query string for the requested query letter
29
35
  def query(query_letter)
30
36
  query_item = @model['targets'].select { |item| item['refId'].to_s == query_letter.to_s }.first
31
- raise QueryLetterDoesNotExistError.new(query_letter, self) if query_item.nil?
37
+ raise QueryLetterDoesNotExistError.new(query_letter, self) unless query_item
32
38
 
33
- query_item['rawSql']
39
+ datasource.raw_query_from_panel_model(query_item)
34
40
  end
35
41
 
36
42
  # @return [String] relative rendering URL for the panel, to create an image out of it
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the interface to Prometheus datasources.
5
+ class PrometheusDatasource < AbstractDatasource
6
+ # +:raw_query+ needs to contain a Prometheus query as String
7
+ # @see AbstractDatasource#request
8
+ def request(query_description)
9
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
10
+
11
+ url = "/api/datasources/proxy/#{id}/api/v1/query_range?"\
12
+ "start=#{query_description[:from]}&end=#{query_description[:to]}"\
13
+ "&query=#{replace_variables(query_description[:raw_query], query_description[:variables])}"
14
+
15
+ webrequest = query_description[:prepared_request]
16
+ webrequest.relative_url = url
17
+ webrequest.options.merge!({ request: Net::HTTP::Get })
18
+
19
+ result = webrequest.execute(query_description[:timeout])
20
+ preformat_response(result.body)
21
+ end
22
+
23
+ # @see AbstractDatasource#raw_query_from_panel_model
24
+ def raw_query_from_panel_model(panel_query_target)
25
+ panel_query_target['expr']
26
+ end
27
+
28
+ private
29
+
30
+ # @see AbstractDatasource#preformat_response
31
+ def preformat_response(response_body)
32
+ # TODO: support multiple metrics as return types
33
+ {
34
+ header: %w[time value],
35
+ content: JSON.parse(response_body)['data']['result'].first['values']
36
+ }
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the interface to all SQL based datasources (tested with PostgreSQL and MariaDB/MySQL).
5
+ class SqlDatasource < AbstractDatasource
6
+ # +:raw_query+ needs to contain a SQL query as String in the respective database dialect
7
+ # @see AbstractDatasource#request
8
+ def request(query_description)
9
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
10
+
11
+ sql = replace_variables(query_description[:raw_query], query_description[:variables])
12
+ request = {
13
+ body: {
14
+ from: query_description[:from],
15
+ to: query_description[:to],
16
+ queries: [rawSql: prepare_sql(sql), datasourceId: id, format: 'table']
17
+ }.to_json,
18
+ request: Net::HTTP::Post
19
+ }
20
+
21
+ webrequest = query_description[:prepared_request]
22
+ webrequest.relative_url = '/api/tsdb/query'
23
+ webrequest.options.merge!(request)
24
+
25
+ result = webrequest.execute(query_description[:timeout])
26
+ preformat_response(result.body)
27
+ end
28
+
29
+ # @see AbstractDatasource#raw_query_from_panel_model
30
+ def raw_query_from_panel_model(panel_query_target)
31
+ panel_query_target['rawSql']
32
+ end
33
+
34
+ private
35
+
36
+ def preformat_response(response_body)
37
+ results = {}
38
+ results.default = []
39
+
40
+ JSON.parse(response_body)['results'].each_value do |query_result|
41
+ if query_result.key?('error')
42
+ results[:header] = results[:header] << ['SQL Error']
43
+ results[:content] = [[query_result['error']]]
44
+
45
+ elsif query_result['tables']
46
+ query_result['tables'].each do |table|
47
+ results[:header] = results[:header] << table['columns'].map { |header| header['text'] }
48
+ results[:content] = table['rows']
49
+ end
50
+
51
+ end
52
+ end
53
+
54
+ results
55
+ end
56
+
57
+ def prepare_sql(sql)
58
+ # remove comments in query
59
+ sql.gsub!(/--[^\r\n]*(?:[\r\n]+|$)/, ' ')
60
+ sql.gsub!(/\r\n/, ' ')
61
+ sql.gsub!(/\n/, ' ')
62
+ sql
63
+ end
64
+ end
65
+ end
@@ -10,6 +10,7 @@ module Grafana
10
10
  class Variable
11
11
  attr_reader :name, :text, :raw_value
12
12
 
13
+ # Translation table to support {https://momentjs.com/docs/#/displaying/}.
13
14
  DATE_MATCHES = { 'M' => '%-m', 'MM' => '%m', 'MMM' => '%b', 'MMMM' => '%B',
14
15
  'D' => '%-d', 'DD' => '%d', 'DDD' => '%-j', 'DDDD' => '%j',
15
16
  'd' => '%w', 'ddd' => '%a', 'dddd' => '%A',
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # This class standardizes all webcalls. Key functionality is to properly support HTTPS calls as a base functionality.
5
+ class WebRequest
6
+ attr_accessor :relative_url, :options
7
+
8
+ @ssl_cert = nil
9
+
10
+ class << self
11
+ attr_accessor :ssl_cert
12
+ end
13
+
14
+ # Initializes a specific HTTP request.
15
+ #
16
+ # Default (can be overridden, by specifying the options Hash):
17
+ # accept: 'application/json'
18
+ # request: Net::HTTP::Get
19
+ # content_type: 'application/json'
20
+ #
21
+ # @param base_url [String] URL which shall be queried
22
+ # @param options [Hash] options, which shall be merged to the request. Also allows `+logger+` option
23
+ def initialize(base_url, options = {})
24
+ @base_url = base_url
25
+ default_options = { accept: 'application/json', request: Net::HTTP::Get, content_type: 'application/json' }
26
+ @options = default_options.merge(options.reject { |k, _v| k == :logger && k == :relative_url })
27
+ @relative_url = options[:relative_url]
28
+ @logger = options[:logger] || Logger.new(nil)
29
+ end
30
+
31
+ # Executes the HTTP request
32
+ #
33
+ # @param timeout [Integer] number of seconds to wait, before the http request is cancelled, defaults to 60 seconds
34
+ # @return [Response] HTTP response object
35
+ def execute(timeout = nil)
36
+ timeout ||= 60
37
+
38
+ uri = URI.parse("#{@base_url}#{@relative_url}")
39
+ @http = Net::HTTP.new(uri.host, uri.port)
40
+ configure_ssl if @base_url =~ /^https/
41
+
42
+ @http.read_timeout = timeout.to_i
43
+
44
+ request = @options[:request].new(uri.request_uri)
45
+ request['Accept'] = @options[:accept] if @options[:accept]
46
+ request['Content-Type'] = @options[:content_type] if @options[:content_type]
47
+ request['Authorization'] = @options[:authorization] if @options[:authorization]
48
+ request.body = @options[:body]
49
+
50
+ @logger.debug("Requesting #{uri} with '#{@options[:body]}' and timeout '#{timeout}'")
51
+ response = @http.request(request)
52
+ @logger.debug("Received response #{response}")
53
+
54
+ response
55
+ end
56
+
57
+ private
58
+
59
+ def configure_ssl
60
+ @http.use_ssl = true
61
+ @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
62
+ if self.class.ssl_cert && !File.exist?(self.class.ssl_cert)
63
+ @logger.warn('SSL certificate file does not exist.')
64
+ elsif self.class.ssl_cert
65
+ @http.cert_store = OpenSSL::X509::Store.new
66
+ @http.cert_store.set_default_paths
67
+ @http.cert_store.add_file(self.class.ssl_cert)
68
+ end
69
+ end
70
+ end
71
+ end