ruby-grafana-reporter 0.5.0 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -0
  3. data/lib/VERSION.rb +2 -2
  4. data/lib/grafana/abstract_datasource.rb +35 -4
  5. data/lib/grafana/dashboard.rb +0 -0
  6. data/lib/grafana/errors.rb +7 -0
  7. data/lib/grafana/grafana.rb +11 -0
  8. data/lib/grafana/grafana_environment_datasource.rb +0 -0
  9. data/lib/grafana/grafana_property_datasource.rb +0 -0
  10. data/lib/grafana/graphite_datasource.rb +10 -3
  11. data/lib/grafana/image_rendering_datasource.rb +0 -0
  12. data/lib/grafana/influxdb_datasource.rb +18 -5
  13. data/lib/grafana/panel.rb +28 -1
  14. data/lib/grafana/prometheus_datasource.rb +54 -13
  15. data/lib/grafana/sql_datasource.rb +16 -6
  16. data/lib/grafana/variable.rb +0 -0
  17. data/lib/grafana/webrequest.rb +0 -0
  18. data/lib/grafana_reporter/abstract_query.rb +1 -1
  19. data/lib/grafana_reporter/abstract_report.rb +0 -0
  20. data/lib/grafana_reporter/abstract_table_format_strategy.rb +0 -0
  21. data/lib/grafana_reporter/alerts_table_query.rb +0 -0
  22. data/lib/grafana_reporter/annotations_table_query.rb +0 -0
  23. data/lib/grafana_reporter/application/webservice.rb +0 -0
  24. data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +0 -0
  25. data/lib/grafana_reporter/asciidoctor/help.rb +30 -3
  26. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +0 -0
  27. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +0 -0
  28. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +1 -1
  29. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +0 -0
  30. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +11 -2
  31. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +0 -0
  32. data/lib/grafana_reporter/configuration.rb +27 -23
  33. data/lib/grafana_reporter/console_configuration_wizard.rb +0 -0
  34. data/lib/grafana_reporter/csv_table_format_strategy.rb +0 -0
  35. data/lib/grafana_reporter/demo_report_wizard.rb +0 -0
  36. data/lib/grafana_reporter/errors.rb +2 -2
  37. data/lib/grafana_reporter/panel_image_query.rb +0 -0
  38. data/lib/grafana_reporter/query_value_query.rb +4 -1
  39. data/lib/grafana_reporter/reporter_environment_datasource.rb +0 -0
  40. data/lib/ruby_grafana_reporter.rb +0 -0
  41. metadata +6 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '06912db0a0635560dca75c00c01c047deac42741c7c6ba2ee717cbccdf26cb19'
4
- data.tar.gz: 8d09e509266737bf1b73c3f6ad5c03255de461c116a6e5ec7b45bfaef56520b7
3
+ metadata.gz: 8b50055253026b61f26a424e6016118db18c4b1998e1a26f8fa6f4c8c37f9d6d
4
+ data.tar.gz: 9ce58b615705b752f13260b4497b297aa1ad193f018a5699e0d4dca53fb1a743
5
5
  SHA512:
6
- metadata.gz: 66e81686e253f8e98101f97b0b08db97f955cd1c274f2ed683f597b675c228adf198cd2c66f9ce3df05506a760564643e43b94c40fa952b13748122ecc8d6890
7
- data.tar.gz: 916ec07231acf19c20b0f01d7bfe9a47d5825a8e0c3a6c67a9fa615e2b993343d1506fb129cc72b66e2be9b02102c15e338a6e8b5746286494de4164a9678c38
6
+ metadata.gz: 3d96e06e01a4f5d861603528f64f57cfdbca4fe32a9790f8bc82982ea8581d6d07243aa5bfccd1fac018f1da1f1ad7a514fc31a9ff1f660aa2abf99ebfd5a2ab
7
+ data.tar.gz: 17b0f9e00461cea9c9fab69572d16b12a69494b06e52500c4f0ba01ced604116c57419726982a87cdd08f462465c4e4e9fe4a891211ff185be2ca52c67c5841d
data/README.md CHANGED
File without changes
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, 5, 0].freeze
4
+ GRAFANA_REPORTER_VERSION = [0, 5, 3].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2021-11-05'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2022-03-24'
@@ -57,6 +57,11 @@ module Grafana
57
57
  @model['name']
58
58
  end
59
59
 
60
+ # @return [String] unique ID of the datasource
61
+ def uid
62
+ @model['uid']
63
+ end
64
+
60
65
  # @return [Integer] ID of the datasource
61
66
  def id
62
67
  @model['id'].to_i
@@ -108,15 +113,14 @@ module Grafana
108
113
  raise NotImplementedError
109
114
  end
110
115
 
111
- private
112
-
113
116
  # Replaces the grafana variables in the given string with their replacement value.
114
117
  #
115
118
  # @param string [String] string in which the variables shall be replaced
116
119
  # @param variables [Hash<String,Variable>] Hash containing the variables, which shall be replaced in the
117
120
  # given string
121
+ # @param overwrite_default_format [String] {Variable#value_formatted} value, if a custom default format should be used, otherwise {#default_variable_format} is used as default, which may be overwritten
118
122
  # @return [String] string in which all variables are properly replaced
119
- def replace_variables(string, variables = {})
123
+ def replace_variables(string, variables, overwrite_default_format = nil)
120
124
  res = string
121
125
  repeat = true
122
126
  repeat_count = 0
@@ -136,7 +140,8 @@ module Grafana
136
140
  next unless var_name =~ /^\w+$/
137
141
 
138
142
  res = res.gsub(/(?:\$\{#{var_name}(?::(?<format>\w+))?\}|\$#{var_name}(?!\w))/) do
139
- format = default_variable_format
143
+ format = overwrite_default_format
144
+ format = default_variable_format if overwrite_default_format.nil?
140
145
  if $LAST_MATCH_INFO
141
146
  format = $LAST_MATCH_INFO[:format] if $LAST_MATCH_INFO[:format]
142
147
  end
@@ -148,5 +153,31 @@ module Grafana
148
153
 
149
154
  res
150
155
  end
156
+
157
+ private
158
+
159
+ # Provides a general method to handle the given query response as general Grafana Dataframe format.
160
+ #
161
+ # This method throws {UnsupportedQueryResponseReceivedError} if the given query response is not a
162
+ # properly formattes dataframe
163
+ #
164
+ # @param response_body [String] raw response body
165
+ def preformat_dataframe_response(response_body)
166
+ json = JSON.parse(response_body)
167
+ data = json['results'].values.first
168
+
169
+ # TODO: check how multiple frames have to be handled
170
+ data = data['frames']
171
+ headers = []
172
+ data.first['schema']['fields'].each do |headline|
173
+ header = headline['config']['displayNameFromDS'].nil? ? headline['name'] : headline['config']['displayNameFromDS']
174
+ headers << header
175
+ end
176
+ content = data.first['data']['values'][0].zip(data.first['data']['values'][1])
177
+ return { header: headers, content: content }
178
+
179
+ rescue
180
+ raise UnsupportedQueryResponseReceivedError, response_body
181
+ end
151
182
  end
152
183
  end
File without changes
@@ -71,4 +71,11 @@ module Grafana
71
71
  super("The datasource query provided, does not look like a grafana datasource target (received: #{query}).")
72
72
  end
73
73
  end
74
+
75
+ # Raised if a datasource query returned with an unsupported result
76
+ class UnsupportedQueryResponseReceivedError < GrafanaError
77
+ def initialize(response)
78
+ super("The datasource request returned with an unsupported response format (received: #{response}).")
79
+ end
80
+ end
74
81
  end
@@ -81,6 +81,17 @@ module Grafana
81
81
  @datasources[datasource_name]
82
82
  end
83
83
 
84
+ # Returns the datasource, which has been queried by the datasource uid.
85
+ #
86
+ # @param datasource_uid [String] unique id of the searched datasource
87
+ # @return [Datasource] Datasource for the specified datasource unique id
88
+ def datasource_by_uid(datasource_uid)
89
+ datasource = @datasources.select { |_name, ds| ds.uid == datasource_uid }.values.first
90
+ raise DatasourceDoesNotExistError.new('uid', datasource_uid) unless datasource
91
+
92
+ datasource
93
+ end
94
+
84
95
  # Returns the datasource, which has been queried by the datasource id.
85
96
  #
86
97
  # @param datasource_id [Integer] id of the searched datasource
File without changes
File without changes
@@ -43,10 +43,14 @@ module Grafana
43
43
 
44
44
  private
45
45
 
46
- # @see AbstractDatasource#preformat_response
47
46
  def preformat_response(response_body)
48
- json = JSON.parse(response_body)
47
+ begin
48
+ return preformat_dataframe_response(response_body)
49
+ rescue
50
+ # TODO: show an info, that the response if not a dataframe
51
+ end
49
52
 
53
+ json = JSON.parse(response_body)
50
54
  header = ['time']
51
55
  content = {}
52
56
 
@@ -66,7 +70,10 @@ module Grafana
66
70
  end
67
71
  end
68
72
 
69
- { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
73
+ return { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
74
+
75
+ rescue
76
+ raise UnsupportedQueryResponseReceivedError, response_body
70
77
  end
71
78
  end
72
79
  end
File without changes
@@ -23,9 +23,13 @@ module Grafana
23
23
  # replace $timeFilter variable
24
24
  query = query.gsub(/\$timeFilter(?=\W|$)/, "time >= #{query_description[:from]}ms and time <= #{query_description[:to]}ms")
25
25
 
26
+ interval = query_description[:variables].delete('interval') || ((query_description[:to].to_i - query_description[:from].to_i) / 1000).to_i
27
+ interval = interval.raw_value if interval.is_a?(Variable)
28
+
26
29
  # replace grafana variables $__interval and $__interval_ms in query
27
- query = query.gsub(/\$(?:__)?interval(?=\W|$)/, "#{((query_description[:to].to_i - query_description[:from].to_i) / 1000 / 1000).to_i}s")
28
- query = query.gsub(/\$(?:__)?interval_ms(?=\W|$)/, "#{((query_description[:to].to_i - query_description[:from].to_i) / 1000).to_i}")
30
+ # TODO: check where calculation and replacement of interval variable should take place
31
+ query = query.gsub(/\$(?:__)?interval(?=\W|$)/, "#{interval.is_a?(String) ? interval : "#{(interval / 1000).to_i}s"}")
32
+ query = query.gsub(/\$(?:__)?interval_ms(?=\W|$)/, "#{interval}")
29
33
 
30
34
  url = "/api/datasources/proxy/#{id}/query?db=#{@model['database']}&q=#{ERB::Util.url_encode(query)}&epoch=ms"
31
35
 
@@ -123,10 +127,16 @@ module Grafana
123
127
  "#{res} #{parts.join(', ')}"
124
128
  end
125
129
 
126
- # @see AbstractDatasource#preformat_response
127
130
  def preformat_response(response_body)
131
+ begin
132
+ return preformat_dataframe_response(response_body)
133
+ rescue
134
+ # TODO: show an info, that the response if not a dataframe
135
+ end
136
+
128
137
  # TODO: how to handle multiple query results?
129
- json = JSON.parse(response_body)['results'].first['series']
138
+ json = JSON.parse(response_body)
139
+ json = json['results'].first['series']
130
140
  return {} if json.nil?
131
141
 
132
142
  header = ['time']
@@ -148,7 +158,10 @@ module Grafana
148
158
  end
149
159
  end
150
160
 
151
- { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
161
+ return { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
162
+
163
+ rescue
164
+ raise UnsupportedQueryResponseReceivedError, response_body
152
165
  end
153
166
  end
154
167
  end
data/lib/grafana/panel.rb CHANGED
@@ -12,6 +12,11 @@ module Grafana
12
12
  def initialize(model, dashboard)
13
13
  @model = model
14
14
  @dashboard = dashboard
15
+
16
+ @datasource_uid_or_name = @model['datasource']
17
+ if @model['datasource'].is_a?(Hash)
18
+ @datasource_uid_or_name = @model['datasource']['uid']
19
+ end
15
20
  end
16
21
 
17
22
  # @return [String] content of the requested field or +''+ if not found
@@ -26,9 +31,22 @@ module Grafana
26
31
  @model['id']
27
32
  end
28
33
 
34
+ # This method should always be called before the +datasource+ method of a
35
+ # panel is invoked, to ensure that the variable names in the datasource
36
+ # field are resolved.
37
+ #
38
+ # @param variables [Hash] variables hash, which should be use to resolve variable datasource
39
+ def resolve_variable_datasource(variables)
40
+ @datasource_uid_or_name = AbstractDatasource.new(nil).replace_variables(@datasource_uid_or_name, variables, 'raw')
41
+ end
42
+
29
43
  # @return [Datasource] datasource object specified for the current panel
30
44
  def datasource
31
- dashboard.grafana.datasource_by_name(@model['datasource'])
45
+ if datasource_kind_is_uid?
46
+ dashboard.grafana.datasource_by_uid(@datasource_uid_or_name)
47
+ else
48
+ dashboard.grafana.datasource_by_name(@datasource_uid_or_name)
49
+ end
32
50
  end
33
51
 
34
52
  # @return [String] query string for the requested query letter
@@ -43,5 +61,14 @@ module Grafana
43
61
  def render_url
44
62
  "/render/d-solo/#{@dashboard.id}?panelId=#{@model['id']}"
45
63
  end
64
+
65
+ private
66
+
67
+ def datasource_kind_is_uid?
68
+ if @model['datasource'].is_a?(Hash)
69
+ return true
70
+ end
71
+ false
72
+ end
46
73
  end
47
74
  end
@@ -14,13 +14,25 @@ module Grafana
14
14
  def request(query_description)
15
15
  raise MissingSqlQueryError if query_description[:raw_query].nil?
16
16
 
17
- # TODO: properly allow endpoint to be set - also check raw_query method
18
- end_point = @endpoint ? @endpoint : "query_range"
17
+ query_hash = query_description[:raw_query].is_a?(Hash) ? query_description[:raw_query] : {}
19
18
 
20
- # TODO: set query option 'step' on request
21
- url = "/api/datasources/proxy/#{id}/api/v1/#{end_point}?"\
22
- "start=#{query_description[:from]}&end=#{query_description[:to]}"\
23
- "&query=#{replace_variables(query_description[:raw_query], query_description[:variables])}"
19
+ # read instant value and convert instant value to boolean value
20
+ instant = query_description[:variables].delete('instant') || query_hash[:instant] || false
21
+ instant = instant.raw_value if instant.is_a?(Variable)
22
+ instant = instant.to_s.downcase == 'true'
23
+ interval = query_description[:variables].delete('interval') || query_hash[:interval] || 15
24
+ interval = interval.raw_value if interval.is_a?(Variable)
25
+ query = query_hash[:query] || query_description[:raw_query]
26
+
27
+ url = if instant
28
+ "/api/datasources/proxy/#{id}/api/v1/query?time=#{query_description[:to]}&query="\
29
+ "#{CGI.escape(replace_variables(query, query_description[:variables]))}"
30
+ else
31
+ "/api/datasources/proxy/#{id}/api/v1/query_range?start=#{query_description[:from]}"\
32
+ "&end=#{query_description[:to]}"\
33
+ "&query=#{CGI.escape(replace_variables(query, query_description[:variables]))}"\
34
+ "&step=#{interval}"
35
+ end
24
36
 
25
37
  webrequest = query_description[:prepared_request]
26
38
  webrequest.relative_url = url
@@ -32,8 +44,8 @@ module Grafana
32
44
 
33
45
  # @see AbstractDatasource#raw_query_from_panel_model
34
46
  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'
36
- panel_query_target['expr']
47
+ { query: panel_query_target['expr'], instant: panel_query_target['instant'],
48
+ interval: panel_query_target['step'] }
37
49
  end
38
50
 
39
51
  # @see AbstractDatasource#default_variable_format
@@ -43,18 +55,44 @@ module Grafana
43
55
 
44
56
  private
45
57
 
46
- # @see AbstractDatasource#preformat_response
47
58
  def preformat_response(response_body)
48
- json = JSON.parse(response_body)['data']['result']
59
+ # TODO: show raw response body to debug case https://github.com/divinity666/ruby-grafana-reporter/issues/24
60
+ begin
61
+ return preformat_dataframe_response(response_body)
62
+ rescue
63
+ # TODO: show an info, that the response if not a dataframe
64
+ end
65
+
66
+ json = JSON.parse(response_body)
67
+
68
+ # handle response with error result
69
+ unless json['error'].nil?
70
+ return { header: ['error'], content: [[ json['error'] ]] }
71
+ end
72
+
73
+ # handle former result formats
74
+ result_type = json['data']['resultType']
75
+ json = json['data']['result']
49
76
 
50
77
  headers = ['time']
51
78
  content = {}
52
79
 
80
+ # handle vector queries
81
+ if result_type == 'vector'
82
+ return {
83
+ header: (headers << 'value') + json.first['metric'].keys,
84
+ content: [ [json.first['value'][0], json.first['value'][1]] + json.first['metric'].values ]
85
+ }
86
+ end
87
+
88
+ # handle scalar queries
89
+ if result_type =~ /^(?:scalar|string)$/
90
+ return { header: headers << result_type, content: [[json[0], json[1]]] }
91
+ end
92
+
53
93
  # keep sorting, if json has only one target item, otherwise merge results and return
54
94
  # as a time sorted array
55
- # TODO properly set headlines
56
95
  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
96
  return { header: headers << json.first['metric']['mode'], content: json.first['values'] }
59
97
  end
60
98
 
@@ -70,7 +108,10 @@ module Grafana
70
108
  end
71
109
  end
72
110
 
73
- { header: headers, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
111
+ return { header: headers, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
112
+
113
+ rescue
114
+ raise UnsupportedQueryResponseReceivedError, response_body
74
115
  end
75
116
  end
76
117
  end
@@ -47,6 +47,12 @@ module Grafana
47
47
  private
48
48
 
49
49
  def preformat_response(response_body)
50
+ begin
51
+ return preformat_dataframe_response(response_body)
52
+ rescue
53
+ # TODO: show an info, that the response if not a dataframe
54
+ end
55
+
50
56
  results = {}
51
57
  results.default = []
52
58
  results[:header] = []
@@ -57,16 +63,20 @@ module Grafana
57
63
  results[:header] = results[:header] + ['SQL Error']
58
64
  results[:content] = [[query_result['error']]]
59
65
 
60
- elsif query_result['tables']
61
- query_result['tables'].each do |table|
62
- results[:header] = results[:header] + table['columns'].map { |header| header['text'] }
63
- results[:content] = table['rows']
66
+ elsif query_result.key?('tables')
67
+ if query_result['tables']
68
+ query_result['tables'].each do |table|
69
+ results[:header] = results[:header] + table['columns'].map { |header| header['text'] }
70
+ results[:content] = table['rows']
71
+ end
64
72
  end
65
-
66
73
  end
67
74
  end
68
75
 
69
- results
76
+ return results
77
+
78
+ rescue
79
+ raise UnsupportedQueryResponseReceivedError, response_body
70
80
  end
71
81
  end
72
82
  end
File without changes
File without changes
@@ -89,7 +89,7 @@ module GrafanaReporter
89
89
  # grafana errors will be directly passed through
90
90
  raise
91
91
  rescue StandardError => e
92
- raise DatasourceRequestInternalError.new(@datasource, e.message)
92
+ raise DatasourceRequestInternalError.new(@datasource, "#{e.message}\n#{e.backtrace.join("\n")}")
93
93
  end
94
94
 
95
95
  raise DatasourceRequestInvalidReturnValueError.new(@datasource, @result) unless datasource_response_valid?
File without changes
File without changes
File without changes
File without changes
@@ -22,12 +22,13 @@ module GrafanaReporter
22
22
  private
23
23
 
24
24
  def github_options
25
- { headline_separator: '#', code_begin: '`', code_end: '`', table_begin: "\n", head_postfix_col: '| -- ' }
25
+ { headline_separator: '#', code_begin: '`', code_end: '`', table_begin: "\n", head_postfix_col: '| -- ',
26
+ table_linebreak: "<br />"}
26
27
  end
27
28
 
28
29
  def asciidoctor_options
29
30
  { headline_separator: '=', code_begin: '`+', code_end: '+`', table_begin: "\n[%autowidth.stretch, "\
30
- "options=\"header\"]\n|===\n", table_end: "\n|===" }
31
+ "options=\"header\"]\n|===\n", table_end: "\n|===", table_linebreak: "\n\n" }
31
32
  end
32
33
 
33
34
  def help_text(opts)
@@ -82,7 +83,7 @@ Usage: #{opts[:code_begin]}#{v[:call]}#{opts[:code_end]}
82
83
  #{v[:description]}#{"\n\nSee also: #{v[:see]}" if v[:see]}#{unless v[:options].empty?
83
84
  %(
84
85
  #{opts[:table_begin]}| Option | Description#{"\n#{opts[:head_postfix_col] * 2}" if opts[:head_postfix_col]}
85
- #{v[:options].sort.map { |_opt_k, opt_v| "| #{opts[:code_begin]}#{opt_v[:call]}#{opts[:code_end]} | #{opt_v[:description].gsub('|', '\|')}#{"\nSee also: #{opt_v[:see]}" if opt_v[:see]}" }.join("\n") }#{opts[:table_end]})
86
+ #{v[:options].sort.map { |_opt_k, opt_v| "| #{opts[:code_begin]}#{opt_v[:call]}#{opts[:code_end]} | #{opt_v[:description].gsub('|', '\|')}#{"#{opts[:table_linebreak]}See also: #{opt_v[:see]}" if opt_v[:see]}" }.join("\n") }#{opts[:table_end]})
86
87
  end}
87
88
  )
88
89
  end
@@ -256,6 +257,20 @@ end}
256
257
  Set a timeout for the current query. If not overridden with `grafana_default_timeout` in the report template,
257
258
  this defaults to 60 seconds.
258
259
 
260
+ interval:
261
+ call: interval="<intervaL>"
262
+ description: >-
263
+ Used to set the interval size for timescale datasources, whereas the value is used without further
264
+ conversion directly in the datasource specific interval parameter.
265
+ Prometheus default: 15 (passed as `step` parameter)
266
+ Influx default: similar to grafana default, i.e. `(to_time - from_time) / 1000`
267
+ (replaces `interval_ms` and `interval` variables in query)
268
+
269
+ instant:
270
+ call: instant="true"
271
+ description: >-
272
+ Optional parameter for Prometheus `instant` queries. Ignored for other datasources than Prometheus.
273
+
259
274
  # ----------------------------------
260
275
  # FUNCTION DOCUMENTATION STARTS HERE
261
276
  # ----------------------------------
@@ -403,6 +418,8 @@ end}
403
418
  transpose:
404
419
  from_timezone:
405
420
  to_timezone:
421
+ instant:
422
+ interval:
406
423
 
407
424
  grafana_panel_query_value:
408
425
  call: 'grafana_panel_query_value:<panel_id>[query="<query_letter>",options]'
@@ -425,6 +442,8 @@ end}
425
442
  to:
426
443
  from_timezone:
427
444
  to_timezone:
445
+ instant:
446
+ interval:
428
447
 
429
448
  grafana_sql_table:
430
449
  call: 'include::grafana_sql_table:<datasource_id>[sql="<sql_query>",options]'
@@ -446,12 +465,18 @@ end}
446
465
  transpose:
447
466
  from_timezone:
448
467
  to_timezone:
468
+ instant:
469
+ interval:
449
470
 
450
471
  grafana_sql_value:
451
472
  call: 'grafana_sql_value:<datasource_id>[sql="<sql_query>",options]'
452
473
  description: >-
453
474
  Returns the value in the first column and the first row of the given query.
454
475
  Grafana variables will be replaced in the SQL statement.
476
+
477
+ Please note that asciidoctor might fail, if you use square brackets in your
478
+ sql statement. To overcome this issue, you'll need to escape the closing
479
+ square brackets, i.e. +]+ needs to be replaced with +\\]+.
455
480
  see: https://grafana.com/docs/grafana/latest/variables/syntax/
456
481
  standard_options:
457
482
  filter_columns:
@@ -463,6 +488,8 @@ end}
463
488
  to:
464
489
  from_timezone:
465
490
  to_timezone:
491
+ instant:
492
+ interval:
466
493
  YAML_HELP
467
494
  end
468
495
  end
@@ -40,7 +40,7 @@ module GrafanaReporter
40
40
  k =~ /^(?:timeout|from|to)$/ ||
41
41
  k =~ /filter_columns|format|replace_values_.*|transpose|from_timezone|
42
42
  to_timezone|result_type|query|table_formatter|include_headline|
43
- column_divider|row_divider/x
43
+ column_divider|row_divider|instant|interval/x
44
44
  end)
45
45
 
46
46
  result
@@ -44,15 +44,24 @@ module GrafanaReporter
44
44
  @report.next_step
45
45
  instance = attrs['instance'] || parent.document.attr('grafana_default_instance') || 'default'
46
46
  attrs['result_type'] = 'sql_value'
47
+ sql = attrs['sql']
47
48
  @report.logger.debug("Processing SqlValueInlineMacro (instance: #{instance}, datasource: #{target},"\
48
- " sql: #{attrs['sql']})")
49
+ " sql: #{sql})")
50
+
51
+ # translate sql statement to fix asciidoctor issue
52
+ # refer https://github.com/asciidoctor/asciidoctor/issues/4072#issuecomment-991305715
53
+ sql_translated = CGI::unescapeHTML(sql) if sql
54
+ if sql != sql_translated
55
+ @report.logger.debug("Translating SQL query to fix asciidoctor issue: #{sql_translated}")
56
+ sql = sql_translated
57
+ end
49
58
 
50
59
  begin
51
60
  # catch properly if datasource could not be identified
52
61
  query = QueryValueQuery.new(@report.grafana(instance),
53
62
  variables: build_attribute_hash(parent.document.attributes, attrs))
54
63
  query.datasource = @report.grafana(instance).datasource_by_id(target)
55
- query.raw_query = attrs['sql']
64
+ query.raw_query = sql
56
65
 
57
66
  create_inline(parent, :quoted, query.execute)
58
67
  rescue Grafana::GrafanaError => e
@@ -263,13 +263,13 @@ module GrafanaReporter
263
263
  cur_pos
264
264
  end
265
265
 
266
- def validate_schema(schema, subject)
266
+ def validate_schema(schema, subject, pattern = nil)
267
267
  return nil if subject.nil?
268
268
 
269
269
  schema.each do |key, config|
270
- type, min_occurence, next_level = config
270
+ type, min_occurence, pattern, next_level = config
271
271
 
272
- validate_schema(next_level, subject[key]) if next_level
272
+ validate_schema(next_level, subject[key], pattern) if next_level
273
273
 
274
274
  if key.nil?
275
275
  # apply to all on this level
@@ -289,9 +289,13 @@ module GrafanaReporter
289
289
  elsif subject.is_a?(Hash)
290
290
  if !subject.key?(key) && min_occurence.positive?
291
291
  raise ConfigurationDoesNotMatchSchemaError.new(key, 'occur', min_occurence, 0)
292
- end
293
- if !subject[key].is_a?(type) && subject.key?(key)
292
+ elsif !subject[key].is_a?(type) && subject.key?(key)
294
293
  raise ConfigurationDoesNotMatchSchemaError.new(key, 'be a', type, subject[key].class)
294
+ elsif pattern
295
+ # validate for regex
296
+ unless subject[key].to_s =~ pattern
297
+ raise ConfigurationDoesNotMatchSchemaError.new(key, 'match pattern', pattern.inspect, subject[key].to_s)
298
+ end
295
299
  end
296
300
 
297
301
  else
@@ -312,35 +316,35 @@ module GrafanaReporter
312
316
  {
313
317
  'grafana' =>
314
318
  [
315
- Hash, 1,
319
+ Hash, 1, nil,
316
320
  {
317
321
  nil =>
318
322
  [
319
- Hash, 1,
323
+ Hash, 1, nil,
320
324
  {
321
- 'host' => [String, 1],
322
- 'api_key' => [String, 0]
325
+ 'host' => [String, 1, %r{^http(s)?://.+}],
326
+ 'api_key' => [String, 0, %r{^(?:[\w]+[=]*)?$}]
323
327
  }
324
328
  ]
325
329
  }
326
330
  ],
327
- 'default-document-attributes' => [Hash, explicit ? 1 : 0],
328
- 'to_file' => [String, 0],
331
+ 'default-document-attributes' => [Hash, explicit ? 1 : 0, nil],
332
+ 'to_file' => [String, 0, nil],
329
333
  'grafana-reporter' =>
330
334
  [
331
- Hash, 1,
335
+ Hash, 1, nil,
332
336
  {
333
- 'check-for-updates' => [Integer, 0],
334
- 'debug-level' => [String, 0],
335
- 'run-mode' => [String, 0],
336
- 'test-instance' => [String, 0],
337
- 'templates-folder' => [String, explicit ? 1 : 0],
338
- 'report-class' => [String, 1],
339
- 'reports-folder' => [String, explicit ? 1 : 0],
340
- 'report-retention' => [Integer, explicit ? 1 : 0],
341
- 'ssl-cert' => [String, 0],
342
- 'webservice-port' => [Integer, explicit ? 1 : 0],
343
- 'callbacks' => [Hash, 0, { nil => [String, 1] }]
337
+ 'check-for-updates' => [Integer, 0, /^[0-9]*$/],
338
+ 'debug-level' => [String, 0, /^(?:DEBUG|INFO|WARN|ERROR|FATAL|UNKNOWN)?$/],
339
+ 'run-mode' => [String, 0, /^(?:test|single-render|webservice)?$/],
340
+ 'test-instance' => [String, 0, nil],
341
+ 'templates-folder' => [String, explicit ? 1 : 0, nil],
342
+ 'report-class' => [String, 1, nil],
343
+ 'reports-folder' => [String, explicit ? 1 : 0, nil],
344
+ 'report-retention' => [Integer, explicit ? 1 : 0, nil],
345
+ 'ssl-cert' => [String, 0, nil],
346
+ 'webservice-port' => [Integer, explicit ? 1 : 0, nil],
347
+ 'callbacks' => [Hash, 0, nil, { nil => [String, 1, nil] }]
344
348
  }
345
349
  ]
346
350
  }
File without changes
File without changes
File without changes
@@ -27,7 +27,7 @@ module GrafanaReporter
27
27
  # Raised if the return value of a datasource request does not match the expected return hash.
28
28
  class DatasourceRequestInvalidReturnValueError < GrafanaReporterError
29
29
  def initialize(datasource, message)
30
- super("The datasource request to '#{datasource.name}' (#{datasource.class})"\
30
+ super("The datasource request to '#{datasource.name}' (#{datasource.class}) "\
31
31
  "returned an invalid value: '#{message}'")
32
32
  end
33
33
  end
@@ -65,7 +65,7 @@ module GrafanaReporter
65
65
  # Details about how to fix that are provided in the message.
66
66
  class ConfigurationDoesNotMatchSchemaError < ConfigurationError
67
67
  def initialize(item, verb, expected, currently)
68
- super("Configuration file does not match schema definition. Expected '#{item}' to #{verb} '#{expected}',"\
68
+ super("Configuration file does not match schema definition. Expected '#{item}' to #{verb} '#{expected}', "\
69
69
  "but was '#{currently}'.")
70
70
  end
71
71
  end
File without changes
@@ -5,7 +5,10 @@ module GrafanaReporter
5
5
  class QueryValueQuery < AbstractQuery
6
6
  # @see Grafana::AbstractQuery#pre_process
7
7
  def pre_process
8
- @datasource = @panel.datasource if @panel
8
+ if @panel
9
+ @panel.resolve_variable_datasource(@variables)
10
+ @datasource = @panel.datasource
11
+ end
9
12
 
10
13
  @variables['result_type'] ||= Variable.new('')
11
14
  end
File without changes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-grafana-reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christian Kohlmeyer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-11-05 00:00:00.000000000 Z
11
+ date: 2022-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -100,9 +100,9 @@ dependencies:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
102
  version: '3.9'
103
- description: Build reports based on grafana dashboards in asciidoctor syntax. Runs
104
- as webservice for easy integration with grafana, or as a standalone, command line
105
- utility.
103
+ description: Build reports based on grafana dashboards in asciidoctor or ERB syntax.
104
+ Runs as webservice for easy integration with grafana, or as a standalone, command
105
+ line utility.
106
106
  email: kohly@gmx.de
107
107
  executables:
108
108
  - ruby-grafana-reporter
@@ -193,8 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0'
195
195
  requirements: []
196
- rubyforge_project:
197
- rubygems_version: 2.7.6.2
196
+ rubygems_version: 3.2.5
198
197
  signing_key:
199
198
  specification_version: 4
200
199
  summary: Reporter Service for Grafana