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 +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
|