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 +4 -4
- data/lib/VERSION.rb +2 -2
- data/lib/grafana/influxdb_datasource.rb +6 -2
- data/lib/grafana/prometheus_datasource.rb +42 -11
- data/lib/grafana_reporter/abstract_query.rb +0 -0
- data/lib/grafana_reporter/asciidoctor/help.rb +30 -3
- data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +1 -1
- data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +11 -2
- data/lib/grafana_reporter/configuration.rb +27 -23
- data/lib/grafana_reporter/errors.rb +2 -2
- data/lib/ruby_grafana_reporter.rb +0 -0
- metadata +6 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a7dd2bc839c1b488d0c4e13d2837686a9c274845ec4f1a2ef46e95708aa4e96d
|
4
|
+
data.tar.gz: 353185c2bbe335ab78b0b2dca7e4edc561ccdfcc1fa72e12d139b8b93da57698
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '079bfb28f6365ecd10612076212447015839c1c31bbb976c99c29dbfd4a5af9e9d45d7d542b6e30d019beee4c409c3a86f0ab84aa6ae5db02352b985d04d2841'
|
7
|
+
data.tar.gz: b4d008a5bd31f825a88ec428b3b9e3b117f60156bdfd1609fea3b06d64ff9037a177a7dcdbee5490d79c177e41abb51e7e345b5b82f056448d7509712dcc6d07
|
data/lib/VERSION.rb
CHANGED
@@ -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
|
-
|
28
|
-
query = query.gsub(/\$(?:__)?
|
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
|
-
|
18
|
-
end_point = @endpoint ? @endpoint : "query_range"
|
17
|
+
query_hash = query_description[:raw_query].is_a?(Hash) ? query_description[:raw_query] : {}
|
19
18
|
|
20
|
-
#
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
|
36
|
-
|
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)
|
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('|', '\|')}#{"
|
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: #{
|
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 =
|
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
|
-
|
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.
|
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
|
+
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.
|
104
|
-
as webservice for easy integration with grafana, or as a standalone, command
|
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
|
-
|
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
|