ruby-grafana-reporter 0.5.2 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de6063d128d57f930e83b17253e34e4e20624ef796e7eb86df2e7d54bee7825c
4
- data.tar.gz: a6a5f8b3bfcdcfc557ede19e7d89a25de8dca277806da0f662c4165d0a05b03c
3
+ metadata.gz: e198f995d87598512fbe2a4578edf8be48630bab415d14c5348cf23a57e47df7
4
+ data.tar.gz: c1f5a3c85ba1081fbfc0b526b1c4e15412d146a8f88af897a0620fd762687482
5
5
  SHA512:
6
- metadata.gz: c9338b1dbd16a81af0c28feb9e8f4efd576ffb3f879aae7dd5d31b00468b32cc18ee49e7a73f87e952e2781845cd1d54124512aef6112194379d3840322fbff8
7
- data.tar.gz: 01bed067acfc6442e62ba17dbda3491e6147778a274c65ccacc7f838e57beda53809e8a8b827fad09266b4b506c2e29d7e8d3b0670c506eb0063eddd40d1aa26
6
+ metadata.gz: 9e5af10cd799f63a8ac662db422cebe1a8f7d1484c5e29e54ec73280c966efc7a57059c5fbe2bef2717b27fe6c2fa1c22c819fce3c851d98d568fd556291d933
7
+ data.tar.gz: 97c1477d8e97a6afa096d7dc4557d4ae15e9aee87ca541af165f898f3cfd1a20d002e2b06c68c98dd4f0c436299129e766e0023f34b6f1a2639d4fcabcfde73d
data/README.md CHANGED
@@ -310,6 +310,7 @@ webservice or a rendering process starts.
310
310
  This is just a collection of things, I am heading for in future, without a schedule.
311
311
 
312
312
  * Support grafana internal datasources
313
+ * Support additional templating variable types
313
314
  * Solve code TODOs
314
315
  * Become [rubocop](https://rubocop.org/) ready
315
316
 
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, 5, 2].freeze
4
+ GRAFANA_REPORTER_VERSION = [0, 6, 0].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2022-03-22'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2022-08-01'
@@ -82,6 +82,7 @@ module Grafana
82
82
  # }
83
83
  #
84
84
  # @param query_description [Hash] query description, which will requested:
85
+ # @option query_description [String] :grafana_version grafana version, for which the request is to be prepared
85
86
  # @option query_description [String] :from +from+ timestamp
86
87
  # @option query_description [String] :to +to+ timestamp
87
88
  # @option query_description [Integer] :timeout expected timeout for the request
@@ -113,15 +114,14 @@ module Grafana
113
114
  raise NotImplementedError
114
115
  end
115
116
 
116
- private
117
-
118
117
  # Replaces the grafana variables in the given string with their replacement value.
119
118
  #
120
119
  # @param string [String] string in which the variables shall be replaced
121
120
  # @param variables [Hash<String,Variable>] Hash containing the variables, which shall be replaced in the
122
121
  # given string
122
+ # @param overwrite_default_format [String] {Variable#value_formatted} value, if a custom default format should be used, otherwise {#default_variable_format} is used as default, which may be overwritten
123
123
  # @return [String] string in which all variables are properly replaced
124
- def replace_variables(string, variables = {})
124
+ def replace_variables(string, variables, overwrite_default_format = nil)
125
125
  res = string
126
126
  repeat = true
127
127
  repeat_count = 0
@@ -141,7 +141,8 @@ module Grafana
141
141
  next unless var_name =~ /^\w+$/
142
142
 
143
143
  res = res.gsub(/(?:\$\{#{var_name}(?::(?<format>\w+))?\}|\$#{var_name}(?!\w))/) do
144
- format = default_variable_format
144
+ format = overwrite_default_format
145
+ format = default_variable_format if overwrite_default_format.nil?
145
146
  if $LAST_MATCH_INFO
146
147
  format = $LAST_MATCH_INFO[:format] if $LAST_MATCH_INFO[:format]
147
148
  end
@@ -153,5 +154,31 @@ module Grafana
153
154
 
154
155
  res
155
156
  end
157
+
158
+ private
159
+
160
+ # Provides a general method to handle the given query response as general Grafana Dataframe format.
161
+ #
162
+ # This method throws {UnsupportedQueryResponseReceivedError} if the given query response is not a
163
+ # properly formattes dataframe
164
+ #
165
+ # @param response_body [String] raw response body
166
+ def preformat_dataframe_response(response_body)
167
+ json = JSON.parse(response_body)
168
+ data = json['results'].values.first
169
+
170
+ # TODO: check how multiple frames have to be handled
171
+ data = data['frames']
172
+ headers = []
173
+ data.first['schema']['fields'].each do |headline|
174
+ header = headline['config']['displayNameFromDS'].nil? ? headline['name'] : headline['config']['displayNameFromDS']
175
+ headers << header
176
+ end
177
+ content = data.first['data']['values'][0].zip(data.first['data']['values'][1])
178
+ return { header: headers, content: content }
179
+
180
+ rescue
181
+ raise UnsupportedQueryResponseReceivedError, response_body
182
+ end
156
183
  end
157
184
  end
@@ -58,7 +58,14 @@ module Grafana
58
58
  list = @model['templating']['list']
59
59
  return unless list.is_a? Array
60
60
 
61
- list.each { |item| @variables << Variable.new(item, self) }
61
+ list.each do |item|
62
+ begin
63
+ @variables << Variable.new(item, self)
64
+ rescue => e
65
+ # TODO: show this message as a warning - needs test cleanup
66
+ @grafana.logger.debug(e.message)
67
+ end
68
+ end
62
69
  end
63
70
 
64
71
  # read panels
@@ -86,7 +86,15 @@ module Grafana
86
86
  # @param datasource_uid [String] unique id of the searched datasource
87
87
  # @return [Datasource] Datasource for the specified datasource unique id
88
88
  def datasource_by_uid(datasource_uid)
89
- datasource = @datasources.select { |_name, ds| ds.uid == datasource_uid }.values.first
89
+ datasource = @datasources.select do |ds_name, ds|
90
+ if (ds.nil?)
91
+ # print debug info for https://github.com/divinity666/ruby-grafana-reporter/issues/29
92
+ @logger.warn("Datasource with name #{ds_name} is nil, which should never happen. Check logs for details.")
93
+ false
94
+ else
95
+ ds.uid == datasource_uid
96
+ end
97
+ end.values.first
90
98
  raise DatasourceDoesNotExistError.new('uid', datasource_uid) unless datasource
91
99
 
92
100
  datasource
@@ -156,6 +164,12 @@ module Grafana
156
164
  json = JSON.parse(settings.body)
157
165
  json['datasources'].select { |_k, v| v['id'].to_i.positive? }.each do |ds_name, ds_value|
158
166
  @datasources[ds_name] = AbstractDatasource.build_instance(ds_value)
167
+
168
+ # print debug info for https://github.com/divinity666/ruby-grafana-reporter/issues/29
169
+ if @datasources[ds_name].nil?
170
+ @logger.error("Datasource with name '#{ds_name}' and configuration: '#{ds_value}' could not be initialized.")
171
+ @datasources.delete(ds_name)
172
+ end
159
173
  end
160
174
  @datasources['default'] = @datasources[json['defaultDatasource']]
161
175
  end
File without changes
@@ -43,13 +43,14 @@ module Grafana
43
43
 
44
44
  private
45
45
 
46
- # @see AbstractDatasource#preformat_response
47
46
  def preformat_response(response_body)
48
- json = JSON.parse(response_body)
49
-
50
- raise UnsupportedQueryResponseReceivedError, response_body if json.first['target'].nil?
51
- raise UnsupportedQueryResponseReceivedError, response_body if json.first['datapoints'].nil?
47
+ begin
48
+ return preformat_dataframe_response(response_body)
49
+ rescue
50
+ # TODO: show an info, that the response if not a dataframe
51
+ end
52
52
 
53
+ json = JSON.parse(response_body)
53
54
  header = ['time']
54
55
  content = {}
55
56
 
@@ -69,7 +70,10 @@ module Grafana
69
70
  end
70
71
  end
71
72
 
72
- { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
73
+ return { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
74
+
75
+ rescue
76
+ raise UnsupportedQueryResponseReceivedError, response_body
73
77
  end
74
78
  end
75
79
  end
@@ -127,14 +127,15 @@ module Grafana
127
127
  "#{res} #{parts.join(', ')}"
128
128
  end
129
129
 
130
- # @see AbstractDatasource#preformat_response
131
130
  def preformat_response(response_body)
131
+ begin
132
+ return preformat_dataframe_response(response_body)
133
+ rescue
134
+ # TODO: show an info, that the response if not a dataframe
135
+ end
136
+
132
137
  # TODO: how to handle multiple query results?
133
138
  json = JSON.parse(response_body)
134
- raise UnsupportedQueryResponseReceivedError, response_body if json['results'].nil?
135
- raise UnsupportedQueryResponseReceivedError, response_body if json['results'].first.nil?
136
- raise UnsupportedQueryResponseReceivedError, response_body if json['results'].first['series'].nil?
137
-
138
139
  json = json['results'].first['series']
139
140
  return {} if json.nil?
140
141
 
@@ -157,7 +158,10 @@ module Grafana
157
158
  end
158
159
  end
159
160
 
160
- { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
161
+ return { header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
162
+
163
+ rescue
164
+ raise UnsupportedQueryResponseReceivedError, response_body
161
165
  end
162
166
  end
163
167
  end
data/lib/grafana/panel.rb CHANGED
@@ -12,6 +12,11 @@ module Grafana
12
12
  def initialize(model, dashboard)
13
13
  @model = model
14
14
  @dashboard = dashboard
15
+
16
+ @datasource_uid_or_name = @model['datasource']
17
+ if @model['datasource'].is_a?(Hash)
18
+ @datasource_uid_or_name = @model['datasource']['uid']
19
+ end
15
20
  end
16
21
 
17
22
  # @return [String] content of the requested field or +''+ if not found
@@ -26,12 +31,21 @@ module Grafana
26
31
  @model['id']
27
32
  end
28
33
 
34
+ # This method should always be called before the +datasource+ method of a
35
+ # panel is invoked, to ensure that the variable names in the datasource
36
+ # field are resolved.
37
+ #
38
+ # @param variables [Hash] variables hash, which should be use to resolve variable datasource
39
+ def resolve_variable_datasource(variables)
40
+ @datasource_uid_or_name = AbstractDatasource.new(nil).replace_variables(@datasource_uid_or_name, variables, 'raw') if @datasource_uid_or_name.is_a?(String)
41
+ end
42
+
29
43
  # @return [Datasource] datasource object specified for the current panel
30
44
  def datasource
31
- if @model['datasource'].is_a?(Hash)
32
- dashboard.grafana.datasource_by_uid(@model['datasource']['uid'])
45
+ if datasource_kind_is_uid?
46
+ dashboard.grafana.datasource_by_uid(@datasource_uid_or_name)
33
47
  else
34
- dashboard.grafana.datasource_by_name(@model['datasource'])
48
+ dashboard.grafana.datasource_by_name(@datasource_uid_or_name)
35
49
  end
36
50
  end
37
51
 
@@ -47,5 +61,14 @@ module Grafana
47
61
  def render_url
48
62
  "/render/d-solo/#{@dashboard.id}?panelId=#{@model['id']}"
49
63
  end
64
+
65
+ private
66
+
67
+ def datasource_kind_is_uid?
68
+ if @model['datasource'].is_a?(Hash)
69
+ return true
70
+ end
71
+ false
72
+ end
50
73
  end
51
74
  end
@@ -24,21 +24,15 @@ module Grafana
24
24
  interval = interval.raw_value if interval.is_a?(Variable)
25
25
  query = query_hash[:query] || query_description[:raw_query]
26
26
 
27
- url = if instant
28
- "/api/datasources/proxy/#{id}/api/v1/query?time=#{query_description[:to]}&query="\
29
- "#{CGI.escape(replace_variables(query, query_description[:variables]))}"
27
+ ver = query_description[:grafana_version].split('.').map{|x| x.to_i}
28
+ request = nil
29
+ if (ver[0] == 7 and ver[1] < 5) or ver[0] < 7
30
+ request = prepare_get_request({query_description: query_description, instant: instant, interval: interval, query: query})
30
31
  else
31
- "/api/datasources/proxy/#{id}/api/v1/query_range?start=#{query_description[:from]}"\
32
- "&end=#{query_description[:to]}"\
33
- "&query=#{CGI.escape(replace_variables(query, query_description[:variables]))}"\
34
- "&step=#{interval}"
32
+ request = prepare_post_request({query_description: query_description, instant: instant, interval: interval, query: query})
35
33
  end
36
34
 
37
- webrequest = query_description[:prepared_request]
38
- webrequest.relative_url = url
39
- webrequest.options.merge!({ request: Net::HTTP::Get })
40
-
41
- result = webrequest.execute(query_description[:timeout])
35
+ result = request.execute(query_description[:timeout])
42
36
  preformat_response(result.body)
43
37
  end
44
38
 
@@ -54,48 +48,80 @@ module Grafana
54
48
  end
55
49
 
56
50
  private
51
+ def prepare_get_request(hash)
52
+ url = if hash[:instant]
53
+ "/api/datasources/proxy/#{id}/api/v1/query?time=#{hash[:query_description][:to]}&query="\
54
+ "#{CGI.escape(replace_variables(hash[:query], hash[:query_description][:variables]))}"
55
+ else
56
+ "/api/datasources/proxy/#{id}/api/v1/query_range?start=#{hash[:query_description][:from]}"\
57
+ "&end=#{hash[:query_description][:to]}"\
58
+ "&query=#{CGI.escape(replace_variables(hash[:query], hash[:query_description][:variables]))}"\
59
+ "&step=#{hash[:interval]}"
60
+ end
61
+
62
+ webrequest = hash[:query_description][:prepared_request]
63
+ webrequest.relative_url = url
64
+ webrequest.options.merge!({ request: Net::HTTP::Get })
65
+
66
+ webrequest
67
+ end
68
+
69
+ def prepare_post_request(hash)
70
+ webrequest = hash[:query_description][:prepared_request]
71
+ webrequest.relative_url = '/api/ds/query'
72
+
73
+ params = {
74
+ from: hash[:query_description][:from],
75
+ to: hash[:query_description][:to],
76
+ queries: [{
77
+ datasource: { type: type, uid: uid },
78
+ datasourceId: id,
79
+ exemplar: false,
80
+ expr: hash[:query],
81
+ format: 'time_series',
82
+ interval: '',
83
+ # intervalFactor: ### 2,
84
+ # intervalMs: ### 15000,
85
+ # legendFormat: '', ### {{job}}
86
+ # maxDataPoints: 999,
87
+ metric: '',
88
+ queryType: 'timeSeriesQuery',
89
+ refId: 'A',
90
+ # requestId: '14A',
91
+ # utcOffsetSec: 7200,
92
+ step: hash[:interval]
93
+ }],
94
+ range: {
95
+ #from: ### "2022-07-31T16:19:26.198Z",
96
+ #to: ### "2022-07-31T16:19:26.198Z",
97
+ raw: { from: hash[:query_description][:variables]['from'].raw_value, to: hash[:query_description][:variables]['to'].raw_value }
98
+ }
99
+ }
100
+
101
+ webrequest.options.merge!({ request: Net::HTTP::Post, body: params.to_json })
102
+
103
+ webrequest
104
+ end
57
105
 
58
- # @see AbstractDatasource#preformat_response
59
106
  def preformat_response(response_body)
60
- json = {}
107
+ # TODO: show raw response body to debug case https://github.com/divinity666/ruby-grafana-reporter/issues/24
61
108
  begin
62
- json = JSON.parse(response_body)
109
+ return preformat_dataframe_response(response_body)
63
110
  rescue
64
- raise UnsupportedQueryResponseReceivedError, response_body
111
+ # TODO: show an info, that the response is not a dataframe
65
112
  end
66
113
 
114
+ json = JSON.parse(response_body)
115
+
67
116
  # handle response with error result
68
117
  unless json['error'].nil?
69
118
  return { header: ['error'], content: [[ json['error'] ]] }
70
119
  end
71
120
 
72
- # handle dataframes
73
- if json['results']
74
- data = json['results'].values.first
75
- raise UnsupportedQueryResponseReceivedError, response_body if data.nil?
76
- raise UnsupportedQueryResponseReceivedError, response_body if data['frames'].nil?
77
- # TODO: check how multiple frames have to be handled
78
-
79
- data = data['frames']
80
- headers = []
81
- data.first['schema']['fields'].each do |headline|
82
- header = headline['config']['displayNameFromDS'].nil? ? headline['name'] : headline['config']['displayNameFromDS']
83
- headers << header
84
- end
85
- content = data.first['data']['values'][0].zip(data.first['data']['values'][1])
86
- return { header: headers, content: content }
87
- end
88
-
89
121
  # handle former result formats
90
- raise UnsupportedQueryResponseReceivedError, response_body if json['data'].nil?
91
- raise UnsupportedQueryResponseReceivedError, response_body if json['data']['resultType'].nil?
92
- raise UnsupportedQueryResponseReceivedError, response_body if json['data']['result'].nil?
93
-
94
122
  result_type = json['data']['resultType']
95
123
  json = json['data']['result']
96
124
 
97
- raise UnsupportedQueryResponseReceivedError, response_body if not result_type =~ /^(?:scalar|string|vector|matrix)$/
98
-
99
125
  headers = ['time']
100
126
  content = {}
101
127
 
@@ -130,7 +156,10 @@ module Grafana
130
156
  end
131
157
  end
132
158
 
133
- { header: headers, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
159
+ return { header: headers, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
160
+
161
+ rescue
162
+ raise UnsupportedQueryResponseReceivedError, response_body
134
163
  end
135
164
  end
136
165
  end
@@ -47,6 +47,12 @@ module Grafana
47
47
  private
48
48
 
49
49
  def preformat_response(response_body)
50
+ begin
51
+ return preformat_dataframe_response(response_body)
52
+ rescue
53
+ # TODO: show an info, that the response if not a dataframe
54
+ end
55
+
50
56
  results = {}
51
57
  results.default = []
52
58
  results[:header] = []
@@ -64,13 +70,13 @@ module Grafana
64
70
  results[:content] = table['rows']
65
71
  end
66
72
  end
67
-
68
- else
69
- raise UnsupportedQueryResponseReceivedError, response_body
70
73
  end
71
74
  end
72
75
 
73
- results
76
+ return results
77
+
78
+ rescue
79
+ raise UnsupportedQueryResponseReceivedError, response_body
74
80
  end
75
81
  end
76
82
  end
@@ -240,7 +240,7 @@ module Grafana
240
240
  if !@config['current'].nil?
241
241
  self.raw_value = @config['current']['value']
242
242
  else
243
- raise GrafanaError.new("Grafana variable with type '#{@config['type']}' and name '#{@config['name']}' could not be handled properly. Please raise a ticket.")
243
+ raise GrafanaError.new("Grafana variable with type '#{@config['type']}' and name '#{@config['name']}' cannot be handled properly by the reporter. Check your results and raise a ticket on github.")
244
244
  end
245
245
  end
246
246
  end
@@ -80,7 +80,8 @@ module GrafanaReporter
80
80
 
81
81
  begin
82
82
  @result = @datasource.request(from: from, to: to, raw_query: raw_query, variables: @variables,
83
- prepared_request: @grafana.prepare_request, timeout: timeout)
83
+ prepared_request: @grafana.prepare_request, timeout: timeout,
84
+ grafana_version: @grafana.version)
84
85
  rescue ::Grafana::GrafanaError
85
86
  # grafana errors will be directly passed through
86
87
  raise
@@ -88,7 +89,7 @@ module GrafanaReporter
88
89
  # grafana errors will be directly passed through
89
90
  raise
90
91
  rescue StandardError => e
91
- raise DatasourceRequestInternalError.new(@datasource, e.message)
92
+ raise DatasourceRequestInternalError.new(@datasource, "#{e.message}\n#{e.backtrace.join("\n")}")
92
93
  end
93
94
 
94
95
  raise DatasourceRequestInvalidReturnValueError.new(@datasource, @result) unless datasource_response_valid?
File without changes
@@ -5,7 +5,10 @@ module GrafanaReporter
5
5
  class QueryValueQuery < AbstractQuery
6
6
  # @see Grafana::AbstractQuery#pre_process
7
7
  def pre_process
8
- @datasource = @panel.datasource if @panel
8
+ if @panel
9
+ @panel.resolve_variable_datasource(@variables)
10
+ @datasource = @panel.datasource
11
+ end
9
12
 
10
13
  @variables['result_type'] ||= Variable.new('')
11
14
  end
@@ -18,17 +18,6 @@ require 'asciidoctor-pdf'
18
18
  require 'zip'
19
19
  require_relative 'VERSION'
20
20
 
21
- # TODO: add test for variable replacement for sql
22
- # TODO: check why value is All instead of $__all
23
- # TODO: check why single sql values are replaced including ticks, whereas grafana does not do so
24
-
25
- # TODO: add FAQ for fixing most common issues with the reporter
26
- # TODO: implement an easy function to document a whole dashboard at once with different presentations
27
- # TODO: add automated test against grafana playground before building a new release
28
- # TODO: allow registration of files to be defined in config file
29
- # TODO: append necessary variables on demo report creation for plain SQL queries, as they are lacking the grafana reference
30
- # TODO: make demo report more readable
31
-
32
21
  folders = [
33
22
  %w[grafana],
34
23
  %w[grafana_reporter logger],
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-grafana-reporter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.2
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Christian Kohlmeyer
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-03-22 00:00:00.000000000 Z
11
+ date: 2022-08-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '1.6'
33
+ version: '2.2'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '1.6'
40
+ version: '2.2'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rubyzip
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -178,7 +178,7 @@ licenses:
178
178
  metadata:
179
179
  source_code_uri: https://github.com/divinity666/ruby-grafana-reporter
180
180
  bug_tracker_uri: https://github.com/divinity666/ruby-grafana-reporter/issues
181
- post_install_message:
181
+ post_install_message:
182
182
  rdoc_options: []
183
183
  require_paths:
184
184
  - lib
@@ -186,7 +186,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
186
186
  requirements:
187
187
  - - ">="
188
188
  - !ruby/object:Gem::Version
189
- version: '2.5'
189
+ version: '2.7'
190
190
  required_rubygems_version: !ruby/object:Gem::Requirement
191
191
  requirements:
192
192
  - - ">="
@@ -194,7 +194,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
194
194
  version: '0'
195
195
  requirements: []
196
196
  rubygems_version: 3.2.5
197
- signing_key:
197
+ signing_key:
198
198
  specification_version: 4
199
199
  summary: Reporter Service for Grafana
200
200
  test_files: []