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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 49639b5da410d46b0ad6cf16a187a3f30389fac610497de09d1e1466a66f22f5
4
- data.tar.gz: 673791b6d38cd46e54292903f07e0fdd16070f5e0abf999565fecf04b930efbb
3
+ metadata.gz: b3d762755e84f775f00d04fe0333389ae011fbf0ed1b13942cd006599e60e0e2
4
+ data.tar.gz: 29c72d42855e2de7ecd43cbced9d6339c36312deebe05fc5210b80928e0d7894
5
5
  SHA512:
6
- metadata.gz: d4bb03253d7681c580c96f11e5e86112e036087b927ff01e0e490bebeb4aeadd5f213f9de7202007523b8bf384ed36f6fcc040d5b81aa5aceef88c7cb191ab50
7
- data.tar.gz: d3695d4d6ce451c01e7ad33a28a942f697d019215cca603827bda44af488e292c4befaf658d266e19b5ca3e84ab1a06cbc5f4f5e189d262b5c8bb415c4ae1013
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
- * [Features](#features)
13
- * [Supported datasources](#supported-datasources)
14
- * [Quick Start](#quick-start)
15
- * [Setup](#setup)
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](./assets/GettingStartedAnimation.gif)
55
+ ![GettingStarted](https://github.com/divinity666/ruby-grafana-reporter/blob/master/.assets/GettingStartedAnimation.gif)
54
56
 
55
- ## Features
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
- * Supports webhook callbacks on before, on cancel and on finishing a report (see
73
- configuration file)
74
- * Solid as a rock, also in case of template errors and whatever else may happen
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
- ## Supported datasources
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
- ## Quick Start
105
+ ## Setup
97
106
 
98
107
 
99
- ### Setup
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 cann be done with the following
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
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Version information
4
- GRAFANA_REPORTER_VERSION = [0, 7, 0].freeze
4
+ GRAFANA_REPORTER_VERSION = [0, 8, 0].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2024-05-19'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2024-06-08'
@@ -39,7 +39,7 @@ module Grafana
39
39
  end
40
40
 
41
41
  def initialize(model)
42
- @model = model
42
+ @model = model || {}
43
43
  end
44
44
 
45
45
  # @return [String] category of the datasource, e.g. +tsdb+ or +sql+
@@ -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 if @organization
33
+ return @organization unless @organization.empty?
31
34
 
32
35
  response = prepare_request({ relative_url: '/api/org/' }).execute
33
- if response.is_a?(Net::HTTPOK)
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
- if response.is_a?(Net::HTTPOK)
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
- # check if we have lower rights
65
- return 'Failed' unless prepare_request({ relative_url: '/api/dashboards/home' }).execute.is_a?(Net::HTTPOK)
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] unless @dashboards[dashboard_uid].nil?
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?
@@ -22,6 +22,7 @@ module Grafana
22
22
  webrequest.relative_url = "/api/alerts#{url_parameters(query_description)}"
23
23
 
24
24
  result = webrequest.execute(query_description[:timeout])
25
+ return unless result
25
26
 
26
27
  json = JSON.parse(result.body)
27
28
 
@@ -21,6 +21,7 @@ module Grafana
21
21
  webrequest.relative_url = "/api/annotations#{url_parameters(query_description)}"
22
22
 
23
23
  result = webrequest.execute(query_description[:timeout])
24
+ return unless result
24
25
 
25
26
  json = JSON.parse(result.body)
26
27
 
@@ -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
- response = @http.request(request)
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
- @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
63
- if self.class.ssl_cert && !File.file?(self.class.ssl_cert)
64
- @logger.warn('SSL certificate file does not exist.')
65
- elsif self.class.ssl_cert
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(self.class.ssl_cert)
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.error(e.message)
206
- row[i] = e.message
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 {AbstractQuery#format_columns}, {AbstractQuery#replace_values} and
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 = format_columns(@result, @variables['format'])
33
- @result = replace_values(@result, @variables.select { |k, _v| k =~ /^replace_values_\d+/ })
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 {AbstractQuery#format_columns}, {AbstractQuery#replace_values} and
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 = format_columns(@result, @variables['format'])
32
- @result = replace_values(@result, @variables.select { |k, _v| k =~ /^replace_values_\d+/ })
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`. Execution of related functions is
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 `_,`. Execution of related functions is applied in the following order
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. Execution of related
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. DEPRECATED: switch to
250
- `table_formatter` named `adoc_plain`, or implement a custom table formatter.
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. DEPRECATED: switch to
257
- `table_formatter` named `adoc_plain`, or implement a custom table formatter.
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/x
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
@@ -74,6 +74,7 @@ module GrafanaReporter
74
74
  # @see ProcessorMixin#build_demo_entry
75
75
  def build_demo_entry(panel)
76
76
  return nil unless panel
77
+ return nil unless panel.model['targets']
77
78
 
78
79
  ref_id = nil
79
80
  panel.model['targets'].each do |item|
@@ -79,6 +79,7 @@ module GrafanaReporter
79
79
  # @see ProcessorMixin#build_demo_entry
80
80
  def build_demo_entry(panel)
81
81
  return nil unless panel
82
+ return nil unless panel.model['targets']
82
83
 
83
84
  ref_id = nil
84
85
  panel.model['targets'].each do |item|
@@ -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?', 'aRi')
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 'Enter API key: '
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?', 'aRi')
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
- # Delegates all not configured calls to the internal and the additional logger.
46
- def method_missing(method, *args)
47
- @internal_logger.send(method, *args)
48
- @additional_logger.send(method, *args)
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
- # Registers all methods to which the internal logger responds.
52
- def respond_to_missing?(method, *_args)
53
- super
54
- @internal_logger.respond_to?(method)
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 {AbstractQuery#format_columns}, {AbstractQuery#replace_values} and
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
- modify_results
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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'rubygems'
4
+ require 'rubygems/name_tuple'
4
5
  require 'rubygems/ext'
5
6
  require 'net/http'
6
7
  require 'fileutils'
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.7.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-05-19 00:00:00.000000000 Z
11
+ date: 2024-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor