ruby-grafana-reporter 0.5.0 → 0.5.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '06912db0a0635560dca75c00c01c047deac42741c7c6ba2ee717cbccdf26cb19'
4
- data.tar.gz: 8d09e509266737bf1b73c3f6ad5c03255de461c116a6e5ec7b45bfaef56520b7
3
+ metadata.gz: a7dd2bc839c1b488d0c4e13d2837686a9c274845ec4f1a2ef46e95708aa4e96d
4
+ data.tar.gz: 353185c2bbe335ab78b0b2dca7e4edc561ccdfcc1fa72e12d139b8b93da57698
5
5
  SHA512:
6
- metadata.gz: 66e81686e253f8e98101f97b0b08db97f955cd1c274f2ed683f597b675c228adf198cd2c66f9ce3df05506a760564643e43b94c40fa952b13748122ecc8d6890
7
- data.tar.gz: 916ec07231acf19c20b0f01d7bfe9a47d5825a8e0c3a6c67a9fa615e2b993343d1506fb129cc72b66e2be9b02102c15e338a6e8b5746286494de4164a9678c38
6
+ metadata.gz: '079bfb28f6365ecd10612076212447015839c1c31bbb976c99c29dbfd4a5af9e9d45d7d542b6e30d019beee4c409c3a86f0ab84aa6ae5db02352b985d04d2841'
7
+ data.tar.gz: b4d008a5bd31f825a88ec428b3b9e3b117f60156bdfd1609fea3b06d64ff9037a177a7dcdbee5490d79c177e41abb51e7e345b5b82f056448d7509712dcc6d07
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, 1].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2021-11-05'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2021-12-18'
@@ -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
 
@@ -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
@@ -45,16 +57,35 @@ module Grafana
45
57
 
46
58
  # @see AbstractDatasource#preformat_response
47
59
  def preformat_response(response_body)
48
- json = JSON.parse(response_body)['data']['result']
60
+ json = JSON.parse(response_body)
61
+
62
+ # handle response with error result
63
+ unless json['error'].nil?
64
+ return { header: ['error'], content: [[ json['error'] ]] }
65
+ end
66
+
67
+ result_type = json['data']['resultType']
68
+ json = json['data']['result']
49
69
 
50
70
  headers = ['time']
51
71
  content = {}
52
72
 
73
+ # handle vector queries
74
+ if result_type == 'vector'
75
+ return {
76
+ header: (headers << 'value') + json.first['metric'].keys,
77
+ content: [ [json.first['value'][0], json.first['value'][1]] + json.first['metric'].values ]
78
+ }
79
+ end
80
+
81
+ # handle scalar queries
82
+ if result_type =~ /^(?:scalar|string)$/
83
+ return { header: headers << result_type, content: [[json[0], json[1]]] }
84
+ end
85
+
53
86
  # keep sorting, if json has only one target item, otherwise merge results and return
54
87
  # as a time sorted array
55
- # TODO properly set headlines
56
88
  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
89
  return { header: headers << json.first['metric']['mode'], content: json.first['values'] }
59
90
  end
60
91
 
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
  }
@@ -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
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.1
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: 2021-12-18 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