ruby-grafana-reporter 0.7.0 → 0.8.0
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/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
|
-

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