ruby-grafana-reporter 0.4.1 → 0.4.5

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +336 -185
  3. data/lib/VERSION.rb +2 -2
  4. data/lib/grafana/abstract_datasource.rb +30 -17
  5. data/lib/grafana/errors.rb +4 -4
  6. data/lib/grafana/grafana.rb +2 -0
  7. data/lib/grafana/grafana_property_datasource.rb +12 -0
  8. data/lib/grafana/graphite_datasource.rb +27 -5
  9. data/lib/grafana/influxdb_datasource.rb +156 -0
  10. data/lib/grafana/panel.rb +1 -1
  11. data/lib/grafana/prometheus_datasource.rb +37 -6
  12. data/lib/grafana/sql_datasource.rb +10 -11
  13. data/lib/grafana/variable.rb +29 -22
  14. data/lib/grafana_reporter/abstract_query.rb +150 -31
  15. data/lib/grafana_reporter/abstract_report.rb +37 -5
  16. data/lib/grafana_reporter/abstract_table_format_strategy.rb +34 -0
  17. data/lib/grafana_reporter/alerts_table_query.rb +5 -6
  18. data/lib/grafana_reporter/annotations_table_query.rb +5 -6
  19. data/lib/grafana_reporter/application/application.rb +7 -2
  20. data/lib/grafana_reporter/application/webservice.rb +35 -30
  21. data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +25 -0
  22. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +7 -5
  23. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +7 -5
  24. data/lib/grafana_reporter/asciidoctor/help.rb +458 -0
  25. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +5 -4
  26. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +5 -4
  27. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +5 -4
  28. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +6 -6
  29. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +4 -4
  30. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +21 -35
  31. data/lib/grafana_reporter/asciidoctor/report.rb +16 -26
  32. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +1 -1
  33. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +6 -4
  34. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +5 -3
  35. data/lib/grafana_reporter/console_configuration_wizard.rb +2 -2
  36. data/lib/grafana_reporter/csv_table_format_strategy.rb +23 -0
  37. data/lib/grafana_reporter/demo_report_wizard.rb +5 -2
  38. data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
  39. data/lib/grafana_reporter/erb/report.rb +14 -21
  40. data/lib/grafana_reporter/erb/report_jail.rb +21 -0
  41. data/lib/grafana_reporter/errors.rb +19 -3
  42. data/lib/grafana_reporter/panel_image_query.rb +2 -5
  43. data/lib/grafana_reporter/query_value_query.rb +1 -19
  44. data/lib/grafana_reporter/report_webhook.rb +12 -8
  45. data/lib/ruby_grafana_reporter.rb +10 -11
  46. metadata +9 -3
  47. data/lib/grafana_reporter/help.rb +0 -443
data/lib/VERSION.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Version information
4
- GRAFANA_REPORTER_VERSION = [0, 4, 1].freeze
4
+ GRAFANA_REPORTER_VERSION = [0, 4, 5].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2021-05-17'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2021-08-26'
@@ -8,19 +8,18 @@ module Grafana
8
8
 
9
9
  @@subclasses = []
10
10
 
11
- # Registers the subclass as datasource, which is asked by {#accepts?}, if it can handle a datasource
12
- # model.
11
+ # Registers the subclass as datasource.
13
12
  # @param subclass [Class] class inheriting from this abstract class
14
13
  def self.inherited(subclass)
15
14
  @@subclasses << subclass
16
15
  end
17
16
 
18
17
  # Overwrite this method, to specify if the current datasource implementation handles the given model.
19
- # This method is called by {#build_instance} to determine, if the current datasource implementation
18
+ # This method is called by {build_instance} to determine, if the current datasource implementation
20
19
  # can handle the given grafana model. By default this method returns false.
21
20
  # @param model [Hash] grafana specification of the datasource to check
22
21
  # @return [Boolean] True if fits, false otherwise
23
- def self.handles?(_model)
22
+ def self.handles?(model)
24
23
  false
25
24
  end
26
25
 
@@ -43,12 +42,12 @@ module Grafana
43
42
  @model = model
44
43
  end
45
44
 
46
- # @return [String] category of the datasource, e.g. `tsdb` or `sql`
45
+ # @return [String] category of the datasource, e.g. +tsdb+ or +sql+
47
46
  def category
48
47
  @model['meta']['category']
49
48
  end
50
49
 
51
- # @return [String] type of the datasource, e.g. `mysql`
50
+ # @return [String] type of the datasource, e.g. +mysql+
52
51
  def type
53
52
  @model['type'] || @model['meta']['id']
54
53
  end
@@ -78,12 +77,12 @@ module Grafana
78
77
  # }
79
78
  #
80
79
  # @param query_description [Hash] query description, which will requested:
81
- # @option [String] :from +from+ timestamp
82
- # @option [String] :to +to+ timestamp
83
- # @option [Integer] :timeout expected timeout for the request
84
- # @option [WebRequest] :prepared_request prepared web request for relevant {Grafana} instance, if this is needed by datasource
85
- # @option [String] :raw_query raw query, which shall be executed. May include variables, which will be replaced before execution
86
- # @option [Hash<Variable>] :variables hash of variables, which can potentially be replaced in the given +:raw_query+
80
+ # @option query_description [String] :from +from+ timestamp
81
+ # @option query_description [String] :to +to+ timestamp
82
+ # @option query_description [Integer] :timeout expected timeout for the request
83
+ # @option query_description [WebRequest] :prepared_request prepared web request for relevant {Grafana} instance, if this is needed by datasource
84
+ # @option query_description [String] :raw_query raw query, which shall be executed. May include variables, which will be replaced before execution
85
+ # @option query_description [Hash<Variable>] :variables hash of variables, which can potentially be replaced in the given +:raw_query+
87
86
  # @return [Hash] sql result formatted as stated above
88
87
  def request(query_description)
89
88
  raise NotImplementedError
@@ -101,6 +100,14 @@ module Grafana
101
100
  raise NotImplementedError
102
101
  end
103
102
 
103
+ # @abstract
104
+ #
105
+ # Overwrite in subclass, to specify the default variable format during replacement of variables.
106
+ # @return [String] default {Variable#value_formatted} format
107
+ def default_variable_format
108
+ raise NotImplementedError
109
+ end
110
+
104
111
  private
105
112
 
106
113
  # Replaces the grafana variables in the given string with their replacement value.
@@ -119,12 +126,18 @@ module Grafana
119
126
  while repeat && (repeat_count < 3)
120
127
  repeat = false
121
128
  repeat_count += 1
122
- variables.each do |var_name, obj|
129
+
130
+ variables.each do |name, variable|
123
131
  # only set ticks if value is string
124
- variable = var_name.gsub(/^var-/, '')
125
- res = res.gsub(/(?:\$\{#{variable}(?::(?<format>\w+))?\}|\$#{variable})/) do
126
- # TODO: respect datasource requirements for formatting here
127
- obj.value_formatted($LAST_MATCH_INFO ? $LAST_MATCH_INFO[:format] : nil)
132
+ var_name = name.gsub(/^var-/, '')
133
+ next unless var_name =~ /^\w+$/
134
+
135
+ res = res.gsub(/(?:\$\{#{var_name}(?::(?<format>\w+))?\}|\$#{var_name}(?!\w))/) do
136
+ format = default_variable_format
137
+ if $LAST_MATCH_INFO
138
+ format = $LAST_MATCH_INFO[:format] if $LAST_MATCH_INFO[:format]
139
+ end
140
+ variable.value_formatted(format)
128
141
  end
129
142
  end
130
143
  repeat = true if res.include?('$')
@@ -37,9 +37,9 @@ module Grafana
37
37
 
38
38
  # Raised if a given datasource does not exist in a specific {Grafana} instance.
39
39
  class DatasourceDoesNotExistError < GrafanaError
40
- # @param field [String] specifies, how the datasource has been searched, e.g. 'id' or 'name'
40
+ # @param field [String] specifies, how the datasource has been searched, e.g. +id+ or +name+
41
41
  # @param datasource_identifier [String] identifier of the datasource, which could not be found,
42
- # e.g. the specifiy id or name
42
+ # e.g. the specified id or name
43
43
  def initialize(field, datasource_identifier)
44
44
  super("Datasource with #{field} '#{datasource_identifier}' does not exist.")
45
45
  end
@@ -49,10 +49,10 @@ module Grafana
49
49
  #
50
50
  # Most likely this happens, because the image renderer is not configures properly in grafana,
51
51
  # or the panel rendering ran into a timeout.
52
+ # @param panel [Panel] panel object, which could not be rendered
52
53
  class ImageCouldNotBeRenderedError < GrafanaError
53
- # @param panel [Panel] panel object, which could not be rendered
54
54
  def initialize(panel)
55
- super("The specified panel '#{panel.id}' from dashboard '#{panel.dashboard.id} could not be "\
55
+ super("The specified panel '#{panel.id}' from dashboard '#{panel.dashboard.id}' could not be "\
56
56
  'rendered to an image.')
57
57
  end
58
58
  end
@@ -50,6 +50,8 @@ module Grafana
50
50
  # @return [Datasource] Datasource for the specified datasource name
51
51
  def datasource_by_name(datasource_name)
52
52
  datasource_name = 'default' if datasource_name.to_s.empty?
53
+ # TODO: PRIO add support for grafana builtin datasource types
54
+ return UnsupportedDatasource.new(nil) if datasource_name.to_s =~ /-- (?:Mixed|Dashboard|Grafana) --/
53
55
  raise DatasourceDoesNotExistError.new('name', datasource_name) unless @datasources[datasource_name]
54
56
 
55
57
  @datasources[datasource_name]
@@ -16,10 +16,22 @@ module Grafana
16
16
  panel = query_description[:raw_query][:panel]
17
17
  property_name = query_description[:raw_query][:property_name]
18
18
 
19
+ return "Panel property '#{property_name}' does not exist for panel '#{panel.id}'" unless panel.field(property_name)
20
+
19
21
  {
20
22
  header: [query_description[:raw_query][:property_name]],
21
23
  content: [replace_variables(panel.field(property_name), query_description[:variables])]
22
24
  }
23
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
24
36
  end
25
37
  end
@@ -36,15 +36,37 @@ module Grafana
36
36
  panel_query_target['target']
37
37
  end
38
38
 
39
+ # @see AbstractDatasource#default_variable_format
40
+ def default_variable_format
41
+ 'glob'
42
+ end
43
+
39
44
  private
40
45
 
41
46
  # @see AbstractDatasource#preformat_response
42
47
  def preformat_response(response_body)
43
- # TODO: support multiple metrics as return types
44
- {
45
- header: %w[value time],
46
- content: JSON.parse(response_body).first['datapoints']
47
- }
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] } }
48
70
  end
49
71
  end
50
72
  end
@@ -0,0 +1,156 @@
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
+ # replace variables
19
+ query = replace_variables(query_description[:raw_query], query_description[:variables])
20
+
21
+ # Unfortunately the grafana internal variables are not replaced in the grafana backend, but in the
22
+ # frontend, i.e. we have to replace them here manually
23
+ # replace $timeFilter variable
24
+ query = query.gsub(/\$timeFilter(?=\W|$)/, "time >= #{query_description[:from]}ms and time <= #{query_description[:to]}ms")
25
+
26
+ # replace grafana variables $__interval and $__interval_ms in query
27
+ # TODO: influx datasource currently uses a fixed values of 1000 width for interval variables specified in a query - it should be possible to calculate this according to grafana
28
+ # TODO: check where calculation and replacement of interval variable should take place
29
+ query = query.gsub(/\$(?:__)?interval(?=\W|$)/, "#{((query_description[:to].to_i - query_description[:from].to_i) / 1000 / 1000).to_i}s")
30
+ query = query.gsub(/\$(?:__)?interval_ms(?=\W|$)/, "#{((query_description[:to].to_i - query_description[:from].to_i) / 1000).to_i}")
31
+
32
+ url = "/api/datasources/proxy/#{id}/query?db=#{@model['database']}&q=#{ERB::Util.url_encode(query)}&epoch=ms"
33
+
34
+ webrequest = query_description[:prepared_request]
35
+ webrequest.relative_url = url
36
+ webrequest.options.merge!({ request: Net::HTTP::Get })
37
+
38
+ result = webrequest.execute(query_description[:timeout])
39
+ preformat_response(result.body)
40
+ end
41
+
42
+ # @see AbstractDatasource#raw_query_from_panel_model
43
+ def raw_query_from_panel_model(panel_query_target)
44
+ return panel_query_target['query'] if panel_query_target['rawQuery']
45
+
46
+ # build composed queries
47
+ build_select(panel_query_target['select']) + build_from(panel_query_target) + build_where(panel_query_target['tags']) + build_group_by(panel_query_target['groupBy'])
48
+ end
49
+
50
+ # @see AbstractDatasource#default_variable_format
51
+ def default_variable_format
52
+ 'regex'
53
+ end
54
+
55
+ private
56
+
57
+ def build_group_by(stmt)
58
+ groups = []
59
+ fill = ""
60
+
61
+ stmt.each do |group|
62
+ case group['type']
63
+ when 'tag'
64
+ groups << "\"#{group['params'].first}\""
65
+
66
+ when 'fill'
67
+ fill = " fill(#{group['params'].first})"
68
+
69
+ else
70
+ groups << "#{group['type']}(#{group['params'].join(', ')})"
71
+
72
+ end
73
+ end
74
+
75
+ " GROUP BY #{groups.join(', ')}#{fill}"
76
+ end
77
+
78
+ def build_where(stmt)
79
+ custom_where = []
80
+
81
+ stmt.each do |where|
82
+ value = where['operator'] =~ /^[=!]~$/ ? where['value'] : "'#{where['value']}'"
83
+ custom_where << "\"#{where['key']}\" #{where['operator']} #{value}"
84
+ end
85
+
86
+ " WHERE #{"(#{custom_where.join(' AND ')}) AND " unless custom_where.empty?}$timeFilter"
87
+ end
88
+
89
+ def build_from(stmt)
90
+ " FROM \"#{"stmt['policy']." unless stmt['policy'] == 'default'}#{stmt['measurement']}\""
91
+ end
92
+
93
+ def build_select(stmt)
94
+ res = "SELECT"
95
+ parts = []
96
+
97
+ stmt.each do |value|
98
+ part = ""
99
+
100
+ value.each do |item|
101
+ case item['type']
102
+ when 'field'
103
+ # frame field parameter as string
104
+ part = "\"#{item['params'].first}\""
105
+
106
+ when 'alias'
107
+ # append AS with parameter as string
108
+ part = "#{part} AS \"#{item['params'].first}\""
109
+
110
+
111
+ when 'math'
112
+ # append parameter as raw value for calculation
113
+ part = "#{part} #{item['params'].first}"
114
+
115
+
116
+ else
117
+ # frame current part by brackets and call by item function including parameters
118
+ part = "#{item['type']}(#{part}#{", #{item['params'].join(', ')}" unless item['params'].empty?})"
119
+ end
120
+ end
121
+
122
+ parts << part
123
+ end
124
+
125
+ "#{res} #{parts.join(', ')}"
126
+ end
127
+
128
+ # @see AbstractDatasource#preformat_response
129
+ def preformat_response(response_body)
130
+ # TODO: how to handle multiple query results?
131
+ json = JSON.parse(response_body)['results'].first['series']
132
+ return {} if json.nil?
133
+
134
+ header = ['time']
135
+ content = {}
136
+
137
+ # keep sorting, if json has only one target item, otherwise merge results and return
138
+ # as a time sorted array
139
+ return { header: header << "#{json.first['name']} #{json.first['columns'][1]} (#{json.first['tags']})", content: json.first['values'] } if json.length == 1
140
+
141
+ # TODO: show warning here, as results may be sorted different
142
+ json.each_index do |i|
143
+ header << "#{json[i]['name']} #{json[i]['columns'][1]} (#{json[i]['tags']})"
144
+ tmp = json[i]['values'].to_h
145
+ tmp.each_key { |key| content[key] = Array.new(json.length) unless content[key] }
146
+
147
+ content.merge!(tmp) do |_key, old, new|
148
+ old[i] = new
149
+ old
150
+ end
151
+ end
152
+
153
+ { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
154
+ end
155
+ end
156
+ end
data/lib/grafana/panel.rb CHANGED
@@ -18,7 +18,7 @@ module Grafana
18
18
  def field(field)
19
19
  return @model[field] if @model.key?(field)
20
20
 
21
- ''
21
+ nil
22
22
  end
23
23
 
24
24
  # @return [String] panel ID
@@ -14,7 +14,11 @@ module Grafana
14
14
  def request(query_description)
15
15
  raise MissingSqlQueryError if query_description[:raw_query].nil?
16
16
 
17
- url = "/api/datasources/proxy/#{id}/api/v1/query_range?"\
17
+ # TODO: properly allow endpoint to be set - also check raw_query method
18
+ end_point = @endpoint ? @endpoint : "query_range"
19
+
20
+ # TODO: set query option 'step' on request
21
+ url = "/api/datasources/proxy/#{id}/api/v1/#{end_point}?"\
18
22
  "start=#{query_description[:from]}&end=#{query_description[:to]}"\
19
23
  "&query=#{replace_variables(query_description[:raw_query], query_description[:variables])}"
20
24
 
@@ -28,18 +32,45 @@ module Grafana
28
32
 
29
33
  # @see AbstractDatasource#raw_query_from_panel_model
30
34
  def raw_query_from_panel_model(panel_query_target)
35
+ @endpoint = panel_query_target['format'] == 'time_series' && (panel_query_target['instant'] == false || !panel_query_target['instant']) ? 'query_range' : 'query'
31
36
  panel_query_target['expr']
32
37
  end
33
38
 
39
+ # @see AbstractDatasource#default_variable_format
40
+ def default_variable_format
41
+ 'regex'
42
+ end
43
+
34
44
  private
35
45
 
36
46
  # @see AbstractDatasource#preformat_response
37
47
  def preformat_response(response_body)
38
- # TODO: support multiple metrics as return types
39
- {
40
- header: %w[time value],
41
- content: JSON.parse(response_body)['data']['result'].first['values']
42
- }
48
+ json = JSON.parse(response_body)['data']['result']
49
+
50
+ headers = ['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
+ # TODO properly set headlines
56
+ if json.length == 1
57
+ return { header: headers << json.first['metric'].to_s, content: [[json.first['value'][1], json.first['value'][0]]] } if json.first.has_key?('value') # this happens for the special case of calls to '/query' endpoint
58
+ return { header: headers << json.first['metric']['mode'], content: json.first['values'] }
59
+ end
60
+
61
+ # TODO: show warning if results may be sorted different
62
+ json.each_index do |i|
63
+ headers += [json[i]['metric']['mode']]
64
+ tmp = json[i]['values'].to_h
65
+ tmp.each_key { |key| content[key] = Array.new(json.length) unless content[key] }
66
+
67
+ content.merge!(tmp) do |_key, old, new|
68
+ old[i] = new
69
+ old
70
+ end
71
+ end
72
+
73
+ { header: headers, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
43
74
  end
44
75
  end
45
76
  end
@@ -19,7 +19,7 @@ module Grafana
19
19
  body: {
20
20
  from: query_description[:from],
21
21
  to: query_description[:to],
22
- queries: [rawSql: prepare_sql(sql), datasourceId: id, format: 'table']
22
+ queries: [rawSql: sql, datasourceId: id, format: 'table']
23
23
  }.to_json,
24
24
  request: Net::HTTP::Post
25
25
  }
@@ -32,11 +32,18 @@ module Grafana
32
32
  preformat_response(result.body)
33
33
  end
34
34
 
35
+ # Currently all composed SQL queries are saved in the dashboard as rawSql, so no conversion
36
+ # necessary here.
35
37
  # @see AbstractDatasource#raw_query_from_panel_model
36
38
  def raw_query_from_panel_model(panel_query_target)
37
39
  panel_query_target['rawSql']
38
40
  end
39
41
 
42
+ # @see AbstractDatasource#default_variable_format
43
+ def default_variable_format
44
+ 'glob'
45
+ end
46
+
40
47
  private
41
48
 
42
49
  def preformat_response(response_body)
@@ -45,12 +52,12 @@ module Grafana
45
52
 
46
53
  JSON.parse(response_body)['results'].each_value do |query_result|
47
54
  if query_result.key?('error')
48
- results[:header] = results[:header] << ['SQL Error']
55
+ results[:header] = results[:header] + ['SQL Error']
49
56
  results[:content] = [[query_result['error']]]
50
57
 
51
58
  elsif query_result['tables']
52
59
  query_result['tables'].each do |table|
53
- results[:header] = results[:header] << table['columns'].map { |header| header['text'] }
60
+ results[:header] = results[:header] + table['columns'].map { |header| header['text'] }
54
61
  results[:content] = table['rows']
55
62
  end
56
63
 
@@ -59,13 +66,5 @@ module Grafana
59
66
 
60
67
  results
61
68
  end
62
-
63
- def prepare_sql(sql)
64
- # remove comments in query
65
- sql.gsub!(/--[^\r\n]*(?:[\r\n]+|$)/, ' ')
66
- sql.gsub!(/\r\n/, ' ')
67
- sql.gsub!(/\n/, ' ')
68
- sql
69
- end
70
69
  end
71
70
  end