ruby-grafana-reporter 0.3.0 → 0.4.4

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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +337 -170
  3. data/bin/ruby-grafana-reporter +5 -5
  4. data/lib/VERSION.rb +3 -2
  5. data/lib/grafana/abstract_datasource.rb +149 -0
  6. data/lib/grafana/dashboard.rb +1 -3
  7. data/lib/grafana/errors.rb +20 -5
  8. data/lib/grafana/grafana.rb +52 -57
  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 +37 -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 +10 -4
  16. data/lib/grafana/prometheus_datasource.rb +67 -0
  17. data/lib/grafana/sql_datasource.rb +70 -0
  18. data/lib/grafana/unsupported_datasource.rb +7 -0
  19. data/lib/grafana/variable.rb +27 -21
  20. data/lib/grafana/webrequest.rb +71 -0
  21. data/lib/grafana_reporter/abstract_query.rb +478 -0
  22. data/lib/grafana_reporter/abstract_report.rb +152 -18
  23. data/lib/grafana_reporter/abstract_table_format_strategy.rb +34 -0
  24. data/lib/grafana_reporter/alerts_table_query.rb +43 -0
  25. data/lib/grafana_reporter/annotations_table_query.rb +42 -0
  26. data/lib/grafana_reporter/application/application.rb +28 -25
  27. data/lib/grafana_reporter/application/webservice.rb +80 -39
  28. data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +25 -0
  29. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +92 -0
  30. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +91 -0
  31. data/lib/grafana_reporter/asciidoctor/help.rb +336 -313
  32. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +78 -0
  33. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +80 -0
  34. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +74 -0
  35. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +99 -0
  36. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
  37. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +50 -0
  38. data/lib/grafana_reporter/asciidoctor/report.rb +41 -82
  39. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  40. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  41. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +94 -0
  42. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +90 -0
  43. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  44. data/lib/grafana_reporter/configuration.rb +26 -8
  45. data/lib/grafana_reporter/console_configuration_wizard.rb +109 -67
  46. data/lib/grafana_reporter/csv_table_format_strategy.rb +23 -0
  47. data/lib/grafana_reporter/demo_report_wizard.rb +104 -0
  48. data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
  49. data/lib/grafana_reporter/erb/report.rb +36 -0
  50. data/lib/grafana_reporter/erb/report_jail.rb +21 -0
  51. data/lib/grafana_reporter/errors.rb +57 -0
  52. data/lib/grafana_reporter/logger/{two_way_logger.rb → two_way_delegate_logger.rb} +1 -1
  53. data/lib/grafana_reporter/panel_image_query.rb +25 -0
  54. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  55. data/lib/grafana_reporter/query_value_query.rb +61 -0
  56. data/lib/grafana_reporter/report_webhook.rb +39 -0
  57. data/lib/ruby_grafana_extension.rb +8 -0
  58. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +1 -3
  59. metadata +49 -38
  60. data/lib/grafana/abstract_panel_query.rb +0 -22
  61. data/lib/grafana/abstract_query.rb +0 -132
  62. data/lib/grafana/abstract_sql_query.rb +0 -51
  63. data/lib/grafana/panel_image_query.rb +0 -52
  64. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -101
  65. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
  66. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -40
  67. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -92
  68. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -91
  69. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -69
  70. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -68
  71. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -61
  72. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -78
  73. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -73
  74. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -20
  75. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -43
  76. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -30
  77. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -70
  78. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -66
  79. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -88
  80. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -36
  81. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -28
  82. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -44
  83. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -40
  84. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -312
  85. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -42
  86. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -44
@@ -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,37 @@
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
+ return "Panel property '#{property_name}' does not exist for panel '#{panel.id}'" unless panel.field(property_name)
20
+
21
+ {
22
+ header: [query_description[:raw_query][:property_name]],
23
+ content: [replace_variables(panel.field(property_name), query_description[:variables])]
24
+ }
25
+ end
26
+
27
+ # @see AbstractDatasource#default_variable_format
28
+ def default_variable_format
29
+ 'glob'
30
+ end
31
+
32
+ # @see AbstractDatasource#name
33
+ def name
34
+ self.class.to_s
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the interface to graphite datasources.
5
+ class GraphiteDatasource < AbstractDatasource
6
+ # @see AbstractDatasource#handles?
7
+ def self.handles?(model)
8
+ tmp = new(model)
9
+ tmp.type == 'graphite'
10
+ end
11
+
12
+ # +:raw_query+ needs to contain a Graphite query as String
13
+ # @see AbstractDatasource#request
14
+ def request(query_description)
15
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
16
+
17
+ request = {
18
+ body: URI.encode_www_form('from': DateTime.strptime(query_description[:from], '%Q').strftime('%H:%M_%Y%m%d'),
19
+ 'until': DateTime.strptime(query_description[:to], '%Q').strftime('%H:%M_%Y%m%d'),
20
+ 'format': 'json',
21
+ 'target': replace_variables(query_description[:raw_query], query_description[:variables])),
22
+ content_type: 'application/x-www-form-urlencoded',
23
+ request: Net::HTTP::Post
24
+ }
25
+
26
+ webrequest = query_description[:prepared_request]
27
+ webrequest.relative_url = "/api/datasources/proxy/#{id}/render"
28
+ webrequest.options.merge!(request)
29
+
30
+ result = webrequest.execute(query_description[:timeout])
31
+ preformat_response(result.body)
32
+ end
33
+
34
+ # @see AbstractDatasource#raw_query_from_panel_model
35
+ def raw_query_from_panel_model(panel_query_target)
36
+ panel_query_target['target']
37
+ end
38
+
39
+ # @see AbstractDatasource#default_variable_format
40
+ def default_variable_format
41
+ 'glob'
42
+ end
43
+
44
+ private
45
+
46
+ # @see AbstractDatasource#preformat_response
47
+ def preformat_response(response_body)
48
+ json = JSON.parse(response_body)
49
+
50
+ header = ['time']
51
+ content = {}
52
+
53
+ # keep sorting, if json has only one target item, otherwise merge results and return
54
+ # as a time sorted array
55
+ return { header: header << json.first['target'], content: json.first['datapoints'].map! { |item| [item[1], item[0]] } } if json.length == 1
56
+
57
+ # TODO: show warning if results may be sorted different
58
+ json.each_index do |i|
59
+ header << json[i]['target']
60
+ tmp = json[i]['datapoints'].map! { |item| [item[1], item[0]] }.to_h
61
+ tmp.each_key { |key| content[key] = Array.new(json.length) unless content[key] }
62
+
63
+ content.merge!(tmp) do |_key, old, new|
64
+ old[i] = new
65
+ old
66
+ end
67
+ end
68
+
69
+ { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
70
+ end
71
+ end
72
+ 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
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the interface to Prometheus datasources.
5
+ class InfluxDbDatasource < AbstractDatasource
6
+ # @see AbstractDatasource#handles?
7
+ def self.handles?(model)
8
+ tmp = new(model)
9
+ tmp.type == 'influxdb'
10
+ end
11
+
12
+ # +:database+ needs to contain the InfluxDb database name
13
+ # +:raw_query+ needs to contain a InfluxDb query as String
14
+ # @see AbstractDatasource#request
15
+ def request(query_description)
16
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
17
+
18
+ url = "/api/datasources/proxy/#{id}/query?db=#{@model['database']}&q=#{ERB::Util.url_encode(query_description[:raw_query])}&epoch=ms"
19
+
20
+ webrequest = query_description[:prepared_request]
21
+ webrequest.relative_url = url
22
+ webrequest.options.merge!({ request: Net::HTTP::Get })
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
+ return panel_query_target['query'] if panel_query_target['rawQuery']
31
+
32
+ # TODO: support composed queries
33
+ raise ComposedQueryNotSupportedError, self
34
+ end
35
+
36
+ # @see AbstractDatasource#default_variable_format
37
+ def default_variable_format
38
+ 'regex'
39
+ end
40
+
41
+ private
42
+
43
+ # @see AbstractDatasource#preformat_response
44
+ def preformat_response(response_body)
45
+ # TODO: how to handle multiple query results?
46
+ json = JSON.parse(response_body)['results'].first['series']
47
+
48
+ header = ['time']
49
+ content = {}
50
+
51
+ # keep sorting, if json has only one target item, otherwise merge results and return
52
+ # as a time sorted array
53
+ return { header: header << "#{json.first['name']} #{json.first['columns'][1]} (#{json.first['tags']})", content: json.first['values'] } if json.length == 1
54
+
55
+ # TODO: show warning here, as results may be sorted different
56
+ json.each_index do |i|
57
+ header << "#{json[i]['name']} #{json[i]['columns'][1]} (#{json[i]['tags']})"
58
+ tmp = json[i]['values'].to_h
59
+ tmp.each_key { |key| content[key] = Array.new(json.length) unless content[key] }
60
+
61
+ content.merge!(tmp) do |_key, old, new|
62
+ old[i] = new
63
+ old
64
+ end
65
+ end
66
+
67
+ { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
68
+ end
69
+ end
70
+ 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
@@ -17,7 +18,7 @@ module Grafana
17
18
  def field(field)
18
19
  return @model[field] if @model.key?(field)
19
20
 
20
- ''
21
+ nil
21
22
  end
22
23
 
23
24
  # @return [String] panel ID
@@ -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,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # Implements the interface to Prometheus datasources.
5
+ class PrometheusDatasource < AbstractDatasource
6
+ # @see AbstractDatasource#handles?
7
+ def self.handles?(model)
8
+ tmp = new(model)
9
+ tmp.type == 'prometheus'
10
+ end
11
+
12
+ # +:raw_query+ needs to contain a Prometheus query as String
13
+ # @see AbstractDatasource#request
14
+ def request(query_description)
15
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
16
+
17
+ url = "/api/datasources/proxy/#{id}/api/v1/query_range?"\
18
+ "start=#{query_description[:from]}&end=#{query_description[:to]}"\
19
+ "&query=#{replace_variables(query_description[:raw_query], query_description[:variables])}"
20
+
21
+ webrequest = query_description[:prepared_request]
22
+ webrequest.relative_url = url
23
+ webrequest.options.merge!({ request: Net::HTTP::Get })
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['expr']
32
+ end
33
+
34
+ # @see AbstractDatasource#default_variable_format
35
+ def default_variable_format
36
+ 'regex'
37
+ end
38
+
39
+ private
40
+
41
+ # @see AbstractDatasource#preformat_response
42
+ def preformat_response(response_body)
43
+ json = JSON.parse(response_body)['data']['result']
44
+
45
+ headers = ['time']
46
+ content = {}
47
+
48
+ # keep sorting, if json has only one target item, otherwise merge results and return
49
+ # as a time sorted array
50
+ return { header: headers << json.first['metric']['mode'], content: json.first['values'] } if json.length == 1
51
+
52
+ # TODO: show warning if results may be sorted different
53
+ json.each_index do |i|
54
+ headers += [json[i]['metric']['mode']]
55
+ tmp = json[i]['values'].to_h
56
+ tmp.each_key { |key| content[key] = Array.new(json.length) unless content[key] }
57
+
58
+ content.merge!(tmp) do |_key, old, new|
59
+ old[i] = new
60
+ old
61
+ end
62
+ end
63
+
64
+ { header: headers, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,70 @@
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
+ # @see AbstractDatasource#handles?
7
+ def self.handles?(model)
8
+ tmp = new(model)
9
+ tmp.category == 'sql'
10
+ end
11
+
12
+ # +:raw_query+ needs to contain a SQL query as String in the respective database dialect
13
+ # @see AbstractDatasource#request
14
+ def request(query_description)
15
+ raise MissingSqlQueryError if query_description[:raw_query].nil?
16
+
17
+ sql = replace_variables(query_description[:raw_query], query_description[:variables])
18
+ request = {
19
+ body: {
20
+ from: query_description[:from],
21
+ to: query_description[:to],
22
+ queries: [rawSql: sql, datasourceId: id, format: 'table']
23
+ }.to_json,
24
+ request: Net::HTTP::Post
25
+ }
26
+
27
+ webrequest = query_description[:prepared_request]
28
+ webrequest.relative_url = '/api/tsdb/query'
29
+ webrequest.options.merge!(request)
30
+
31
+ result = webrequest.execute(query_description[:timeout])
32
+ preformat_response(result.body)
33
+ end
34
+
35
+ # Currently all composed SQL queries are saved in the dashboard as rawSql, so no conversion
36
+ # necessary here.
37
+ # @see AbstractDatasource#raw_query_from_panel_model
38
+ def raw_query_from_panel_model(panel_query_target)
39
+ panel_query_target['rawSql']
40
+ end
41
+
42
+ # @see AbstractDatasource#default_variable_format
43
+ def default_variable_format
44
+ 'glob'
45
+ end
46
+
47
+ private
48
+
49
+ def preformat_response(response_body)
50
+ results = {}
51
+ results.default = []
52
+
53
+ JSON.parse(response_body)['results'].each_value do |query_result|
54
+ if query_result.key?('error')
55
+ results[:header] = results[:header] + ['SQL Error']
56
+ results[:content] = [[query_result['error']]]
57
+
58
+ elsif query_result['tables']
59
+ query_result['tables'].each do |table|
60
+ results[:header] = results[:header] + table['columns'].map { |header| header['text'] }
61
+ results[:content] = table['rows']
62
+ end
63
+
64
+ end
65
+ end
66
+
67
+ results
68
+ end
69
+ end
70
+ end