ruby-grafana-reporter 0.8.0 → 0.9.4

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: b3d762755e84f775f00d04fe0333389ae011fbf0ed1b13942cd006599e60e0e2
4
- data.tar.gz: 29c72d42855e2de7ecd43cbced9d6339c36312deebe05fc5210b80928e0d7894
3
+ metadata.gz: 57a3edcb1aa6c41ed2a26688dc6a8a80fba3b9169aa79ee061eeb2d2c0df1cb9
4
+ data.tar.gz: 897ae84eb2672b645528e118ff7107e2fab7a4f334f63a4c87bb29a020cf93a8
5
5
  SHA512:
6
- metadata.gz: 2efa68036513d9d7c73ad13296e369bd0c945bf97dd5101f288b3c646443bbc95af0d96f876ab1c23d1424d2f450c1618d24e58705ecbfd406b31adfdd1c7871
7
- data.tar.gz: b37f3d569f59a2ae22bea51894cde019ca395c253a4c62d83cbbfd9cd692010e327996c98fb1f0711f8f4ba396cd1eda35b203ebec7c92d95a855218aeb09fdd
6
+ metadata.gz: b3889cace3794d62bfd50978a24c4eb39cda1148398a4bed8c4354d5c51ad339faa7da7a81b4f001cb29c383ac98334494520a9cbeec2b44019dcafae883bfdb
7
+ data.tar.gz: 9a7fbf743f0e48e114deaa1d518ba993ea0318229ab391d90481bad43f859b20fca9f8ffaafe6799729a81ace22673d4904797963c776cf9a22c942289f35c23
data/README.md CHANGED
@@ -17,26 +17,17 @@ Reporting Service for Grafana
17
17
  * [Installation](#installation)
18
18
  * [Grafana integration](#grafana-integration)
19
19
  * [Advanced information](#advanced-information)
20
- * [Webservice](#webservice)
20
+ * [Use grafana variables in templates](#use-grafana-variables-in-templates)
21
+ * [Webservice endpoints](#webservice-endpoints)
22
+ * [API endpoints](#api-endpoints)
21
23
  * [Using ERB templates](#using-erb-templates)
22
24
  * [Using webhooks](#using-webhooks)
23
- * [Developing your own plugin](#developing-your-own-plugin)
25
+ * [Developing your own datasource plugin](#developing-your-own-datasource-plugin)
24
26
  * [Roadmap](#roadmap)
25
27
  * [Contributing](#contributing)
26
28
  * [Licensing](#licensing)
27
29
  * [Acknowledgements](#acknowledgements)
28
30
 
29
- ## Your support is appreciated!
30
-
31
- Hey there! I provide you this software free of charge. I have already
32
- spend a lot of my private time in developing, maintaining and supporting it.
33
-
34
- If you enjoy my work, feel free to
35
-
36
- [![buymeacoffee](https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0)](https://ko-fi.com/divinity666)
37
-
38
- Thanks for your support and keeping this project alive!
39
-
40
31
  ## About the project
41
32
 
42
33
  [Grafana](https://github.com/grafana/grafana) is a great tool for monitoring and
@@ -62,11 +53,12 @@ By default (an extended version of) Asciidoctor is enabled as template language.
62
53
 
63
54
  ### Features
64
55
 
65
- * Supports creation of reports for multiple [grafana](https://github.com/grafana/grafana)
66
- dashboards (and also multiple grafana installations!) in one resulting report
67
- * PDF (default), HTML and many other report formats are supported
68
- * Easy-to-use configuration wizard, including fully automated functionality to create a
69
- demo report for your dashboard
56
+ * Use Asciidoctor or ERB template language for defining templates
57
+ * Render your templates as PDF (default), HTML, CSV, text files and any other format
58
+ you can think of
59
+ * Create reports containing data from multiple [grafana](https://github.com/grafana/grafana)
60
+ dashboards or even multiple grafana installations
61
+ * Easy-to-use configuration wizard including automated demo report creation
70
62
  * Include dynamic content from grafana (find here a reference for all
71
63
  [asciidcotor reporter calls](FUNCTION_CALLS.md)):
72
64
  * panels as images
@@ -77,8 +69,7 @@ database queries
77
69
  * webservice to be called directly from grafana
78
70
  * standalone command line tool, e.g. to be automated with `cron` or `bash` scrips
79
71
  * microservice from standard asciidoctor docker container without any dependencies
80
- * Use webhook callbacks on before, on cancel and on finishing a report (see
81
- configuration file) to combine them with your services
72
+ * Integrate it in your toolchain with webhooks and API calls
82
73
  * Solid as a rock - no matter if you do mistakes in your configuration or grafana does no
83
74
  longer match templates: the ruby-grafana-reporter webservice will always return properly.
84
75
  * Full [API documentation](https://rubydoc.info/gems/ruby-grafana-reporter) available
@@ -234,6 +225,21 @@ The main endpoint to call for report generation is configured in the previous ch
234
225
 
235
226
  However, if you would like to see, currently running report generations and previously generated reports, you may want to call the endpoint `/overview`.
236
227
 
228
+ ### API endpoints
229
+
230
+ If you want to call the reporter programatically, you might want to get machine
231
+ usable endpoints. Therefore the following API endpoints are available:
232
+
233
+ POST /api/v1/render
234
+ GET /api/v1/status
235
+ DELETE /api/v1/cancel
236
+
237
+ Those endpoints have to be called with the same URL parameters, as the webservice
238
+ endpoints.
239
+
240
+ If you want to receive the report itself, you'll have to use the `/view_report`
241
+ call from the webservice endpoints.
242
+
237
243
  ### Using ERB templates
238
244
 
239
245
  By default the configuration wizard will setup the reporter with the asciidoctor
@@ -280,6 +286,12 @@ grafana-reporter:
280
286
  callbacks:
281
287
  all:
282
288
  - http://<<your_callback_url>>
289
+ on_before_create:
290
+ - http://<<your_callback_url>>
291
+ on_after_cancel:
292
+ - http://<<your_callback_url>>
293
+ on_after_finish:
294
+ - http://<<your_callback_url>>
283
295
  ````
284
296
 
285
297
  Remember to restart the reporter, if it is running as a webservice.
@@ -288,7 +300,7 @@ After having done so, your callback url will be called for each event with
288
300
  a JSON body including all necessary information of the report. For details see
289
301
  [callback](https://rubydoc.info/gems/ruby-grafana-reporter/GrafanaReporter/ReportWebhook#callback-instance_method).
290
302
 
291
- ### Developing your own plugin
303
+ ### Developing your own datasource plugin
292
304
 
293
305
  The reporter is designed to allow easy integration of your own plugins,
294
306
  without having to modify the reporter base source on github (or anywhere
@@ -372,6 +384,12 @@ This is just a collection of things, I am heading for in future, without a sched
372
384
  If you'd like to contribute, please fork the repository and use a feature
373
385
  branch. Pull requests are warmly welcome.
374
386
 
387
+ If you enjoy my work, and can't support with code contributions, feel free to
388
+
389
+ [![buymeacoffee](https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0)](https://ko-fi.com/divinity666)
390
+
391
+ Thanks for your support and keeping this project alive!
392
+
375
393
  ## Licensing
376
394
 
377
395
  The code in this project is licensed under MIT license.
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, 8, 0].freeze
4
+ GRAFANA_REPORTER_VERSION = [0, 9, 4].freeze
5
5
  # Release date
6
- GRAFANA_REPORTER_RELEASE_DATE = '2024-06-08'
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2024-08-22'
@@ -62,8 +62,7 @@ module Grafana
62
62
  begin
63
63
  @variables << Variable.new(item, self)
64
64
  rescue => e
65
- # TODO: show this message as a warning - needs test cleanup
66
- @grafana.logger.debug(e.message)
65
+ @grafana.logger.warn(e.message)
67
66
  end
68
67
  end
69
68
  end
@@ -23,6 +23,7 @@ module Grafana
23
23
 
24
24
  result = webrequest.execute(query_description[:timeout])
25
25
  return unless result
26
+ return result.body unless result.code.to_s == "200"
26
27
 
27
28
  json = JSON.parse(result.body)
28
29
 
@@ -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']}' cannot be handled properly by the reporter. Check your results and raise a ticket on github.")
243
+ raise GrafanaError.new("Dashboard variable with type '#{@config['type']}' and name '#{@config['name']}' cannot be handled properly by the reporter in queries. Check your resulting report and raise a ticket on github if you face issues.")
244
244
  end
245
245
  end
246
246
  end
@@ -134,6 +134,7 @@ module GrafanaReporter
134
134
  begin
135
135
  template_ext = config.report_class.default_template_extension
136
136
  report_ext = config.report_class.default_result_extension
137
+ report_ext = 'zip' if config.default_document_attributes["convert-backend"] != "pdf" and not config.default_document_attributes["convert-backend"].nil?
137
138
  default_to_file = File.basename(config.template.to_s.gsub(/(?:\.#{template_ext})?$/, ".#{report_ext}"))
138
139
 
139
140
  to_file = config.to_file
@@ -144,27 +144,56 @@ module GrafanaReporter
144
144
  attrs[k] = v.length == 1 ? v[0] : v
145
145
  end
146
146
 
147
- case request.split("\r\n")[0]
148
- when %r{^GET /render[? ]}
149
- return render_report(attrs)
147
+ parsed_url = request.split("\r\n")[0].match(%r{(?<verb>GET|POST|DELETE) (?<subpath>/.*?)(?:api/v1/(?<api_action>render|status|cancel))?(?<html_action>render|overview|view_report|cancel_report|view_log)?[ ?]})
148
+ parsed_url = {} if not parsed_url
150
149
 
151
- when %r{^GET /overview[? ]}
150
+ case parsed_url
151
+ # API calls
152
+ when -> (h) { h['verb'] == 'POST' && h['api_action'] == 'render' }
153
+ return render_report(attrs, parsed_url['subpath'], true)
154
+
155
+ when -> (h) { h['verb'] == 'GET' && h['api_action'] == 'status' }
156
+ return report_status(attrs)
157
+
158
+ when -> (h) { h['verb'] == 'DELETE' && h['api_action'] == 'cancel' }
159
+ return cancel_report(attrs, parsed_url['subpath'], true)
160
+
161
+ # HTML calls
162
+ when -> (h) { h['verb'] == 'GET' && h['html_action'] == 'render' }
163
+ return render_report(attrs, parsed_url['subpath'])
164
+
165
+ when -> (h) { h['verb'] == 'GET' && h['html_action'] == 'overview' }
152
166
  # show overview for current reports
153
- return get_reports_status_as_html(@reports)
167
+ return get_reports_status_as_html(@reports, parsed_url['subpath'])
154
168
 
155
- when %r{^GET /view_report[? ]}
156
- return view_report(attrs)
169
+ when -> (h) { h['verb'] == 'GET' && h['html_action'] == 'view_report' }
170
+ return view_report(attrs, parsed_url['subpath'])
157
171
 
158
- when %r{^GET /cancel_report[? ]}
159
- return cancel_report(attrs)
172
+ when -> (h) { h['verb'] == 'GET' && h['html_action'] == 'cancel_report' }
173
+ return cancel_report(attrs, parsed_url['subpath'])
160
174
 
161
- when %r{^GET /view_log[? ]}
175
+ when -> (h) { h['verb'] == 'GET' && h['html_action'] == 'view_log' }
162
176
  return view_log(attrs)
163
177
  end
164
178
 
165
179
  raise WebserviceUnknownPathError, request.split("\r\n")[0]
166
180
  end
167
181
 
182
+ def report_status(attrs)
183
+ report = @reports.select { |r| r.object_id.to_s == attrs['report_id'].to_s }.first
184
+ raise WebserviceGeneralRenderingError, 'report_status has been called without valid id' if report.nil?
185
+
186
+ response = {
187
+ report_id: report.object_id,
188
+ progress: report.progress,
189
+ state: report.status,
190
+ done: report.done,
191
+ execution_time: report.execution_time
192
+ }
193
+
194
+ http_response(200, 'OK', JSON.generate(response), "Content-Type": "application/json")
195
+ end
196
+
168
197
  def view_log(attrs)
169
198
  # view report if already available, or show status view
170
199
  report = @reports.select { |r| r.object_id.to_s == attrs['report_id'].to_s }.first
@@ -175,7 +204,7 @@ module GrafanaReporter
175
204
  http_response(200, 'OK', content, "Content-Type": 'text/plain')
176
205
  end
177
206
 
178
- def cancel_report(attrs)
207
+ def cancel_report(attrs, subpath, as_json=false)
179
208
  # view report if already available, or show status view
180
209
  report = @reports.select { |r| r.object_id.to_s == attrs['report_id'].to_s }.first
181
210
  raise WebserviceGeneralRenderingError, 'cancel_report has been called without valid id' if report.nil?
@@ -183,28 +212,34 @@ module GrafanaReporter
183
212
  report.cancel! unless report.done
184
213
 
185
214
  # redirect to view_report page
186
- http_response(302, 'Found', nil, Location: "/view_report?report_id=#{report.object_id}")
215
+ return http_response(302, 'Found', nil, Location: "#{subpath}view_report?report_id=#{report.object_id}") if not as_json
216
+
217
+ http_response(200, 'OK', nil)
187
218
  end
188
219
 
189
- def view_report(attrs)
220
+ def view_report(attrs, subpath)
190
221
  # view report if already available, or show status view
191
222
  report = @reports.select { |r| r.object_id.to_s == attrs['report_id'].to_s }.first
192
223
  raise WebserviceGeneralRenderingError, 'view_report has been called without valid id' if report.nil?
193
224
 
194
225
  # show report status
195
- return get_reports_status_as_html([report]) if !report.done || !report.error.empty?
226
+ return get_reports_status_as_html([report], subpath) if !report.done || !report.error.empty?
196
227
 
197
228
  # provide report
198
- @logger.debug("Returning PDF report at #{report.path}")
229
+ @logger.debug("Returning report file #{report.path}")
199
230
  content = File.read(report.path, mode: 'rb')
200
231
  return http_response(200, 'OK', content, "Content-Type": 'application/pdf') if content.start_with?('%PDF')
201
232
 
233
+ return http_response(200, 'OK', content, "Content-Type": 'application/octet-stream',
234
+ "Content-Disposition": 'attachment; '\
235
+ "filename=report_#{attrs['report_id']}.zip") if content.start_with?('PK')
236
+
202
237
  http_response(200, 'OK', content, "Content-Type": 'application/octet-stream',
203
238
  "Content-Disposition": 'attachment; '\
204
- "filename=report_#{attrs['report_id']}.zip")
239
+ "filename=report_#{attrs['report_id']}.#{report.class.default_result_extension}")
205
240
  end
206
241
 
207
- def render_report(attrs)
242
+ def render_report(attrs, subpath, as_json=false)
208
243
  # build report
209
244
  template_file = "#{@config.templates_folder}#{attrs['var-template']}"
210
245
 
@@ -222,10 +257,13 @@ module GrafanaReporter
222
257
  end
223
258
  @reports << report
224
259
 
225
- http_response(302, 'Found', nil, Location: "/view_report?report_id=#{report.object_id}")
260
+ return http_response(302, 'Found', nil, Location: "#{subpath}view_report?report_id=#{report.object_id}") if not as_json
261
+
262
+ response = {report_id: report.object_id}
263
+ return http_response(200, 'OK', JSON.generate(response))
226
264
  end
227
265
 
228
- def get_reports_status_as_html(reports)
266
+ def get_reports_status_as_html(reports, subpath)
229
267
  i = reports.length
230
268
 
231
269
  # TODO: make reporter HTML results customizable
@@ -245,14 +283,14 @@ module GrafanaReporter
245
283
  <td><%= report.status %> (<%= (report.progress * 100).to_i %>%)</td>
246
284
  <td><%= report.error.join('<br>') %></td>
247
285
  <td><% if !report.done && !report.cancel %>
248
- <a href="/cancel_report?report_id=<%= report.object_id %>">Cancel</a>
286
+ <a href="<%= subpath %>cancel_report?report_id=<%= report.object_id %>">Cancel</a>
249
287
  <% end %>
250
288
  &nbsp;
251
289
  <% if (report.status == 'finished') || (report.status == 'cancelled') %>
252
- <a href="/view_report?report_id=<%= report.object_id %>">View</a>
290
+ <a href="<%= subpath %>view_report?report_id=<%= report.object_id %>">View</a>
253
291
  <% end %>
254
292
  &nbsp;
255
- <a href="/view_log?report_id=<%= report.object_id %>">Log</a></td></tr>
293
+ <a href="<%= subpath %>view_log?report_id=<%= report.object_id %>">Log</a></td></tr>
256
294
  <% end.join('') %>
257
295
  <tbody>
258
296
  </table>
@@ -267,8 +305,8 @@ module GrafanaReporter
267
305
  end
268
306
 
269
307
  def http_response(code, text, body, opts = {})
270
- "HTTP/1.1 #{code} #{text}\r\n#{opts.map { |k, v| "#{k}: #{v}" }.join("\r\n")}"\
271
- "#{body ? "\r\nContent-Length: #{body.to_s.bytesize}" : ''}\r\n\r\n#{body}"
308
+ "HTTP/1.1 #{code} #{text}\r\n#{opts.map { |k, v| "#{k}: #{v}" }.join("\r\n")}#{opts.length > 0 ? "\r\n" : ""}"\
309
+ "#{body ? "Content-Length: #{body.to_s.bytesize}" : ''}\r\n\r\n#{body}"
272
310
  end
273
311
  end
274
312
  end
@@ -80,6 +80,7 @@ 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['targets']
83
84
 
84
85
  ref_id = nil
85
86
  panel.model['targets'].each do |item|
@@ -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|
@@ -41,36 +41,29 @@ module GrafanaReporter
41
41
  ::Asciidoctor.convert_file(@template, extension_registry: registry, backend: attrs['convert-backend'],
42
42
  to_file: path, attributes: attrs, header_footer: true)
43
43
 
44
- # store report including als images as ZIP file, if the result is not a PDF
44
+ # store report including all images as ZIP file, if the result is not a PDF
45
45
  if attrs['convert-backend'] != 'pdf'
46
46
  # build zip file
47
- zip_file = Tempfile.new('gf_zip')
48
47
  buffer = Zip::OutputStream.write_buffer do |zipfile|
49
48
  # add report file
50
- zipfile.put_next_entry("#{path.gsub(@config.reports_folder, '')}.#{attrs['convert-backend']}")
51
- zipfile.write File.read(path)
49
+ zipfile.put_next_entry("#{path.gsub(@config.reports_folder, '').gsub(/\.[\w\d]+$/, '')}.#{attrs['convert-backend']}")
50
+ zipfile.write File.open(path, 'rb') { |f| f.read }
52
51
 
53
52
  # add image files
54
53
  @image_files.each do |file|
55
54
  zipfile.put_next_entry(file.path.gsub(@config.images_folder, ''))
56
- zipfile.write File.read(file.path)
55
+ zipfile.write File.open(file.path, 'rb') { |f| f.read }
57
56
  end
58
57
  end
59
- File.open(zip_file, 'wb') do |f|
60
- f.write buffer.string
61
- end
62
58
 
63
- # replace original file with zip file
64
- zip_file.rewind
59
+ # write zip file
65
60
  begin
66
- File.write(path, zip_file.read)
61
+ File.open(path, 'wb') do |f|
62
+ f.write buffer.string
63
+ end
67
64
  rescue StandardError => e
68
- logger.fatal("Could not overwrite report file '#{path}' with ZIP file. (#{e.message}).")
65
+ logger.fatal("Could not overwrite file '#{path}' with zipped file. (#{e.message}).")
69
66
  end
70
-
71
- # cleanup temporary zip file
72
- zip_file.close
73
- zip_file.unlink
74
67
  end
75
68
 
76
69
  clean_image_files
@@ -84,6 +84,8 @@ module GrafanaReporter
84
84
  end
85
85
  end
86
86
  return nil unless ref_id
87
+ # FIXME this filters out e.g. prometheus in demo reports, as the query method returns a Hash instead of a string
88
+ return nil unless panel.query(ref_id).is_a?(String)
87
89
 
88
90
  "|===\ninclude::grafana_sql_table:#{panel.dashboard.grafana.datasource_by_model_entry(panel.model['datasource']).id}"\
89
91
  "[sql=\"#{panel.query(ref_id).gsub(/"/, '\"').gsub("\r\n", ' ').gsub("\n", ' ').gsub(/\\/, '\\\\')}\",filter_columns=\"time\","\
@@ -89,6 +89,8 @@ module GrafanaReporter
89
89
  end
90
90
  end
91
91
  return nil unless ref_id
92
+ # FIXME this filters out e.g. prometheus in demo reports, as the query method returns a Hash instead of a string
93
+ return nil unless panel.query(ref_id).is_a?(String)
92
94
 
93
95
  "grafana_sql_value:#{panel.dashboard.grafana.datasource_by_model_entry(panel.model['datasource']).id}"\
94
96
  "[sql=\"#{panel.query(ref_id).gsub(/"/, '\"').gsub("\r\n", ' ').gsub("\n", ' ').gsub(/\\/, '\\\\')}\",from=\"now-1h\","\
@@ -319,8 +319,10 @@ default-document-attributes:
319
319
  end
320
320
 
321
321
  def user_input(text, default)
322
+ $stdout.sync = true
322
323
  print "#{text} [#{default}]: "
323
- input = gets.gsub(/\n$/, '')
324
+ $stdout.sync = false
325
+ input = gets.gsub(/[\n\r]*$/, '')
324
326
  input = default if input.empty?
325
327
  input
326
328
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'rubygems'
4
- require 'rubygems/name_tuple'
5
- require 'rubygems/ext'
3
+ require 'rubygems' # for OCRAN
6
4
  require 'net/http'
7
5
  require 'fileutils'
8
6
  require 'yaml'
@@ -16,7 +14,7 @@ require 'date'
16
14
  require 'time'
17
15
  require 'logger'
18
16
  require 'asciidoctor'
19
- require 'asciidoctor/extensions'
17
+ require 'asciidoctor/converter/html5' # for OCRAN
20
18
  require 'asciidoctor-pdf'
21
19
  require 'zip'
22
20
  require_relative 'VERSION'
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.8.0
4
+ version: 0.9.4
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-06-08 00:00:00.000000000 Z
11
+ date: 2024-08-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: asciidoctor