ruby-grafana-reporter 0.5.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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