ruby-grafana-reporter 0.6.3 → 0.7.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: e06395c66d098c01d5248872f7aac2bc8dd36c4f9c6f02774ffe022fd0cafc9f
4
- data.tar.gz: 6ad05dfac340497fb0be61b14b6420a348b05dbd71b14e17b55553573cfcf3b8
3
+ metadata.gz: 49639b5da410d46b0ad6cf16a187a3f30389fac610497de09d1e1466a66f22f5
4
+ data.tar.gz: 673791b6d38cd46e54292903f07e0fdd16070f5e0abf999565fecf04b930efbb
5
5
  SHA512:
6
- metadata.gz: 769a0c9258678b32100b21e38782ca668467ad2e016e63e3bc293d96311bb06d74b0b2a71f4033a576c71c94185e072ad37ac55d8d4cdc13a516a92f6c359895
7
- data.tar.gz: 53a645d2db1eac77ed0e6977932b2947eb41efccf33f49f77db6c7ea4254ebd2f0c299fb1211546356734b744ca6359f36f392ae3afcf4243c835da3c7671bfa
6
+ metadata.gz: d4bb03253d7681c580c96f11e5e86112e036087b927ff01e0e490bebeb4aeadd5f213f9de7202007523b8bf384ed36f6fcc040d5b81aa5aceef88c7cb191ab50
7
+ data.tar.gz: d3695d4d6ce451c01e7ad33a28a942f697d019215cca603827bda44af488e292c4befaf658d266e19b5ca3e84ab1a06cbc5f4f5e189d262b5c8bb415c4ae1013
data/README.md CHANGED
@@ -20,7 +20,20 @@ Reporting Service for Grafana
20
20
  * [Using webhooks](#using-webhooks)
21
21
  * [Developing your own plugin](#developing-your-own-plugin)
22
22
  * [Roadmap](#roadmap)
23
- * [Donations](#donations)
23
+ * [Contributing](#contributing)
24
+ * [Licensing](#licensing)
25
+ * [Acknowledgements](#acknowledgements)
26
+
27
+ ## Your support is appreciated!
28
+
29
+ Hey there! I provide you this software free of charge. I have already
30
+ spend a lot of my private time in developing, maintaining and supporting it.
31
+
32
+ If you enjoy my work, feel free to
33
+
34
+ [![buymeacoffee](https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0)](https://ko-fi.com/divinity666)
35
+
36
+ Thanks for your support and keeping this project alive!
24
37
 
25
38
  ## About the project
26
39
 
@@ -35,6 +48,10 @@ dashboards and to use it in your custom templates to finally create reports in P
35
48
 
36
49
  By default (an extended version of) Asciidoctor is enabled as template language.
37
50
 
51
+ ## Getting started
52
+
53
+ ![GettingStarted](./assets/GettingStartedAnimation.gif)
54
+
38
55
  ## Features
39
56
 
40
57
  * Supports creation of reports for multiple [grafana](https://github.com/grafana/grafana)
@@ -136,8 +153,10 @@ asciidoctor:
136
153
 
137
154
  ### Grafana integration
138
155
 
139
- For using the reporter directly from grafana, you need to simply add a link to your
140
- grafana dashboard:
156
+ For using the reporter directly from grafana, the reporter has to run as webservice, i.e. it has to be
157
+ called without the `-t` parameter.
158
+
159
+ If this is the case, you simply simply need add a link to your grafana dashboard:
141
160
 
142
161
  * Open the dashboard configuration
143
162
  * Select `Links`
@@ -168,7 +187,32 @@ a variable and forward it to the reporter.
168
187
 
169
188
  ## Advanced information
170
189
 
171
- ### Webservice
190
+ ### Use grafana variables in templates
191
+ It is common practice to use dashboard variables in grafana, to allow users to show
192
+ the dashboard for a specific set of data only. This is where grafana variables are
193
+ used.
194
+
195
+ Those variables are then also used in panel queries, to react on selecting or entering
196
+ those variables.
197
+
198
+ You may provide those variables during report generation to the reporter. Therefore
199
+ you have to specify them in the individual calls.
200
+
201
+ 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
203
+ calls:
204
+
205
+ ````
206
+ grafana_panel_image:1[var-serverid=main-server]
207
+ grafana_panel_image:1[var-serverid=replica-server]
208
+ ````
209
+
210
+ This will render two images: one for `main-server` and one for `replica-server`.
211
+
212
+ So, to forward grafana variables to the reporter calls, you simply have to use the
213
+ form `var-<<your-variable-name>>` and specify those in your reporter template.
214
+
215
+ ### Webservice endpoints
172
216
 
173
217
  Running the reporter as a webservice provides the following URLs
174
218
 
@@ -329,9 +373,3 @@ The code in this project is licensed under MIT license.
329
373
  * [grafana](https://github.com/grafana/grafana)
330
374
 
331
375
  Inspired by [Izak Marai's grafana reporter](https://github.com/IzakMarais/reporter)
332
-
333
- ## Donations
334
-
335
- If you like this project and you would like to support my work, feel free to donate. :)
336
-
337
- [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate?hosted_button_id=35LH6JNLPHPHQ)
@@ -2,4 +2,4 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  require_relative '../lib/ruby_grafana_reporter'
5
- GrafanaReporter::Application::Application.new.configure_and_run(ARGV) unless defined?(Ocra)
5
+ GrafanaReporter::Application::Application.new.configure_and_run(ARGV) unless defined?(Ocran)
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, 6, 3].freeze
4
+ GRAFANA_REPORTER_VERSION = [0, 7, 0].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2023-01-08'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2024-05-19'
@@ -68,6 +68,23 @@ module Grafana
68
68
  'NON-Admin'
69
69
  end
70
70
 
71
+ # Returns the datasource, which has been queried by model entry in the panel model.
72
+ #
73
+ # @param model_entry [Object] model entry of the searched datasource (e.g. String or Hash)
74
+ # @return [Datasource] Datasource for the specified datasource model entry
75
+ def datasource_by_model_entry(model_entry)
76
+ datasource = nil
77
+ if model_entry.is_a?(String)
78
+ datasource = datasource_by_name(model_entry)
79
+ elsif model_entry.is_a?(Hash)
80
+ datasource = datasource_by_uid(model_entry['uid'])
81
+ end
82
+
83
+ raise DatasourceDoesNotExistError.new('model entry', model_entry) unless datasource
84
+
85
+ datasource
86
+ end
87
+
71
88
  # Returns the datasource, which has been queried by the datasource name.
72
89
  #
73
90
  # @param datasource_name [String] name of the searched datasource
@@ -86,6 +103,8 @@ module Grafana
86
103
  # @param datasource_uid [String] unique id of the searched datasource
87
104
  # @return [Datasource] Datasource for the specified datasource unique id
88
105
  def datasource_by_uid(datasource_uid)
106
+ raise DatasourceDoesNotExistError.new('uid', datasource_uid) unless datasource_uid
107
+
89
108
  clean_nil_datasources
90
109
  datasource = @datasources.select { |ds_name, ds| ds.uid == datasource_uid }.values.first
91
110
  raise DatasourceDoesNotExistError.new('uid', datasource_uid) unless datasource
@@ -16,7 +16,7 @@ module Grafana
16
16
  webrequest.relative_url = panel.render_url + url_params(query_description)
17
17
  webrequest.options.merge!({ accept: 'image/png' })
18
18
 
19
- result = webrequest.execute
19
+ result = webrequest.execute(query_description[:timeout])
20
20
 
21
21
  raise ImageCouldNotBeRenderedError, panel if result.body.include?('<html')
22
22
 
@@ -26,7 +26,7 @@ module Grafana
26
26
  private
27
27
 
28
28
  def url_params(query_desc)
29
- url_vars = query_desc[:variables].select { |k, _v| k =~ /^(?:timeout|height|width|theme|fullscreen|var-.+)$/ }
29
+ url_vars = query_desc[:variables].select { |k, _v| k =~ /^(?:timeout|scale|height|width|theme|fullscreen|var-.+)$/ }
30
30
  url_vars = default_vars.merge(url_vars)
31
31
  url_vars['from'] = Variable.new(query_desc[:from])
32
32
  url_vars['to'] = Variable.new(query_desc[:to])
@@ -31,11 +31,36 @@ module Grafana
31
31
  query = query.gsub(/\$(?:__)?interval(?=\W|$)/, "#{interval.is_a?(String) ? interval : "#{(interval / 1000).to_i}s"}")
32
32
  query = query.gsub(/\$(?:__)?interval_ms(?=\W|$)/, "#{interval}")
33
33
 
34
- url = "/api/datasources/proxy/#{id}/query?db=#{@model['database']}&q=#{ERB::Util.url_encode(query)}&epoch=ms"
35
-
36
34
  webrequest = query_description[:prepared_request]
37
- webrequest.relative_url = url
38
- webrequest.options.merge!({ request: Net::HTTP::Get })
35
+ request = {}
36
+
37
+ ver = query_description[:grafana_version].split('.').map{|x| x.to_i}
38
+ if ver[0] >= 8
39
+ webrequest.relative_url = "/api/ds/query?ds_type=influxdb"
40
+
41
+ request = {
42
+ request: Net::HTTP::Post,
43
+ body: {
44
+ from: query_description[:from],
45
+ to: query_description[:to],
46
+ queries: [
47
+ {
48
+ datasource: {type: "influxdb"},
49
+ datasourceId: id,
50
+ intervalMs: interval,
51
+ query: query
52
+ }
53
+ ]}.to_json
54
+ }
55
+ else
56
+ webrequest.relative_url = "/api/datasources/proxy/#{id}/query?db=#{@model['database']}&q=#{ERB::Util.url_encode(query)}&epoch=ms"
57
+ request = {
58
+ request: Net::HTTP::Get
59
+ }
60
+ end
61
+
62
+ webrequest.options.merge!(request)
63
+
39
64
 
40
65
  result = webrequest.execute(query_description[:timeout])
41
66
  preformat_response(result.body)
@@ -43,7 +68,7 @@ module Grafana
43
68
 
44
69
  # @see AbstractDatasource#raw_query_from_panel_model
45
70
  def raw_query_from_panel_model(panel_query_target)
46
- return panel_query_target['query'] if panel_query_target['rawQuery']
71
+ return panel_query_target['query'] if panel_query_target['query'] or panel_query_target['rawQuery']
47
72
 
48
73
  # build composed queries
49
74
  build_select(panel_query_target['select']) + build_from(panel_query_target) + build_where(panel_query_target['tags']) + build_group_by(panel_query_target['groupBy'])
@@ -383,7 +383,7 @@ module GrafanaReporter
383
383
  delta_match = date_spec.match(/^(?<op>(?:-|\+))(?<count>\d+)?(?<unit>[smhdwMy])/)
384
384
  if delta_match
385
385
  date = delta_date(date, "#{delta_match[:op]}#{delta_match[:count] || 1}".to_i, delta_match[:unit])
386
- date_spec = date_spec.gsub(/^#{delta_match[:op]}#{delta_match[:count]}#{delta_match[:unit]}/, '')
386
+ date_spec = date_spec.gsub(/^#{delta_match[:op] == '+' ? '\+' : '-'}#{delta_match[:count]}#{delta_match[:unit]}/, '')
387
387
  end
388
388
 
389
389
  raise TimeRangeUnknownError, orig_date unless fit_match || delta_match
@@ -138,7 +138,7 @@ module GrafanaReporter
138
138
 
139
139
  # automatically add extension, if a file with default template extension exists
140
140
  @template = "#{@template}.#{self.class.default_template_extension}" if File.file?("#{@template}.#{self.class.default_template_extension}") && !File.file?(@template.to_s)
141
- raise MissingTemplateError, @template.to_s unless File.file?(@template.to_s)
141
+ raise MissingTemplateError, "#{@template}.#{self.class.default_template_extension}" unless File.file?(@template.to_s)
142
142
 
143
143
  notify(:on_before_create)
144
144
  @start_time = Time.new
@@ -34,8 +34,8 @@ module GrafanaReporter
34
34
  action_wizard = false
35
35
 
36
36
  parser = OptionParser.new do |opts|
37
- opts.banner = if ENV['OCRA_EXECUTABLE']
38
- "Usage: #{ENV['OCRA_EXECUTABLE'].gsub("#{Dir.pwd}/".gsub('/', '\\'), '')} [options]"
37
+ opts.banner = if ENV['OCRAN_EXECUTABLE']
38
+ "Usage: #{ENV['OCRAN_EXECUTABLE'].gsub("#{Dir.pwd}/".gsub('/', '\\'), '')} [options]"
39
39
  else
40
40
  "Usage: #{Gem.ruby} #{$PROGRAM_NAME} [options]"
41
41
  end
@@ -229,6 +229,12 @@ end}
229
229
  escaped by using `_,`. Execution of related functions is applied in the following order
230
230
  `format`, `replace_values`, `filter_columns`, `transpose`.
231
231
 
232
+ select_value:
233
+ call: select_value="<select_value>"
234
+ description: >-
235
+ Allows the selection of a specific value from the result set. Supported options are `min`, `max`, `avg`,
236
+ `sum`, `first`, `last`.
237
+
232
238
  transpose:
233
239
  call: transpose="true"
234
240
  description: >-
@@ -391,6 +397,9 @@ end}
391
397
  render-width:
392
398
  description: can be used to override default `width` in which the panel shall be rendered
393
399
  call: render-width="<width>"
400
+ render-scale:
401
+ description: can be used to override default scale in which the panel shall be rendered
402
+ call: render-scale="<scale>"
394
403
  render-theme:
395
404
  description: can be used to override default `theme` in which the panel shall be rendered (light by default)
396
405
  call: render-theme="<theme>"
@@ -453,6 +462,7 @@ end}
453
462
  from:
454
463
  instance:
455
464
  replace_values:
465
+ select_value:
456
466
  timeout:
457
467
  to:
458
468
  from_timezone:
@@ -502,6 +512,7 @@ end}
502
512
  from:
503
513
  instance:
504
514
  replace_values:
515
+ select_value:
505
516
  timeout:
506
517
  to:
507
518
  from_timezone:
@@ -80,7 +80,6 @@ module GrafanaReporter
80
80
  # @see ProcessorMixin#build_demo_entry
81
81
  def build_demo_entry(panel)
82
82
  return nil unless panel
83
- return nil unless panel.model['type'].include?('table')
84
83
 
85
84
  ref_id = nil
86
85
  panel.model['targets'].each do |item|
@@ -74,7 +74,6 @@ 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['type'] == 'singlestat'
78
77
 
79
78
  ref_id = nil
80
79
  panel.model['targets'].each do |item|
@@ -40,7 +40,7 @@ module GrafanaReporter
40
40
  k =~ /^(?:timeout|from|to)$/ ||
41
41
  k =~ /filter_columns|format|replace_values_.*|transpose|from_timezone|
42
42
  to_timezone|result_type|query|table_formatter|include_headline|
43
- column_divider|row_divider|instant|interval|verbose_log/x
43
+ column_divider|row_divider|instant|interval|verbose_log|select_value/x
44
44
  end)
45
45
 
46
46
  result
@@ -74,7 +74,6 @@ 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['type'].include?('table')
78
77
 
79
78
  ref_id = nil
80
79
  panel.model['targets'].each do |item|
@@ -85,8 +84,8 @@ module GrafanaReporter
85
84
  end
86
85
  return nil unless ref_id
87
86
 
88
- "|===\ninclude::grafana_sql_table:#{panel.dashboard.grafana.datasource_by_name(panel.model['datasource']).id}"\
89
- "[sql=\"#{panel.query(ref_id).gsub(/"/, '\"').gsub("\n", ' ').gsub(/\\/, '\\\\')}\",filter_columns=\"time\","\
87
+ "|===\ninclude::grafana_sql_table:#{panel.dashboard.grafana.datasource_by_model_entry(panel.model['datasource']).id}"\
88
+ "[sql=\"#{panel.query(ref_id).gsub(/"/, '\"').gsub("\r\n", ' ').gsub("\n", ' ').gsub(/\\/, '\\\\')}\",filter_columns=\"time\","\
90
89
  "dashboard=\"#{panel.dashboard.id}\",from=\"now-1h\",to=\"now\"]\n|==="
91
90
  end
92
91
  end
@@ -79,7 +79,6 @@ 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['type'] == 'singlestat'
83
82
 
84
83
  ref_id = nil
85
84
  panel.model['targets'].each do |item|
@@ -90,8 +89,8 @@ module GrafanaReporter
90
89
  end
91
90
  return nil unless ref_id
92
91
 
93
- "grafana_sql_value:#{panel.dashboard.grafana.datasource_by_name(panel.model['datasource']).id}"\
94
- "[sql=\"#{panel.query(ref_id).gsub(/"/, '\"').gsub("\n", ' ').gsub(/\\/, '\\\\')}\",from=\"now-1h\","\
92
+ "grafana_sql_value:#{panel.dashboard.grafana.datasource_by_model_entry(panel.model['datasource']).id}"\
93
+ "[sql=\"#{panel.query(ref_id).gsub(/"/, '\"').gsub("\r\n", ' ').gsub("\n", ' ').gsub(/\\/, '\\\\')}\",from=\"now-1h\","\
95
94
  'to="now"]'
96
95
  end
97
96
  end
@@ -32,7 +32,7 @@ module GrafanaReporter
32
32
  demo_report ||= '<<your_report_name>>'
33
33
  config_param = config_file == Configuration::DEFAULT_CONFIG_FILE_NAME ? '' : " -c #{config_file}"
34
34
  program_call = "#{Gem.ruby} #{$PROGRAM_NAME}"
35
- program_call = ENV['OCRA_EXECUTABLE'].gsub("#{Dir.pwd}/".gsub('/', '\\'), '') if ENV['OCRA_EXECUTABLE']
35
+ program_call = ENV['OCRAN_EXECUTABLE'].gsub("#{Dir.pwd}/".gsub('/', '\\'), '') if ENV['OCRAN_EXECUTABLE']
36
36
 
37
37
  puts
38
38
  puts 'Now everything is setup properly. Create your reports as required in the templates '\
@@ -50,7 +50,7 @@ module GrafanaReporter
50
50
  # Thrown if a non existing template has been specified.
51
51
  class MissingTemplateError < ConfigurationError
52
52
  def initialize(template)
53
- super("Given report template '#{template}' is not a valid template.")
53
+ super("Accessing report template file '#{template}' is not possible. Check if file exists and is accessible.")
54
54
  end
55
55
  end
56
56
 
@@ -79,6 +79,15 @@ module GrafanaReporter
79
79
  end
80
80
  end
81
81
 
82
+ # Thrown, if the value configuration in {QueryValueQuery#select_value} is
83
+ # invalid.
84
+ class UnsupportedSelectValueStatementError < GrafanaReporterError
85
+ def initialize(statement)
86
+ super("Unsupported 'select_value' specified in template file: '#{statement}'. Supported values are 'min', 'max', "\
87
+ "'avg', 'sum', 'first', 'last'.")
88
+ end
89
+ end
90
+
82
91
  # Thrown, if a configured parameter is malformed.
83
92
  class MalformedAttributeContentError < GrafanaReporterError
84
93
  def initialize(message, attribute, content)
@@ -33,7 +33,30 @@ module GrafanaReporter
33
33
 
34
34
  when /(?:panel_value|sql_value)/
35
35
  tmp = @result[:content] || []
36
- @result = tmp.flatten.first
36
+ # use only first column of return values and replace null values with zero
37
+ tmp = tmp.map{ |item| item[0] || 0 }
38
+
39
+ # as default behaviour we fallback to the first_value, as this was the default in older releases
40
+ select_value = 'first'
41
+ select_value = @variables['select_value'].raw_value if @variables['select_value']
42
+ case select_value
43
+ when 'min'
44
+ result = tmp.min
45
+ when 'max'
46
+ result = tmp.max
47
+ when 'avg'
48
+ result = tmp.size > 0 ? tmp.sum / tmp.size : 0
49
+ when 'sum'
50
+ result = tmp.sum
51
+ when 'last'
52
+ result = tmp.last
53
+ when 'first'
54
+ result = tmp.first
55
+ else
56
+ raise UnsupportedSelectValueStatementError, @variables['select_value'].raw_value
57
+ end
58
+
59
+ @result = result
37
60
 
38
61
  else
39
62
  raise StandardError, "Unsupported 'result_type' received: '#{@variables['result_type'].raw_value}'"
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'rubygems'
4
+ require 'rubygems/ext'
3
5
  require 'net/http'
4
6
  require 'fileutils'
5
7
  require 'yaml'
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.6.3
4
+ version: 0.7.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: 2023-01-08 00:00:00.000000000 Z
11
+ date: 2024-05-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor
@@ -206,7 +206,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
206
206
  - !ruby/object:Gem::Version
207
207
  version: '0'
208
208
  requirements: []
209
- rubygems_version: 3.1.4
209
+ rubygems_version: 3.1.6
210
210
  signing_key:
211
211
  specification_version: 4
212
212
  summary: Reporter Service for Grafana