ruby-grafana-reporter 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +22 -13
- data/lib/VERSION.rb +2 -2
- data/lib/grafana/abstract_datasource.rb +1 -1
- data/lib/grafana/grafana.rb +26 -13
- data/lib/grafana/grafana_alerts_datasource.rb +1 -0
- data/lib/grafana/grafana_annotations_datasource.rb +1 -0
- data/lib/grafana/webrequest.rb +25 -14
- data/lib/grafana_reporter/abstract_query.rb +26 -3
- data/lib/grafana_reporter/abstract_report.rb +2 -0
- data/lib/grafana_reporter/alerts_table_query.rb +5 -5
- data/lib/grafana_reporter/annotations_table_query.rb +5 -5
- data/lib/grafana_reporter/application/application.rb +2 -9
- data/lib/grafana_reporter/asciidoctor/help.rb +41 -14
- data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +5 -4
- data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +1 -0
- data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +1 -0
- data/lib/grafana_reporter/configuration.rb +15 -3
- data/lib/grafana_reporter/console_configuration_wizard.rb +58 -5
- data/lib/grafana_reporter/logger/two_way_delegate_logger.rb +26 -8
- data/lib/grafana_reporter/query_value_query.rb +11 -15
- data/lib/ruby_grafana_reporter.rb +1 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b3d762755e84f775f00d04fe0333389ae011fbf0ed1b13942cd006599e60e0e2
|
4
|
+
data.tar.gz: 29c72d42855e2de7ecd43cbced9d6339c36312deebe05fc5210b80928e0d7894
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2efa68036513d9d7c73ad13296e369bd0c945bf97dd5101f288b3c646443bbc95af0d96f876ab1c23d1424d2f450c1618d24e58705ecbfd406b31adfdd1c7871
|
7
|
+
data.tar.gz: b37f3d569f59a2ae22bea51894cde019ca395c253a4c62d83cbbfd9cd692010e327996c98fb1f0711f8f4ba396cd1eda35b203ebec7c92d95a855218aeb09fdd
|
data/README.md
CHANGED
@@ -9,10 +9,12 @@ Reporting Service for Grafana
|
|
9
9
|
## Table of Contents
|
10
10
|
|
11
11
|
* [About the project](#about-the-project)
|
12
|
-
* [
|
13
|
-
* [
|
14
|
-
* [
|
15
|
-
* [
|
12
|
+
* [Getting started](#getting-started)
|
13
|
+
* [Use cases](#use-cases)
|
14
|
+
* [Features](#features)
|
15
|
+
* [Supported datasources](#supported-datasources)
|
16
|
+
* [Setup](#setup)
|
17
|
+
* [Installation](#installation)
|
16
18
|
* [Grafana integration](#grafana-integration)
|
17
19
|
* [Advanced information](#advanced-information)
|
18
20
|
* [Webservice](#webservice)
|
@@ -50,9 +52,15 @@ By default (an extended version of) Asciidoctor is enabled as template language.
|
|
50
52
|
|
51
53
|
## Getting started
|
52
54
|
|
53
|
-
![GettingStarted](
|
55
|
+
![GettingStarted](https://github.com/divinity666/ruby-grafana-reporter/blob/master/.assets/GettingStartedAnimation.gif)
|
54
56
|
|
55
|
-
|
57
|
+
### Use cases
|
58
|
+
|
59
|
+
* Create an automated PDF report about your server infrastructure health for your management
|
60
|
+
* Allow users to build an on-demand CSV file containing data shown on your dashboard, for further use in Excel
|
61
|
+
* Export your home meter data as a static web-page, that you can publish to the web
|
62
|
+
|
63
|
+
### Features
|
56
64
|
|
57
65
|
* Supports creation of reports for multiple [grafana](https://github.com/grafana/grafana)
|
58
66
|
dashboards (and also multiple grafana installations!) in one resulting report
|
@@ -69,12 +77,13 @@ database queries
|
|
69
77
|
* webservice to be called directly from grafana
|
70
78
|
* standalone command line tool, e.g. to be automated with `cron` or `bash` scrips
|
71
79
|
* microservice from standard asciidoctor docker container without any dependencies
|
72
|
-
*
|
73
|
-
configuration file)
|
74
|
-
* Solid as a rock
|
80
|
+
* Use webhook callbacks on before, on cancel and on finishing a report (see
|
81
|
+
configuration file) to combine them with your services
|
82
|
+
* Solid as a rock - no matter if you do mistakes in your configuration or grafana does no
|
83
|
+
longer match templates: the ruby-grafana-reporter webservice will always return properly.
|
75
84
|
* Full [API documentation](https://rubydoc.info/gems/ruby-grafana-reporter) available
|
76
85
|
|
77
|
-
|
86
|
+
### Supported datasources
|
78
87
|
|
79
88
|
Functionalities are provided as shown here:
|
80
89
|
|
@@ -93,10 +102,10 @@ Composed queries are all kinds of query, where the grafana UI feature (aka visua
|
|
93
102
|
mode) for query specifications are used. In this case grafana is translating the UI query
|
94
103
|
specification to a raw query, which then in fact is sent to the database.
|
95
104
|
|
96
|
-
##
|
105
|
+
## Setup
|
97
106
|
|
98
107
|
|
99
|
-
###
|
108
|
+
### Installation
|
100
109
|
|
101
110
|
You don't have a grafana setup runnning already? No worries, just configure
|
102
111
|
`https://play.grafana.org` in the configuration wizard and see the magic
|
@@ -199,7 +208,7 @@ You may provide those variables during report generation to the reporter. Theref
|
|
199
208
|
you have to specify them in the individual calls.
|
200
209
|
|
201
210
|
Let's say, you have a variable called `serverid` in the dashboard. You may now want
|
202
|
-
to set this variable for a panel image rendering. This
|
211
|
+
to set this variable for a panel image rendering. This can be done with the following
|
203
212
|
calls:
|
204
213
|
|
205
214
|
````
|
data/lib/VERSION.rb
CHANGED
data/lib/grafana/grafana.rb
CHANGED
@@ -15,25 +15,27 @@ module Grafana
|
|
15
15
|
# trailing slash, e.g. +https://localhost:3000+.
|
16
16
|
# @param key [String] API key for the grafana instance, if required
|
17
17
|
# @param opts [Hash] additional options.
|
18
|
-
# Currently supporting +:logger+.
|
18
|
+
# Currently supporting +:logger+ and +:ssl_disable_verify+.
|
19
19
|
def initialize(base_uri, key = nil, opts = {})
|
20
20
|
@base_uri = base_uri
|
21
21
|
@key = key
|
22
22
|
@dashboards = {}
|
23
|
+
@organization = {}
|
23
24
|
@logger = opts[:logger] || ::Logger.new(nil)
|
25
|
+
@ssl_disable_verify = opts[:ssl_disable_verify] || false
|
26
|
+
@ssl_cert = opts[:ssl_cert]
|
24
27
|
|
25
28
|
initialize_datasources unless @base_uri.empty?
|
26
29
|
end
|
27
30
|
|
28
31
|
# @return [Hash] Information about the current organization
|
29
32
|
def organization
|
30
|
-
return @organization
|
33
|
+
return @organization unless @organization.empty?
|
31
34
|
|
32
35
|
response = prepare_request({ relative_url: '/api/org/' }).execute
|
33
|
-
|
34
|
-
@organization = JSON.parse(response.body)
|
35
|
-
end
|
36
|
+
return @organization unless response.is_a?(Net::HTTPOK)
|
36
37
|
|
38
|
+
@organization = JSON.parse(response.body)
|
37
39
|
@organization
|
38
40
|
end
|
39
41
|
|
@@ -42,10 +44,9 @@ module Grafana
|
|
42
44
|
return @version if @version
|
43
45
|
|
44
46
|
response = prepare_request({ relative_url: '/api/health' }).execute
|
45
|
-
|
46
|
-
@version = JSON.parse(response.body)['version']
|
47
|
-
end
|
47
|
+
return @version unless response.is_a?(Net::HTTPOK)
|
48
48
|
|
49
|
+
@version = JSON.parse(response.body)['version']
|
49
50
|
@version
|
50
51
|
end
|
51
52
|
|
@@ -54,15 +55,24 @@ module Grafana
|
|
54
55
|
# Running this function also determines, if the API configured here has Admin or NON-Admin privileges,
|
55
56
|
# or even fails on connecting to grafana.
|
56
57
|
#
|
57
|
-
# @return [String] +Admin+, +NON-Admin+ or +Failed+ is returned, depending on the test results
|
58
|
+
# @return [String] +Admin+, +NON-Admin+, +SSLError+ or +Failed+ is returned, depending on the test results
|
58
59
|
def test_connection
|
60
|
+
@logger.warn('Reporter disabled the SSL verification for grafana. This is a potential security risk.') if @ssl_disable_verify
|
61
|
+
|
59
62
|
if prepare_request({ relative_url: '/api/datasources' }).execute.is_a?(Net::HTTPOK)
|
60
63
|
# we have admin rights
|
61
64
|
@logger.warn('Reporter is running with Admin privileges on grafana. This is a potential security risk.')
|
62
65
|
return 'Admin'
|
63
66
|
end
|
64
|
-
|
65
|
-
|
67
|
+
|
68
|
+
# check if we have lower rights or an SSL error occurs
|
69
|
+
case prepare_request({ relative_url: '/api/dashboards/home' }).execute(nil, true)
|
70
|
+
when Net::HTTPOK
|
71
|
+
when OpenSSL::SSL::SSLError
|
72
|
+
return 'SSLError'
|
73
|
+
else
|
74
|
+
return 'Failed'
|
75
|
+
end
|
66
76
|
|
67
77
|
@logger.info('Reporter is running with NON-Admin privileges on grafana.')
|
68
78
|
'NON-Admin'
|
@@ -141,7 +151,7 @@ module Grafana
|
|
141
151
|
# @param dashboard_uid [String] UID of the searched {Dashboard}
|
142
152
|
# @return [Dashboard] dashboard object, if it has been found
|
143
153
|
def dashboard(dashboard_uid)
|
144
|
-
return @dashboards[dashboard_uid]
|
154
|
+
return @dashboards[dashboard_uid] if @dashboards[dashboard_uid]
|
145
155
|
|
146
156
|
response = prepare_request({ relative_url: "/api/dashboards/uid/#{dashboard_uid}" }).execute
|
147
157
|
raise DashboardDoesNotExistError, dashboard_uid unless response.is_a?(Net::HTTPOK)
|
@@ -163,7 +173,7 @@ module Grafana
|
|
163
173
|
# @return [WebRequest] webrequest prepared for execution
|
164
174
|
def prepare_request(options = {})
|
165
175
|
auth = @key ? { authorization: "Bearer #{@key}" } : {}
|
166
|
-
WebRequest.new(@base_uri, auth.merge({ logger: @logger }).merge(options))
|
176
|
+
WebRequest.new(@base_uri, auth.merge({ logger: @logger, ssl_disable_verify: @ssl_disable_verify, ssl_cert: @ssl_cert }).merge(options))
|
167
177
|
end
|
168
178
|
|
169
179
|
private
|
@@ -183,6 +193,9 @@ module Grafana
|
|
183
193
|
@logger.error("Datasource with name '#{ds_name}' and configuration: '#{ds_value}' could not be initialized.")
|
184
194
|
@datasources.delete(ds_name)
|
185
195
|
end
|
196
|
+
rescue OpenSSL::SSL::SSLError => e
|
197
|
+
@logger.error(e.message)
|
198
|
+
|
186
199
|
end
|
187
200
|
|
188
201
|
@datasources['default'] = @datasources[json['defaultDatasource']] if not @datasources[json['defaultDatasource']].nil?
|
data/lib/grafana/webrequest.rb
CHANGED
@@ -5,12 +5,6 @@ module Grafana
|
|
5
5
|
class WebRequest
|
6
6
|
attr_accessor :relative_url, :options
|
7
7
|
|
8
|
-
@ssl_cert = nil
|
9
|
-
|
10
|
-
class << self
|
11
|
-
attr_accessor :ssl_cert
|
12
|
-
end
|
13
|
-
|
14
8
|
# Initializes a specific HTTP request.
|
15
9
|
#
|
16
10
|
# Default (can be overridden, by specifying the options Hash):
|
@@ -23,16 +17,19 @@ module Grafana
|
|
23
17
|
def initialize(base_url, options = {})
|
24
18
|
@base_url = base_url
|
25
19
|
default_options = { accept: 'application/json', request: Net::HTTP::Get, content_type: 'application/json' }
|
26
|
-
@options = default_options.merge(options.reject { |k, _v| k == :logger && k == :relative_url })
|
20
|
+
@options = default_options.merge(options.reject { |k, _v| k == :logger && k == :relative_url && k == :ssl_disable_verify })
|
27
21
|
@relative_url = options[:relative_url]
|
28
22
|
@logger = options[:logger] || Logger.new(nil)
|
23
|
+
@ssl_disable_verify = options[:ssl_disable_verify] || false
|
24
|
+
@ssl_cert = options[:ssl_cert]
|
29
25
|
end
|
30
26
|
|
31
27
|
# Executes the HTTP request
|
32
28
|
#
|
33
29
|
# @param timeout [Integer] number of seconds to wait, before the http request is cancelled, defaults to 60 seconds
|
30
|
+
# @param return_ssl_error [Boolean] True, if the SSL error object shall be returned on SSL error
|
34
31
|
# @return [Response] HTTP response object
|
35
|
-
def execute(timeout = nil)
|
32
|
+
def execute(timeout = nil, return_ssl_error = false)
|
36
33
|
timeout ||= 60
|
37
34
|
|
38
35
|
uri = URI.parse("#{@base_url}#{@relative_url}")
|
@@ -48,7 +45,13 @@ module Grafana
|
|
48
45
|
request.body = @options[:body]
|
49
46
|
|
50
47
|
@logger.debug("Requesting #{uri} with '#{@options[:body]}' and timeout '#{timeout}'")
|
51
|
-
|
48
|
+
begin
|
49
|
+
response = @http.request(request)
|
50
|
+
rescue OpenSSL::SSL::SSLError => e
|
51
|
+
@logger.error(e.message)
|
52
|
+
return e if return_ssl_error
|
53
|
+
return nil
|
54
|
+
end
|
52
55
|
@logger.debug("Received response #{response}")
|
53
56
|
@logger.debug("HTTP response body: #{response.body}") unless response.code =~ /^2.*/
|
54
57
|
|
@@ -59,13 +62,21 @@ module Grafana
|
|
59
62
|
|
60
63
|
def configure_ssl
|
61
64
|
@http.use_ssl = true
|
62
|
-
|
63
|
-
if
|
64
|
-
|
65
|
-
|
65
|
+
|
66
|
+
# allow OpenSSL::SSL::VERIFY_NONE if explicitly specified
|
67
|
+
if @ssl_disable_verify
|
68
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
69
|
+
else
|
70
|
+
@http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
71
|
+
end
|
72
|
+
|
73
|
+
if @ssl_cert && !File.file?(@ssl_cert)
|
74
|
+
@logger.warn("SSL certificate file '#{@ssl_cert}' does not exist.")
|
75
|
+
elsif @ssl_cert
|
76
|
+
@logger.debug("Using ssl certificate '#{@ssl_cert}'.")
|
66
77
|
@http.cert_store = OpenSSL::X509::Store.new
|
67
78
|
@http.cert_store.set_default_paths
|
68
|
-
@http.cert_store.add_file(
|
79
|
+
@http.cert_store.add_file(@ssl_cert)
|
69
80
|
end
|
70
81
|
end
|
71
82
|
end
|
@@ -140,7 +140,6 @@ module GrafanaReporter
|
|
140
140
|
def transpose(result, transpose_variable)
|
141
141
|
return result unless transpose_variable
|
142
142
|
return result unless transpose_variable.raw_value == 'true'
|
143
|
-
|
144
143
|
result[:content] = result[:content].transpose
|
145
144
|
|
146
145
|
result
|
@@ -202,8 +201,8 @@ module GrafanaReporter
|
|
202
201
|
row[i] = format % row[i] if row[i]
|
203
202
|
end
|
204
203
|
rescue StandardError => e
|
205
|
-
@logger.
|
206
|
-
|
204
|
+
@logger.warn("Formatting of row #{i} with content '#{row[i]}' and format request '#{format}'"\
|
205
|
+
" was not possible. Row is left unchanged (message: #{e.message})")
|
207
206
|
end
|
208
207
|
end
|
209
208
|
end
|
@@ -395,6 +394,30 @@ module GrafanaReporter
|
|
395
394
|
(Time.at(date.to_time.to_i).to_i * 1000).to_s
|
396
395
|
end
|
397
396
|
|
397
|
+
# Applies a given action string, separated by commas, in the given order to the results.
|
398
|
+
def apply(result, actions, variables)
|
399
|
+
actions.raw_value.split(',').each do |action|
|
400
|
+
case action.strip
|
401
|
+
when 'filter_columns'
|
402
|
+
result = filter_columns(result, variables['filter_columns'])
|
403
|
+
when 'format'
|
404
|
+
result = format_columns(result, variables['format'])
|
405
|
+
when 'replace_values'
|
406
|
+
result = replace_values(result, variables.select { |k, _v| k =~ /^replace_values_\d+/ })
|
407
|
+
when 'transpose!'
|
408
|
+
result = transpose(result, Variable.new('true'))
|
409
|
+
when 'transpose'
|
410
|
+
result = transpose(result, variables['transpose'])
|
411
|
+
else
|
412
|
+
@logger.warn("Unsupported action '#{action}' configured in 'after_fetch' or 'after_calculate'. Only" \
|
413
|
+
" the following options are supported: filter_columns, format, replace_values, transpose,"\
|
414
|
+
" transpose!")
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
result
|
419
|
+
end
|
420
|
+
|
398
421
|
private
|
399
422
|
|
400
423
|
# Used to specify variables to be used for this query. This method ensures, that only the values of the
|
@@ -64,6 +64,8 @@ module GrafanaReporter
|
|
64
64
|
unless @grafana_instances[instance]
|
65
65
|
@grafana_instances[instance] = ::Grafana::Grafana.new(@config.grafana_host(instance),
|
66
66
|
@config.grafana_api_key(instance),
|
67
|
+
ssl_disable_verify: @config.grafana_ssl_disable_verify(instance),
|
68
|
+
ssl_cert: @config.grafana_ssl_cert(instance),
|
67
69
|
logger: @logger)
|
68
70
|
end
|
69
71
|
@grafana_instances[instance]
|
@@ -20,18 +20,18 @@ module GrafanaReporter
|
|
20
20
|
raise MissingMandatoryAttributeError, 'columns' unless @raw_query['columns']
|
21
21
|
|
22
22
|
@datasource = Grafana::GrafanaAlertsDatasource.new(nil)
|
23
|
+
@variables['after_fetch'] ||= ::Grafana::Variable.new('filter_columns')
|
24
|
+
@variables['after_calculate'] ||= ::Grafana::Variable.new('format,replace_values,transpose')
|
23
25
|
end
|
24
26
|
|
25
27
|
# Filter the query result for the given columns and sets the result in the preformatted SQL
|
26
28
|
# result stlye.
|
27
29
|
#
|
28
|
-
# Additionally it applies
|
29
|
-
# {AbstractQuery#filter_columns}.
|
30
|
+
# Additionally it applies 'after_fetch' and 'after_calculate' actions.
|
30
31
|
# @return [void]
|
31
32
|
def post_process
|
32
|
-
@result =
|
33
|
-
@result =
|
34
|
-
@result = filter_columns(@result, @variables['filter_columns'])
|
33
|
+
@result = apply(@result, @variables['after_fetch'], @variables)
|
34
|
+
@result = apply(@result, @variables['after_calculate'], @variables)
|
35
35
|
|
36
36
|
@result = format_table_output(@result,
|
37
37
|
row_divider: @variables['row_divider'],
|
@@ -19,18 +19,18 @@ module GrafanaReporter
|
|
19
19
|
raise MissingMandatoryAttributeError, 'columns' unless @raw_query['columns']
|
20
20
|
|
21
21
|
@datasource = Grafana::GrafanaAnnotationsDatasource.new(nil)
|
22
|
+
@variables['after_fetch'] ||= ::Grafana::Variable.new('filter_columns')
|
23
|
+
@variables['after_calculate'] ||= ::Grafana::Variable.new('format,replace_values,transpose')
|
22
24
|
end
|
23
25
|
|
24
26
|
# Filters the query result for the given columns and sets the result
|
25
27
|
# in the preformatted SQL result style.
|
26
28
|
#
|
27
|
-
# Additionally it applies
|
28
|
-
# {AbstractQuery#filter_columns}.
|
29
|
+
# Additionally it applies 'after_fetch' and 'after_calculate' actions.
|
29
30
|
# @return [void]
|
30
31
|
def post_process
|
31
|
-
@result =
|
32
|
-
@result =
|
33
|
-
@result = filter_columns(@result, @variables['filter_columns'])
|
32
|
+
@result = apply(@result, @variables['after_fetch'], @variables)
|
33
|
+
@result = apply(@result, @variables['after_calculate'], @variables)
|
34
34
|
|
35
35
|
@result = format_table_output(@result,
|
36
36
|
row_divider: @variables['row_divider'],
|
@@ -64,15 +64,6 @@ module GrafanaReporter
|
|
64
64
|
tmp_config.set_param("default-document-attributes:#{list[0]}", list[1])
|
65
65
|
end
|
66
66
|
|
67
|
-
opts.on('--ssl-cert FILE', 'Manually specify a SSL cert file for HTTPS connection to grafana. Only '\
|
68
|
-
'needed if not working properly otherwise.') do |file|
|
69
|
-
if File.file?(file)
|
70
|
-
tmp_config.set_param('grafana-reporter:ssl-cert', file)
|
71
|
-
else
|
72
|
-
config.logger.warn("SSL certificate file #{file} does not exist. Setting will be ignored.")
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
67
|
opts.on('--test GRAFANA_INSTANCE', 'test current configuration against given GRAFANA_INSTANCE') do |instance|
|
77
68
|
tmp_config.set_param('grafana-reporter:run-mode', 'test')
|
78
69
|
tmp_config.set_param('grafana-reporter:test-instance', instance)
|
@@ -134,6 +125,8 @@ module GrafanaReporter
|
|
134
125
|
when Configuration::MODE_CONNECTION_TEST
|
135
126
|
res = Grafana::Grafana.new(config.grafana_host(config.test_instance),
|
136
127
|
config.grafana_api_key(config.test_instance),
|
128
|
+
ssl_cert: config.grafana_ssl_cert(config.test_instance),
|
129
|
+
ssl_disable_verify: config.grafana_ssl_disable_verify(config.test_instance),
|
137
130
|
logger: config.logger).test_connection
|
138
131
|
puts res
|
139
132
|
|
@@ -194,6 +194,26 @@ end}
|
|
194
194
|
call: to="<timestamp>"
|
195
195
|
description: can be used to override default `to` time
|
196
196
|
|
197
|
+
after_fetch:
|
198
|
+
call: after_fetch="<action_1>,<action_2>,..."
|
199
|
+
description: >-
|
200
|
+
Specify the actions, that shall be performed after the query data has been fetched (and before
|
201
|
+
`select_value` calculations are performed). Possible values are: `format`, `replace_values`,
|
202
|
+
`filter_columns`, `transpose` and `transpose!`. `transpose!` enforces a transposition of the
|
203
|
+
table, independent from the configuration `transpose="true"`, which can specifically be useful,
|
204
|
+
if a table is being transposed twice.
|
205
|
+
Default: `filter_columns`
|
206
|
+
|
207
|
+
after_calculate:
|
208
|
+
call: after_calculate="<action_1>,<action_2>,..."
|
209
|
+
description: >-
|
210
|
+
Specify the actions, that shall be performed after the query calculations have been finished,
|
211
|
+
i.e. after `select_value` calculations have been performed. Possible values are: `format`,
|
212
|
+
`replace_values`, `filter_columns`, `transpose` and `transpose!`. `transpose!` enforces a
|
213
|
+
transposition of the table, independent from the configuration `transpose="true"`, which can
|
214
|
+
specifically be useful, if a table is being transposed twice.
|
215
|
+
Default: `format,replace_values,transpose`
|
216
|
+
|
197
217
|
format:
|
198
218
|
call: format="<format_col1>,<format_col2>,..."
|
199
219
|
description: >-
|
@@ -202,8 +222,6 @@ end}
|
|
202
222
|
apply `%.2f` to the first column and `%.3f` to the second column. All other columns would not be
|
203
223
|
formatted. You may also format time in milliseconds to a time format by specifying e.g. `date:iso`.
|
204
224
|
Commas in format strings are supported, but have to be escaped by using `_,`.
|
205
|
-
Execution of related functions is applied in the following order `format`,
|
206
|
-
`replace_values`, `filter_columns`, `transpose`.
|
207
225
|
see: 'https://ruby-doc.org/core/Kernel.html#method-i-sprintf'
|
208
226
|
|
209
227
|
replace_values:
|
@@ -212,9 +230,7 @@ end}
|
|
212
230
|
Specify result values which shall be replaced, e.g. `2:OK` will replace query values `2` with value `OK`.
|
213
231
|
Replacing several values is possible by separating by `,`. Matches with regular expressions are also
|
214
232
|
supported, but must be full matches, i.e. have to start with `^` and end with `$`, e.g. `^[012]$:OK`.
|
215
|
-
Number replacements can also be performed, e.g. `<8.2` or `<>3`.
|
216
|
-
applied in the following order `format`,
|
217
|
-
`replace_values`, `filter_columns`, `transpose`.
|
233
|
+
Number replacements can also be performed, e.g. `<8.2` or `<>3`.
|
218
234
|
see: https://ruby-doc.org/core/Regexp.html#class-Regexp-label-Character+Classes
|
219
235
|
|
220
236
|
include_headline:
|
@@ -226,8 +242,7 @@ end}
|
|
226
242
|
call: filter_columns="<column_name_1>,<column_name_2>,..."
|
227
243
|
description: >-
|
228
244
|
Removes specified columns from result. Commas in format strings are supported, but have to be
|
229
|
-
escaped by using `_,`.
|
230
|
-
`format`, `replace_values`, `filter_columns`, `transpose`.
|
245
|
+
escaped by using `_,`.
|
231
246
|
|
232
247
|
select_value:
|
233
248
|
call: select_value="<select_value>"
|
@@ -238,23 +253,23 @@ end}
|
|
238
253
|
transpose:
|
239
254
|
call: transpose="true"
|
240
255
|
description: >-
|
241
|
-
Transposes the query result, i.e. columns become rows and rows become columnns.
|
242
|
-
functions is applied in the following order `format`, `replace_values`, `filter_columns`,
|
243
|
-
`transpose`.
|
256
|
+
Transposes the query result, i.e. columns become rows and rows become columnns.
|
244
257
|
|
245
258
|
column_divider:
|
246
259
|
call: column_divider="<divider>"
|
247
260
|
description: >-
|
248
261
|
Replace the default column divider with another one, when used in conjunction with `table_formatter` set to
|
249
|
-
`adoc_deprecated`. Defaults to ` | ` for being interpreted as a asciidoctor column.
|
250
|
-
`table_formatter` named `adoc_plain`, or implement a custom table
|
262
|
+
`adoc_deprecated`. Defaults to ` | ` for being interpreted as a asciidoctor column. Note that this option is
|
263
|
+
DEPRECATED. As a replacement, switch to `table_formatter` named `adoc_plain`, or implement a custom table
|
264
|
+
formatter.
|
251
265
|
|
252
266
|
row_divider:
|
253
267
|
call: row_divider="<divider>"
|
254
268
|
description: >-
|
255
269
|
Replace the default row divider with another one, when used in conjunction with `table_formatter` set to
|
256
|
-
`adoc_deprecated`. Defaults to `| ` for being interpreted as a asciidoctor row.
|
257
|
-
`table_formatter` named `adoc_plain`, or implement a custom table
|
270
|
+
`adoc_deprecated`. Defaults to `| ` for being interpreted as a asciidoctor row. Note that this option is
|
271
|
+
DEPRECATED. As a replacement, switch to `table_formatter` named `adoc_plain`, or implement a custom table
|
272
|
+
formatter.
|
258
273
|
|
259
274
|
table_formatter:
|
260
275
|
call: table_formatter="<formatter>"
|
@@ -323,6 +338,8 @@ end}
|
|
323
338
|
or `grafana_default_dashboard` is set.
|
324
339
|
call: panel="<panel_id>"
|
325
340
|
standard_options:
|
341
|
+
after_calculate:
|
342
|
+
after_fetch:
|
326
343
|
column_divider:
|
327
344
|
dashboard: >-
|
328
345
|
If this option, or the global option `grafana_default_dashboard` is set, the resulting alerts will be limited to
|
@@ -359,6 +376,8 @@ end}
|
|
359
376
|
`grafana_default_dashboard` is set.
|
360
377
|
call: panel="<panel_id>"
|
361
378
|
standard_options:
|
379
|
+
after_calculate:
|
380
|
+
after_fetch:
|
362
381
|
column_divider:
|
363
382
|
dashboard: >-
|
364
383
|
If this option, or the global option `grafana_default_dashboard` is set, the resulting alerts will be limited to this
|
@@ -426,6 +445,8 @@ end}
|
|
426
445
|
call: query="<query_letter>"
|
427
446
|
description: +<query_letter>+ needs to point to the grafana query which shall be evaluated, e.g. +A+ or +B+.
|
428
447
|
standard_options:
|
448
|
+
after_calculate:
|
449
|
+
after_fetch:
|
429
450
|
column_divider:
|
430
451
|
dashboard:
|
431
452
|
filter_columns:
|
@@ -456,6 +477,8 @@ end}
|
|
456
477
|
call: query="<query_letter>"
|
457
478
|
description: +<query_letter>+ needs to point to the grafana query which shall be evaluated, e.g. +A+ or +B+.
|
458
479
|
standard_options:
|
480
|
+
after_calculate:
|
481
|
+
after_fetch:
|
459
482
|
dashboard:
|
460
483
|
filter_columns:
|
461
484
|
format:
|
@@ -478,6 +501,8 @@ end}
|
|
478
501
|
Grafana variables will be replaced in the SQL statement.
|
479
502
|
see: https://grafana.com/docs/grafana/latest/variables/syntax/
|
480
503
|
standard_options:
|
504
|
+
after_calculate:
|
505
|
+
after_fetch:
|
481
506
|
column_divider:
|
482
507
|
filter_columns:
|
483
508
|
format:
|
@@ -507,6 +532,8 @@ end}
|
|
507
532
|
square brackets, i.e. +]+ needs to be replaced with +\\]+.
|
508
533
|
see: https://grafana.com/docs/grafana/latest/variables/syntax/
|
509
534
|
standard_options:
|
535
|
+
after_calculate:
|
536
|
+
after_fetch:
|
510
537
|
filter_columns:
|
511
538
|
format:
|
512
539
|
from:
|
@@ -36,11 +36,12 @@ module GrafanaReporter
|
|
36
36
|
|
37
37
|
result.merge!(item_hash.select do |k, _v|
|
38
38
|
# TODO: specify accepted options for each processor class individually
|
39
|
-
k =~ /^(?:var-|render-)/ ||
|
40
|
-
k =~ /^(?:timeout|from|to)$/ ||
|
41
|
-
k =~ /filter_columns|format|replace_values_.*|transpose|from_timezone|
|
39
|
+
k.to_s =~ /^(?:var-|render-)/ ||
|
40
|
+
k.to_s =~ /^(?:timeout|from|to)$/ ||
|
41
|
+
k.to_s =~ /filter_columns|format|replace_values_.*|transpose|from_timezone|
|
42
42
|
to_timezone|result_type|query|table_formatter|include_headline|
|
43
|
-
column_divider|row_divider|instant|interval|verbose_log|select_value
|
43
|
+
column_divider|row_divider|instant|interval|verbose_log|select_value|
|
44
|
+
after_fetch|after_calculate/x
|
44
45
|
end)
|
45
46
|
|
46
47
|
result
|
@@ -96,6 +96,18 @@ module GrafanaReporter
|
|
96
96
|
get_config("grafana:#{instance}:api_key")
|
97
97
|
end
|
98
98
|
|
99
|
+
# @param instance [String] grafana instance name, for which the value shall be retrieved.
|
100
|
+
# @return [String] configured 'ssl-cert' for the requested grafana instance.
|
101
|
+
def grafana_ssl_cert(instance = 'default')
|
102
|
+
get_config("grafana:#{instance}:ssl-cert")
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param instance [String] grafana instance name, for which the value shall be retrieved.
|
106
|
+
# @return [String] configured 'ssl-disable-verify' for the requested grafana instance.
|
107
|
+
def grafana_ssl_disable_verify(instance = 'default')
|
108
|
+
get_config("grafana:#{instance}:ssl-disable-verify") || false
|
109
|
+
end
|
110
|
+
|
99
111
|
# @return [String] configured folder, in which the report templates are stored including trailing slash.
|
100
112
|
# By default: current folder.
|
101
113
|
def templates_folder
|
@@ -242,7 +254,6 @@ module GrafanaReporter
|
|
242
254
|
@logger.level = Object.const_get("::Logger::Severity::#{debug_level}") if debug_level =~ /DEBUG|INFO|WARN|
|
243
255
|
ERROR|FATAL|UNKNOWN/x
|
244
256
|
self.report_class = Object.const_get(rep_class) if rep_class
|
245
|
-
::Grafana::WebRequest.ssl_cert = get_config('grafana-reporter:ssl-cert')
|
246
257
|
|
247
258
|
# register callbacks
|
248
259
|
callbacks = get_config('grafana-reporter:callbacks')
|
@@ -323,7 +334,9 @@ module GrafanaReporter
|
|
323
334
|
Hash, 1, nil,
|
324
335
|
{
|
325
336
|
'host' => [String, 1, %r{^http(s)?://.+}],
|
326
|
-
'api_key' => [String, 0, %r{^(?:[\w]+[=]*)?$}]
|
337
|
+
'api_key' => [String, 0, %r{^(?:[\w]+[=]*)?$}],
|
338
|
+
'ssl-disable-verify' => [TrueClass, 0, nil],
|
339
|
+
'ssl-cert' => [String, 0, nil]
|
327
340
|
}
|
328
341
|
]
|
329
342
|
}
|
@@ -342,7 +355,6 @@ module GrafanaReporter
|
|
342
355
|
'report-class' => [String, 1, nil],
|
343
356
|
'reports-folder' => [String, explicit ? 1 : 0, nil],
|
344
357
|
'report-retention' => [Integer, explicit ? 1 : 0, nil],
|
345
|
-
'ssl-cert' => [String, 0, nil],
|
346
358
|
'webservice-port' => [Integer, explicit ? 1 : 0, nil],
|
347
359
|
'callbacks' => [Hash, 0, nil, { nil => [String, 1, nil] }]
|
348
360
|
}
|
@@ -139,7 +139,9 @@ default-document-attributes:
|
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
142
|
-
grafana = ::Grafana::Grafana.new(config.grafana_host, config.grafana_api_key
|
142
|
+
grafana = ::Grafana::Grafana.new(config.grafana_host, config.grafana_api_key,
|
143
|
+
ssl_cert: config.grafana_ssl_cert,
|
144
|
+
ssl_disable_verify: config.grafana_ssl_disable_verify)
|
143
145
|
demo_report_content = DemoReportWizard.new(config.report_class.demo_report_classes).build(grafana)
|
144
146
|
|
145
147
|
begin
|
@@ -155,32 +157,40 @@ default-document-attributes:
|
|
155
157
|
|
156
158
|
def ui_config_grafana(config)
|
157
159
|
valid = false
|
160
|
+
|
158
161
|
url = nil
|
159
162
|
api_key = nil
|
163
|
+
ssl_disable_verify = false
|
164
|
+
ssl_cert = nil
|
165
|
+
|
160
166
|
until valid
|
161
167
|
url ||= user_input('Specify grafana host', 'http://localhost:3000')
|
162
168
|
print "Testing connection to '#{url}' #{api_key ? '_with_' : '_without_'} API key..."
|
163
169
|
begin
|
164
170
|
res = Grafana::Grafana.new(url,
|
165
171
|
api_key,
|
172
|
+
ssl_disable_verify: ssl_disable_verify,
|
173
|
+
ssl_cert: ssl_cert,
|
166
174
|
logger: config.logger).test_connection
|
175
|
+
|
167
176
|
rescue StandardError => e
|
168
177
|
puts
|
169
178
|
puts e.message
|
179
|
+
|
170
180
|
end
|
171
181
|
puts 'done.'
|
172
182
|
|
173
183
|
case res
|
174
184
|
when 'Admin'
|
175
185
|
tmp = user_input('Access to grafana is permitted as Admin, which is a potential security risk.'\
|
176
|
-
' Do you want to use another [a]pi key, [r]e-enter url key or [i]gnore?', '
|
186
|
+
' Do you want to use another [a]pi key, [r]e-enter url key or [i]gnore?', 'R')
|
177
187
|
|
178
188
|
case tmp
|
179
189
|
when /(?:i|I)$/
|
180
190
|
valid = true
|
181
191
|
|
182
192
|
when /(?:a|A)$/
|
183
|
-
print
|
193
|
+
print('Enter API key: ')
|
184
194
|
api_key = gets.strip
|
185
195
|
|
186
196
|
else
|
@@ -193,9 +203,52 @@ default-document-attributes:
|
|
193
203
|
print 'Access to grafana is permitted as NON-Admin.'
|
194
204
|
valid = true
|
195
205
|
|
206
|
+
when 'SSLError'
|
207
|
+
ssl_disable_verify = false
|
208
|
+
tmp = user_input('Could not connect to grafana, because of a SSL connection error. Do you want to provide the SSL'\
|
209
|
+
' [c]ertificate file, [d]isable SSL verification (POTENTIAL SECURITY RISK) or [i]gnore and proceed?', 'C')
|
210
|
+
|
211
|
+
case tmp
|
212
|
+
when /(?:i|I)$/
|
213
|
+
# skip this step
|
214
|
+
|
215
|
+
when /(?:d|D)$/
|
216
|
+
ssl_disable_verify = true
|
217
|
+
|
218
|
+
else
|
219
|
+
ssl_configured = false
|
220
|
+
|
221
|
+
until ssl_configured
|
222
|
+
print('Enter path and filename to SSL certificate: ')
|
223
|
+
ssl_cert = gets.strip
|
224
|
+
|
225
|
+
# check if file exists
|
226
|
+
unless File.exist?(ssl_cert)
|
227
|
+
tmp = user_input('Could not read SSL certificate file. Do you want to re-enter the path to the used SSL'\
|
228
|
+
' [c]ertificate file or [s]kip configuration of SSL certificate file?', 'C')
|
229
|
+
|
230
|
+
case tmp
|
231
|
+
when /(?:s|S)$/
|
232
|
+
ssl_configured = true
|
233
|
+
ssl_cert = nil
|
234
|
+
|
235
|
+
else
|
236
|
+
# try entering path again
|
237
|
+
|
238
|
+
end
|
239
|
+
|
240
|
+
else
|
241
|
+
# SSL file exists, try connecting with that file
|
242
|
+
ssl_configured = true
|
243
|
+
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
end
|
248
|
+
|
196
249
|
else
|
197
250
|
tmp = user_input("Grafana could not be accessed at '#{url}'. Do you want to use an [a]pi key,"\
|
198
|
-
' [r]e-enter url, or [i]gnore and proceed?', '
|
251
|
+
' [r]e-enter url, or [i]gnore and proceed?', 'R')
|
199
252
|
|
200
253
|
case tmp
|
201
254
|
when /(?:i|I)$/
|
@@ -215,7 +268,7 @@ default-document-attributes:
|
|
215
268
|
end
|
216
269
|
%(grafana:
|
217
270
|
default:
|
218
|
-
host: #{url}#{api_key ? "\n api_key: #{api_key}" : ''}
|
271
|
+
host: #{url}#{api_key ? "\n api_key: #{api_key}" : ''}#{ssl_disable_verify ? "\n ssl-disable-verify: #{ssl_disable_verify}" : ''}
|
219
272
|
)
|
220
273
|
end
|
221
274
|
|
@@ -42,16 +42,34 @@ module GrafanaReporter
|
|
42
42
|
@additional_logger = logger || ::Logger.new(nil)
|
43
43
|
end
|
44
44
|
|
45
|
-
|
46
|
-
|
47
|
-
@
|
48
|
-
|
45
|
+
def fatal(*args)
|
46
|
+
@internal_logger.fatal(*args)
|
47
|
+
@additional_logger.fatal(*args)
|
48
|
+
end
|
49
|
+
|
50
|
+
def error(*args)
|
51
|
+
@internal_logger.error(*args)
|
52
|
+
@additional_logger.error(*args)
|
53
|
+
end
|
54
|
+
|
55
|
+
def warn(*args)
|
56
|
+
@internal_logger.warn(*args)
|
57
|
+
@additional_logger.warn(*args)
|
49
58
|
end
|
50
59
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
60
|
+
def info(*args)
|
61
|
+
@internal_logger.info(*args)
|
62
|
+
@additional_logger.info(*args)
|
63
|
+
end
|
64
|
+
|
65
|
+
def debug(*args)
|
66
|
+
@internal_logger.debug(*args)
|
67
|
+
@additional_logger.debug(*args)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Registers all methods to which the internal logger will respond.
|
71
|
+
def method_missing(method, *args)
|
72
|
+
@internal_logger.send(method, *args)
|
55
73
|
end
|
56
74
|
end
|
57
75
|
end
|
@@ -10,21 +10,24 @@ module GrafanaReporter
|
|
10
10
|
@datasource = @panel.datasource
|
11
11
|
end
|
12
12
|
|
13
|
-
@variables['result_type'] ||= Variable.new('')
|
13
|
+
@variables['result_type'] ||= ::Grafana::Variable.new('')
|
14
|
+
@variables['after_fetch'] ||= ::Grafana::Variable.new('filter_columns')
|
15
|
+
@variables['after_calculate'] ||= ::Grafana::Variable.new('format,replace_values,transpose')
|
14
16
|
end
|
15
17
|
|
16
|
-
# Executes
|
17
|
-
# {AbstractQuery#filter_columns} on the query results.
|
18
|
+
# Executes 'after_fetch', 'after_calculate' and 'select_value' on the on the query results.
|
18
19
|
#
|
19
20
|
# Finally the results are formatted as a asciidoctor table.
|
20
21
|
# @see Grafana::AbstractQuery#post_process
|
21
22
|
def post_process
|
22
|
-
|
23
|
+
@result = apply(@result, @variables['after_fetch'], @variables)
|
23
24
|
|
24
25
|
case @variables['result_type'].raw_value
|
25
26
|
when 'object'
|
27
|
+
@result = apply(@result, @variables['after_calculate'], @variables)
|
26
28
|
|
27
29
|
when /(?:panel_table|sql_table)/
|
30
|
+
@result = apply(@result, @variables['after_calculate'], @variables)
|
28
31
|
@result = format_table_output(@result, row_divider: @variables['row_divider'],
|
29
32
|
column_divider: @variables['column_divider'],
|
30
33
|
table_formatter: @variables['table_formatter'],
|
@@ -32,7 +35,7 @@ module GrafanaReporter
|
|
32
35
|
transpose: @variables['transpose'])
|
33
36
|
|
34
37
|
when /(?:panel_value|sql_value)/
|
35
|
-
tmp = @result[:content] || []
|
38
|
+
tmp = @result[:content] || [[]]
|
36
39
|
# use only first column of return values and replace null values with zero
|
37
40
|
tmp = tmp.map{ |item| item[0] || 0 }
|
38
41
|
|
@@ -56,7 +59,9 @@ module GrafanaReporter
|
|
56
59
|
raise UnsupportedSelectValueStatementError, @variables['select_value'].raw_value
|
57
60
|
end
|
58
61
|
|
59
|
-
@result = result
|
62
|
+
@result[:content] = [[result]]
|
63
|
+
@result = apply(@result, @variables['after_calculate'], @variables)
|
64
|
+
@result = @result[:content].flatten.first
|
60
65
|
|
61
66
|
else
|
62
67
|
raise StandardError, "Unsupported 'result_type' received: '#{@variables['result_type'].raw_value}'"
|
@@ -80,14 +85,5 @@ module GrafanaReporter
|
|
80
85
|
|
81
86
|
end
|
82
87
|
end
|
83
|
-
|
84
|
-
private
|
85
|
-
|
86
|
-
def modify_results
|
87
|
-
@result = format_columns(@result, @variables['format'])
|
88
|
-
@result = replace_values(@result, @variables.select { |k, _v| k =~ /^replace_values_\d+/ })
|
89
|
-
@result = filter_columns(@result, @variables['filter_columns'])
|
90
|
-
@result = transpose(@result, @variables['transpose'])
|
91
|
-
end
|
92
88
|
end
|
93
89
|
end
|
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.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christian Kohlmeyer
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-06-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: asciidoctor
|