ruby-grafana-reporter 0.3.0 → 0.4.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -71
  3. data/bin/ruby-grafana-reporter +5 -5
  4. data/lib/VERSION.rb +3 -2
  5. data/lib/grafana/abstract_datasource.rb +116 -0
  6. data/lib/grafana/dashboard.rb +1 -3
  7. data/lib/grafana/errors.rb +15 -0
  8. data/lib/grafana/grafana.rb +53 -56
  9. data/lib/grafana/grafana_alerts_datasource.rb +57 -0
  10. data/lib/grafana/grafana_annotations_datasource.rb +56 -0
  11. data/lib/grafana/grafana_property_datasource.rb +25 -0
  12. data/lib/grafana/graphite_datasource.rb +44 -0
  13. data/lib/grafana/image_rendering_datasource.rb +44 -0
  14. data/lib/grafana/panel.rb +9 -3
  15. data/lib/grafana/prometheus_datasource.rb +39 -0
  16. data/lib/grafana/sql_datasource.rb +65 -0
  17. data/lib/grafana/variable.rb +1 -0
  18. data/lib/grafana/webrequest.rb +71 -0
  19. data/lib/grafana_reporter/abstract_query.rb +401 -0
  20. data/lib/grafana_reporter/abstract_report.rb +54 -3
  21. data/lib/grafana_reporter/alerts_table_query.rb +44 -0
  22. data/lib/grafana_reporter/annotations_table_query.rb +43 -0
  23. data/lib/grafana_reporter/application/application.rb +12 -8
  24. data/lib/grafana_reporter/application/webservice.rb +18 -6
  25. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +90 -0
  26. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +89 -0
  27. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +76 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +77 -0
  29. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +72 -0
  30. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +98 -0
  31. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
  32. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +23 -0
  33. data/lib/grafana_reporter/asciidoctor/report.rb +24 -31
  34. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  35. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  36. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +92 -0
  37. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +88 -0
  38. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  39. data/lib/grafana_reporter/configuration.rb +12 -6
  40. data/lib/grafana_reporter/console_configuration_wizard.rb +115 -65
  41. data/lib/grafana_reporter/demo_report_wizard.rb +87 -0
  42. data/lib/grafana_reporter/errors.rb +33 -0
  43. data/lib/grafana_reporter/help.rb +447 -0
  44. data/lib/grafana_reporter/logger/two_way_logger.rb +1 -1
  45. data/lib/grafana_reporter/panel_image_query.rb +29 -0
  46. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  47. data/lib/grafana_reporter/query_value_query.rb +79 -0
  48. data/lib/grafana_reporter/report_webhook.rb +35 -0
  49. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +0 -3
  50. metadata +37 -35
  51. data/lib/grafana/abstract_panel_query.rb +0 -22
  52. data/lib/grafana/abstract_query.rb +0 -132
  53. data/lib/grafana/abstract_sql_query.rb +0 -51
  54. data/lib/grafana/panel_image_query.rb +0 -52
  55. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -101
  56. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
  57. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -40
  58. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -92
  59. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -91
  60. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -69
  61. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -68
  62. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -61
  63. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -78
  64. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -73
  65. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -20
  66. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -43
  67. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -30
  68. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -70
  69. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -66
  70. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -88
  71. data/lib/grafana_reporter/asciidoctor/help.rb +0 -435
  72. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -36
  73. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -28
  74. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -44
  75. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -40
  76. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -312
  77. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -42
  78. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '00728deba4250ed327170393bbeaac1b1d6d1002463a5a243285ac570f09231f'
4
- data.tar.gz: 2985a11f6404e622ff279dbe3e328b38e4202bf665b2fef9e23746bbfd6c3459
3
+ metadata.gz: db7e3fbf0f8c1a3d7d9ce8d3e3e215b6f6bb5ec3c170252d575e0516b3312b20
4
+ data.tar.gz: b05cfa64a8d5bef650917df47ad9b84a140f302e87d2c6664c1fab3272e7212e
5
5
  SHA512:
6
- metadata.gz: 92de552918a451f9bac1196dfbb05dc1ad64e0059f4c90fe06c079e5e181fee96a07858ae0c5815e3dbf37358c4aa271fcd8d129bb6721f3c8a53f285325fc25
7
- data.tar.gz: aff95df6e4b5a8636905ec498433d1fcdae6d7b9aa2d26640815a68160c2f74c39078adfbe32a67c9115855ddd97683c6844ea0347e18cf010586e2bd036faed
6
+ metadata.gz: 3925cc108c3cedc51d6a37926b4ba937296cfd55137d8af40b9254bb92383cf253713321e95e6cfcfeb50de9adf7acd11777cf108227527d2c87658d74173a51
7
+ data.tar.gz: 79c345933fd16d7c2acfa24715ef09119c1973c605bb5a7b2caa959fcf08ae748e087338717d6e4343281e68a9a3ed9440d5c74e57846691f7033ef2f41e4079
data/README.md CHANGED
@@ -9,15 +9,11 @@ Reporting Service for Grafana
9
9
  ## Table of Contents
10
10
 
11
11
  * [About the project](#about-the-project)
12
- * [Getting started](#getting-started)
13
- * [Grafana integration](#grafana-integration)
14
- * [Webservice overview](#webservice-overview)
15
- * [Documentation](#documentation)
16
12
  * [Features](#features)
13
+ * [Quick Start](#quick-start)
14
+ * [Grafana integration](#grafana-integration)
15
+ * [Webservice overview](#webservice-overview)
17
16
  * [Roadmap](#roadmap)
18
- * [Contributing](#contributing)
19
- * [Licensing](#licensing)
20
- * [Acknowledgements](#acknowledgements)
21
17
  * [Donations](#donations)
22
18
 
23
19
  ## About the project
@@ -26,53 +22,88 @@ Did you ever want to create (professional) reports based on Grafana dashboards?
26
22
  I did so in order to being able to automatically get monthly reports of my
27
23
  home's energy usage. That's how it started.
28
24
 
29
- The reporter provides reporting capabilities for Grafana. It is based on
30
- (but not limited to) [asciidoctor](https://github.com/asciidoctor/asciidoctor)
31
- report templates, which can dynamically integrate Grafana panels, queries,
32
- images etc. to create dynamic PDF reports on the fly.
25
+ ## Features
26
+
27
+ * Build PDF reports based on [grafana](https://github.com/grafana/grafana) dashboards
28
+ (other formats supported)
29
+ * Include dynamic content from grafana (see [function documentation](FUNCTION_CALLS.md)
30
+ as a detailed reference):
31
+ * panels as images
32
+ * tables based on grafana panel queries or custom database queries (no images!)
33
+ * single values to be integrated in text, based on grafana panel queries or custom
34
+ database queries
35
+ * Multi purpose use of the reporter
36
+ * webservice to be called directly from grafana - it also runs without further
37
+ dependencies in the standard asciidoctor docker container!
38
+ * standalone command line tool, e.g. to be automated with cron or bash scrips
39
+ * Comes with a complete configuration wizard, including functionality to build a
40
+ demo report on top of the configured grafana host
41
+ * Supports all SQL based datasources, as well as graphite and prometheus
42
+ * Solid as a rock, also in case of template errors and whatever else may happen
43
+ * Full [API documentation](https://rubydoc.info/gems/ruby-grafana-reporter) available
33
44
 
34
- The report may also be returned in any other format that asciidoctor supports.
45
+ ## Quick Start
35
46
 
36
- The reporter can run standalone or as a webservice. It is built to
37
- integrate without further dependencies with the asciidoctor docker image.
47
+ You don't have a grafana setup runnning already? No worries, just configure
48
+ `https://play.grafana.org` in the configuration wizard and see the magic
49
+ happen for that!
38
50
 
39
- Can't wait to see, what functions the reporter provides within the asciidoctor
40
- templates? Have a look at the [function documentation](FUNCTION_CALLS.md).
51
+ If your grafana setup requires a login, you'll have to setup an api key for
52
+ the reporter. Please follow the steps
53
+ [described here](https://github.com/divinity666/ruby-grafana-reporter/issues/2#issuecomment-811836757)
54
+ first.
41
55
 
42
- ## Getting started
56
+ **Windows:**
43
57
 
44
- There exist several ways of installing the reporter. If you need further
45
- installation help, or want to use a "baremetal" ruby setup or a docker
46
- integration, please have a look at the more extended
47
- [installation documentation](INSTALL.md).
58
+ * [Download latest Windows executable](https://github.com/divinity666/ruby-grafana-reporter/releases/latest)
59
+ * `ruby-grafana-reporter -w`
48
60
 
49
- Windows users may directly use the provided executable.
61
+ Known issues:
62
+ * images are currently not included in PDF conversions due to missing support in Prawn gem for windows;
63
+ other target formats do work properly with images
50
64
 
51
- Following these steps sets up the reporter on a fresh Raspberry Pi installation:
65
+ **Raspberry Pi:**
52
66
 
53
- sudo apt-get install ruby
54
- gem install ruby-grafana-reporter
67
+ * `sudo apt-get install ruby`
68
+ * `gem install ruby-grafana-reporter`
69
+ * `ruby-grafana-reporter -w`
55
70
 
56
- That's it. Let's now configure a grafana setup with the configuration wizard:
71
+ **Ruby environment:**
57
72
 
58
- ruby-grafana-reporter -w
73
+ * `gem install ruby-grafana-reporter`
74
+ * `ruby-grafana-reporter -w`
59
75
 
60
- It is strongly recommended, to also create the demo PDF file, as stated at the end
61
- of the procedure, to get a detailed documentation of all the reporter capabilities.
62
- The whole [function documentation](FUNCTION_CALLS.md) is also available at the
63
- previous link.
76
+ **Docker environment** (advanced users):
64
77
 
65
- To run the reporter as a service, you only need to call it like this:
78
+ * [Download latest single-rb file](https://github.com/divinity666/ruby-grafana-reporter/releases/latest)
79
+ to an empty folder
80
+ * create a configuration file by calling `ruby ruby-grafana-reporter -w` (if in doubt,
81
+ run the command within your docker container)
82
+ * create file `/<<path-to-single-rb-file-folder>>/startup.sh` with the following
83
+ content:
66
84
 
67
- ruby-grafana-reporter
85
+ ```
86
+ cd /documents
87
+ ruby bin/ruby-grafana-reporter
88
+ ```
89
+ * add asciidoctor your compose yaml:
68
90
 
69
- Neat, isn't it?
91
+ ```
92
+ asciidoctor:
93
+ image: asciidoctor/docker-asciidoctor
94
+ container_name: asciidoctor
95
+ hostname: asciidoctor
96
+ volumes:
97
+ - /<<path-to-single-rb-file-folder>>:/documents
98
+ command:
99
+ sh /documents/startup.sh
100
+ restart: unless-stopped
101
+ ```
102
+ * start/restart the asciidoctor docker container
70
103
 
71
- ### Grafana integration
104
+ ## Grafana integration
72
105
 
73
- The key feature of the report is, that it can easily be integrated with grafana
74
- (I've not even been talking about the features it is providing for that, but
75
- you'll find them having a look in the example results above).
106
+ The key feature of the report is, that it can easily be integrated with grafana.
76
107
 
77
108
  For accessing the reporter from grafana, you need to simply add a link to your
78
109
  grafana dashboard:
@@ -82,27 +113,27 @@ grafana dashboard:
82
113
  * Select `Add`
83
114
  * Fill out as following:
84
115
  * Type: `link`
85
- * Url: `http://<<your-server-url>>:<<your-webservice-port>>/render?var-template=myfirsttemplate`
86
- * Title: `MyFirstReport`
116
+ * Url: `http://<<your-server-url>>:<<your-webservice-port>>/render?var-template=demo_report`
117
+ * Title: `Demo Report`
87
118
  * Select `Time range`
88
119
  * Select `Variable values`
89
120
  * Select `Add`
90
121
 
91
- Now go back to your dashboard and click the newly generated 'MyFirstReport'
122
+ Now go back to your dashboard and click the newly generated `Demo Report`
92
123
  link on it. Now the renderer should start it's task and show you the expected
93
124
  results.
94
125
 
95
- But now the fun just starts! Try out the functions stated in the
96
- 'MyFirstReport' PDF file, to include the dynamic content in your asciidoctor
97
- template.
126
+ Please note, that the reporter won't automatically refresh your screen to update
127
+ the progress. Simply hit `F5` to refresh your browser. After the report has been
128
+ successfully built, it will show the PDF after the next refresh automatically.
98
129
 
99
- Additionally you might want to make the selection of the template variable.
130
+ You want to select a template in grafana, which shall then be rendered?
100
131
  Piece of cake: Just add a dashboard variable to your grafana dashboard named
101
132
  `template` and let the user select or enter a template name. To make use of it,
102
- you should change the link of the 'MyFirstReport' link to
103
- `http://<<your-server-url>>:<<your-webservice-port>>/render?`
104
-
105
- That's it. Let me know your feedback!
133
+ you should change the link of the `Demo Report` link to
134
+ `http://<<your-server-url>>:<<your-webservice-port>>/render?`. On
135
+ hitting the new link in the dashboard, grafana will add the selected template as
136
+ a variable and forward it to the reporter.
106
137
 
107
138
  ## Webservice overview
108
139
 
@@ -117,33 +148,13 @@ The main endpoint to call for report generation is configured in the previous ch
117
148
 
118
149
  However, if you would like to see, currently running report generations and previously generated reports, you may want to call the endpoint `/overview`.
119
150
 
120
- ## Documentation
121
-
122
- The [function documentation](FUNCTION_CALLS.md) contains a complete overview of
123
- all possible grafana calls, to generate dynamic report templates.
124
-
125
- The [API documentation](https://rubydoc.info/gems/ruby-grafana-reporter) can be
126
- found here.
127
-
128
- ## Features
129
-
130
- * Build report template including all imaginable grafana content:
131
- * panels as images
132
- * panel table query or custom query results as real document tables (not images!)
133
- * single panel value or custom query single value result integrated in texts
134
- * Solid as a rock, also in case of template errors and whatever else may happen
135
- * Fully controllable as command line application or as a webservice
136
- * Seamlessly integrates with asciidoctor docker container
137
- * Developed to support other tools than asciidoctor as well
138
-
139
151
  ## Roadmap
140
152
 
141
153
  This is just a collection of things, I am heading for in future, without a schedule.
142
154
 
143
- * Add a simple plugin system to support specific asciidoctor modifications
155
+ * Support all grafana datasources
144
156
  * Solve code TODOs
145
157
  * Become [rubocop](https://rubocop.org/) ready
146
- * Clean and properly setup test cases
147
158
 
148
159
  ## Contributing
149
160
 
@@ -167,4 +178,3 @@ If this project saves you as much time as I hope it does, and if you'd like to
167
178
  support my work, feel free donate, even a cup of coffee is appreciated :)
168
179
 
169
180
  [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate?hosted_button_id=35LH6JNLPHPHQ)
170
-
@@ -1,5 +1,5 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- require_relative '../lib/ruby-grafana-reporter'
5
- GrafanaReporter::Application::Application.new.configure_and_run(ARGV) unless defined?(Ocra)
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/ruby_grafana_reporter'
5
+ GrafanaReporter::Application::Application.new.configure_and_run(ARGV) unless defined?(Ocra)
data/lib/VERSION.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # Version information
4
- GRAFANA_REPORTER_VERSION = [0, 3, 0].freeze
5
- GRAFANA_REPORTER_RELEASE_DATE = '2021-03-02'
4
+ GRAFANA_REPORTER_VERSION = [0, 4, 0].freeze
5
+ # Release date
6
+ GRAFANA_REPORTER_RELEASE_DATE = '2021-04-14'
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grafana
4
+ # This abstract class defines the base functionalities for the common datasource implementations.
5
+ # Additionally it provides a factory method to build a real datasource from a given specification.
6
+ class AbstractDatasource
7
+ attr_reader :model
8
+
9
+ # Factory method to build a datasource from a given datasource Hash description.
10
+ # @param ds_model [Hash] grafana specification of a single datasource
11
+ # @return [AbstractDatasource] instance of a fitting datasource implementation
12
+ def self.build_instance(ds_model)
13
+ raise InvalidDatasourceQueryProvidedError, ds_model unless ds_model.is_a?(Hash)
14
+ raise InvalidDatasourceQueryProvidedError, ds_model unless ds_model['meta']
15
+ raise InvalidDatasourceQueryProvidedError, ds_model unless ds_model['meta']['id']
16
+ raise InvalidDatasourceQueryProvidedError, ds_model unless ds_model['meta']['category']
17
+
18
+ return SqlDatasource.new(ds_model) if ds_model['meta']['category'] == 'sql'
19
+
20
+ case ds_model['meta']['id']
21
+ when 'graphite'
22
+ return GraphiteDatasource.new(ds_model)
23
+
24
+ when 'prometheus'
25
+ return PrometheusDatasource.new(ds_model)
26
+
27
+ end
28
+
29
+ raise DatasourceTypeNotSupportedError.new(ds_model['name'], ds_model['meta']['id'])
30
+ end
31
+
32
+ def initialize(model)
33
+ @model = model
34
+ end
35
+
36
+ # @return [String] name of the datasource
37
+ def name
38
+ @model['name']
39
+ end
40
+
41
+ # @return [Integer] ID of the datasource
42
+ def id
43
+ @model['id'].to_i
44
+ end
45
+
46
+ # @abstract
47
+ #
48
+ # Executes a request for the current database with the given options.
49
+ #
50
+ # Used format of the response will always be the following:
51
+ #
52
+ # {
53
+ # :header => [column_title_1, column_title_2],
54
+ # :content => [
55
+ # [row_1_column_1, row_1_column_2],
56
+ # [row_2_column_1, row_2_column_2]
57
+ # ]
58
+ # }
59
+ #
60
+ # @param query_description [Hash] query description, which will requested:
61
+ # @option [String] :from +from+ timestamp
62
+ # @option [String] :to +to+ timestamp
63
+ # @option [Integer] :timeout expected timeout for the request
64
+ # @option [WebRequest] :prepared_request prepared web request for relevant {Grafana} instance, if this is needed by datasource
65
+ # @option [String] :raw_query raw query, which shall be executed. May include variables, which will be replaced before execution
66
+ # @option [Hash<Variable>] :variables hash of variables, which can potentially be replaced in the given +:raw_query+
67
+ # @return [Hash] sql result formatted as stated above
68
+ def request(query_description)
69
+ raise NotImplementedError
70
+ end
71
+
72
+ # @abstract
73
+ #
74
+ # The different datasources supported by grafana use different ways to store the query in the
75
+ # panel's JSON model. This method extracts a query from that description, that can be used
76
+ # by the {AbstractDatasource} implementation of the datasource.
77
+ #
78
+ # @param panel_query_target [Hash] grafana panel target, which contains the query description
79
+ # @return [String] query string, which can be used as +raw_query+ in a {#request}
80
+ def raw_query_from_panel_model(panel_query_target)
81
+ raise NotImplementedError
82
+ end
83
+
84
+ private
85
+
86
+ # Replaces the grafana variables in the given string with their replacement value.
87
+ #
88
+ # @param string [String] string in which the variables shall be replaced
89
+ # @param variables [Hash<String,Variable>] Hash containing the variables, which shall be replaced in the
90
+ # given string
91
+ # @return [String] string in which all variables are properly replaced
92
+ def replace_variables(string, variables = {})
93
+ res = string
94
+ repeat = true
95
+ repeat_count = 0
96
+
97
+ # TODO: find a proper way to replace variables recursively instead of over and over again
98
+ # TODO: add tests for recursive replacement of variable
99
+ while repeat && (repeat_count < 3)
100
+ repeat = false
101
+ repeat_count += 1
102
+ variables.each do |var_name, obj|
103
+ # only set ticks if value is string
104
+ variable = var_name.gsub(/^var-/, '')
105
+ res = res.gsub(/(?:\$\{#{variable}(?::(?<format>\w+))?\}|\$#{variable})/) do
106
+ # TODO: respect datasource requirements for formatting here
107
+ obj.value_formatted($LAST_MATCH_INFO ? $LAST_MATCH_INFO[:format] : nil)
108
+ end
109
+ end
110
+ repeat = true if res.include?('$')
111
+ end
112
+
113
+ res
114
+ end
115
+ end
116
+ end
@@ -53,9 +53,7 @@ module Grafana
53
53
  list = @model['templating']['list']
54
54
  return unless list.is_a? Array
55
55
 
56
- list.each do |item|
57
- @variables << Variable.new(item)
58
- end
56
+ list.each { |item| @variables << Variable.new(item) }
59
57
  end
60
58
 
61
59
  # read panels
@@ -63,4 +63,19 @@ module Grafana
63
63
  super('No SQL statement has been specified.')
64
64
  end
65
65
  end
66
+
67
+ # Raised if a datasource shall be queried, which is not (yet) supported by the reporter
68
+ class DatasourceTypeNotSupportedError < GrafanaError
69
+ def initialize(name, type)
70
+ super("The configured datasource with name '#{name}' is of type '#{type}', which is currently "\
71
+ 'not supported by ruby-grafana-reporter. It will only be usable in panel image queries.')
72
+ end
73
+ end
74
+
75
+ # Raised if a datasource shall be queried, which is not (yet) supported by the reporter
76
+ class InvalidDatasourceQueryProvidedError < GrafanaError
77
+ def initialize(query)
78
+ super("The datasource query provided, does not look like a grafana datasource target (received: #{query}).")
79
+ end
80
+ end
66
81
  end
@@ -13,12 +13,11 @@ module Grafana
13
13
  # trailing slash, e.g. +https://localhost:3000+.
14
14
  # @param key [String] API key for the grafana instance, if required
15
15
  # @param opts [Hash] additional options.
16
- # Currently supporting +:logger+ and +:ssl_cert+.
16
+ # Currently supporting +:logger+.
17
17
  def initialize(base_uri, key = nil, opts = {})
18
18
  @base_uri = base_uri
19
19
  @key = key
20
20
  @dashboards = {}
21
- @ssl_cert = opts[:ssl_cert]
22
21
  @logger = opts[:logger] || ::Logger.new(nil)
23
22
 
24
23
  initialize_datasources unless @base_uri.empty?
@@ -31,33 +30,52 @@ module Grafana
31
30
  #
32
31
  # @return [String] +Admin+, +NON-Admin+ or +Failed+ is returned, depending on the test results
33
32
  def test_connection
34
- if execute_http_request('/api/datasources').is_a?(Net::HTTPOK)
33
+ if prepare_request({ relative_url: '/api/datasources' }).execute.is_a?(Net::HTTPOK)
35
34
  # we have admin rights
36
35
  @logger.warn('Reporter is running with Admin privileges on grafana. This is a potential security risk.')
37
36
  return 'Admin'
38
37
  end
39
38
  # check if we have lower rights
40
- return 'Failed' unless execute_http_request('/api/dashboards/home').is_a?(Net::HTTPOK)
39
+ return 'Failed' unless prepare_request({ relative_url: '/api/dashboards/home' }).execute.is_a?(Net::HTTPOK)
41
40
 
42
41
  @logger.info('Reporter is running with NON-Admin privileges on grafana.')
43
42
  'NON-Admin'
44
43
  end
45
44
 
46
- # Returns the ID of a datasource, which has been queried by the datasource name.
45
+ # Returns the datasource, which has been queried by the datasource name.
47
46
  #
48
- # @return [Integer] ID for the specified datasource name
49
- def datasource_id(datasource_name)
50
- datasource_name ||= 'default'
51
- return @datasources[datasource_name] if @datasources[datasource_name]
47
+ # @param datasource_name [String] name of the searched datasource
48
+ # @return [Datasource] Datasource for the specified datasource name
49
+ def datasource_by_name(datasource_name)
50
+ datasource_name = 'default' if datasource_name.to_s.empty?
51
+ raise DatasourceDoesNotExistError.new('name', datasource_name) unless @datasources[datasource_name]
52
52
 
53
- raise DatasourceDoesNotExistError.new('name', datasource_name)
53
+ @datasources[datasource_name]
54
54
  end
55
55
 
56
- # Returns if the given datasource ID exists for the grafana instance.
56
+ # Returns the datasource, which has been queried by the datasource id.
57
57
  #
58
- # @return [Boolean] true if exists, false otherwise
59
- def datasource_id_exists?(datasource_id)
60
- @datasources.value?(datasource_id)
58
+ # @param datasource_id [Integer] id of the searched datasource
59
+ # @return [Datasource] Datasource for the specified datasource id
60
+ def datasource_by_id(datasource_id)
61
+ datasource = @datasources.select { |_name, ds| ds.id == datasource_id.to_i }.values.first
62
+ raise DatasourceDoesNotExistError.new('id', datasource_id) unless datasource
63
+
64
+ datasource
65
+ end
66
+
67
+ # @return [Array] Array of dashboard uids within the current grafana object
68
+ def dashboard_ids
69
+ response = prepare_request({ relative_url: '/api/search' }).execute
70
+ return [] unless response.is_a?(Net::HTTPOK)
71
+
72
+ dashboards = JSON.parse(response.body)
73
+
74
+ dashboards.each do |dashboard|
75
+ @dashboards[dashboard['uid']] = nil unless @dashboards[dashboard['uid']]
76
+ end
77
+
78
+ @dashboards.keys
61
79
  end
62
80
 
63
81
  # @param dashboard_uid [String] UID of the searched {Dashboard}
@@ -65,54 +83,27 @@ module Grafana
65
83
  def dashboard(dashboard_uid)
66
84
  return @dashboards[dashboard_uid] unless @dashboards[dashboard_uid].nil?
67
85
 
68
- response = execute_http_request("/api/dashboards/uid/#{dashboard_uid}")
69
- model = JSON.parse(response.body)['dashboard']
70
-
71
- raise DashboardDoesNotExistError, dashboard_uid if model.nil?
86
+ response = prepare_request({ relative_url: "/api/dashboards/uid/#{dashboard_uid}" }).execute
87
+ raise DashboardDoesNotExistError, dashboard_uid unless response.is_a?(Net::HTTPOK)
72
88
 
73
89
  # cache dashboard for reuse
90
+ model = JSON.parse(response.body)['dashboard']
74
91
  @dashboards[dashboard_uid] = Dashboard.new(model, self)
75
92
 
76
93
  @dashboards[dashboard_uid]
77
94
  end
78
95
 
79
- # Runs a specific HTTP request against the current grafana instance.
96
+ # Prepares a {WebRequest} object for the current {Grafana} instance, which may be enriched
97
+ # with further properties and can then run {WebRequest#execute}.
80
98
  #
81
- # Default (can be overridden, by specifying the options Hash):
82
- # accept: 'application/json'
83
- # request: Net::HTTP::Get
84
- # content_type: 'application/json'
85
- #
86
- # @param relative_uri [String] relative URL with a leading slash, which shall be queried
87
- # @param options [Hash] options, which shall be merged to the request.
88
- # @param timeout [Integer] number of seconds to wait, before the http request is cancelled, defaults to 60 seconds
89
- def execute_http_request(relative_uri, options = {}, timeout = nil)
90
- uri = URI.parse(@base_uri + relative_uri)
91
- default_options = { accept: 'application/json', request: Net::HTTP::Get, content_type: 'application/json' }
92
- options = default_options.merge(options)
93
-
94
- http = Net::HTTP.new(uri.host, uri.port)
95
- if @base_uri =~ /^https/
96
- http.use_ssl = true
97
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
98
- if @ssl_cert && !File.exist?(@ssl_cert)
99
- @logger.warn('SSL certificate file does not exist.')
100
- elsif @ssl_cert
101
- http.cert_store = OpenSSL::X509::Store.new
102
- http.cert_store.set_default_paths
103
- http.cert_store.add_file(@ssl_cert)
104
- end
105
- end
106
- http.read_timeout = timeout.to_i if timeout
107
-
108
- request = options[:request].new(uri.request_uri)
109
- request['Accept'] = options[:accept]
110
- request['Content-Type'] = options[:content_type]
111
- request['Authorization'] = "Bearer #{@key}" unless @key.nil?
112
- request.body = options[:body]
113
-
114
- @logger.debug("Requesting #{relative_uri} with '#{options[:body]}' and timeout '#{http.read_timeout}'")
115
- http.request(request)
99
+ # @option options [Hash] :relative_url relative URL with a leading slash, which shall be queried
100
+ # @option options [Hash] :accept
101
+ # @option options [Hash] :body
102
+ # @option options [Hash] :content_type
103
+ # @return [WebRequest] webrequest prepared for execution
104
+ def prepare_request(options = {})
105
+ auth = @key ? { authorization: "Bearer #{@key}" } : {}
106
+ WebRequest.new(@base_uri, auth.merge({ logger: @logger }).merge(options))
116
107
  end
117
108
 
118
109
  private
@@ -120,12 +111,18 @@ module Grafana
120
111
  def initialize_datasources
121
112
  @datasources = {}
122
113
 
123
- settings = execute_http_request('/api/frontend/settings')
114
+ settings = prepare_request({ relative_url: '/api/frontend/settings' }).execute
124
115
  return unless settings.is_a?(Net::HTTPOK)
125
116
 
126
117
  json = JSON.parse(settings.body)
127
118
  json['datasources'].select { |_k, v| v['id'].to_i.positive? }.each do |ds_name, ds_value|
128
- @datasources[ds_name] = ds_value['id'].to_i
119
+ begin
120
+ @datasources[ds_name] = AbstractDatasource.build_instance(ds_value)
121
+ rescue DatasourceTypeNotSupportedError => e
122
+ # an unsupported datasource type has been configured in the dashboard
123
+ # - no worries here
124
+ @logger.warn(e.message)
125
+ end
129
126
  end
130
127
  @datasources['default'] = @datasources[json['defaultDatasource']]
131
128
  end