ruby-grafana-reporter 0.1.7 → 0.2.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 +4 -4
- data/README.md +166 -339
- data/bin/ruby-grafana-reporter +5 -4
- data/lib/VERSION.rb +5 -3
- data/lib/grafana/abstract_panel_query.rb +22 -20
- data/lib/grafana/abstract_query.rb +132 -127
- data/lib/grafana/abstract_sql_query.rb +51 -42
- data/lib/grafana/dashboard.rb +77 -66
- data/lib/grafana/errors.rb +66 -61
- data/lib/grafana/grafana.rb +130 -131
- data/lib/grafana/panel.rb +41 -39
- data/lib/grafana/panel_image_query.rb +52 -49
- data/lib/grafana/variable.rb +217 -259
- data/lib/grafana_reporter/abstract_report.rb +112 -109
- data/lib/grafana_reporter/application/application.rb +404 -229
- data/lib/grafana_reporter/application/errors.rb +33 -30
- data/lib/grafana_reporter/application/webservice.rb +231 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +104 -99
- data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +99 -96
- data/lib/grafana_reporter/asciidoctor/errors.rb +40 -37
- data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +92 -86
- data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +91 -86
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +69 -67
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +68 -65
- data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +61 -58
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +78 -75
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +73 -70
- data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +20 -18
- data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +43 -41
- data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +70 -67
- data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +66 -65
- data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +61 -57
- data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +34 -32
- data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +25 -23
- data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +44 -43
- data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +38 -36
- data/lib/grafana_reporter/asciidoctor/query_mixin.rb +310 -309
- data/lib/grafana_reporter/asciidoctor/report.rb +177 -159
- data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +37 -34
- data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +39 -32
- data/lib/grafana_reporter/configuration.rb +257 -326
- data/lib/grafana_reporter/errors.rb +48 -38
- data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
- data/lib/ruby-grafana-reporter.rb +29 -27
- metadata +10 -23
data/lib/grafana/errors.rb
CHANGED
@@ -1,61 +1,66 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# A top level alarm for all other errors in current module.
|
5
|
+
class GrafanaError < StandardError
|
6
|
+
def initialize(message)
|
7
|
+
super("GrafanaError: #{message} (#{self.class})")
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
# Raised if a given dashboard does not exist in a specific {Grafana} instance.
|
12
|
+
class DashboardDoesNotExistError < GrafanaError
|
13
|
+
# @param dashboard_uid [String] dashboard uid, which could not be found
|
14
|
+
def initialize(dashboard_uid)
|
15
|
+
super("The specified dashboard '#{dashboard_uid}' does not exist.")
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# Raised if a given panel does not exist on a specific {Dashboard} in the current {Grafana} instance.
|
20
|
+
class PanelDoesNotExistError < GrafanaError
|
21
|
+
# @param panel_id [String] panel id, which could not be found on the dashboard
|
22
|
+
# @param dashboard [Dashboard] dashboard object on which the panel could not be found
|
23
|
+
def initialize(panel_id, dashboard)
|
24
|
+
super("The specified panel id '#{panel_id}' does not exist on the dashboard '#{dashboard.id}'.")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Raised if a given query letter does not exist on a specific {Panel}.
|
29
|
+
class QueryLetterDoesNotExistError < GrafanaError
|
30
|
+
# @param query_letter [String] query letter name, which could not be found on the panel
|
31
|
+
# @param panel [Panel] panel object on which the query could not be found
|
32
|
+
def initialize(query_letter, panel)
|
33
|
+
super("The specified query '#{query_letter}' does not exist in the panel '#{panel.id}' "\
|
34
|
+
"in dashboard '#{panel.dashboard}'.")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Raised if a given datasource does not exist in a specific {Grafana} instance.
|
39
|
+
class DatasourceDoesNotExistError < GrafanaError
|
40
|
+
# @param field [String] specifies, how the datasource has been searched, e.g. 'id' or 'name'
|
41
|
+
# @param datasource_identifier [String] identifier of the datasource, which could not be found,
|
42
|
+
# e.g. the specifiy id or name
|
43
|
+
def initialize(field, datasource_identifier)
|
44
|
+
super("Datasource with #{field} '#{datasource_identifier}' does not exist.")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Raised if a {Panel} could not be rendered as an image.
|
49
|
+
#
|
50
|
+
# Most likely this happens, because the image renderer is not configures properly in grafana,
|
51
|
+
# or the panel rendering ran into a timeout.
|
52
|
+
class ImageCouldNotBeRenderedError < GrafanaError
|
53
|
+
# @param panel [Panel] panel object, which could not be rendered
|
54
|
+
def initialize(panel)
|
55
|
+
super("The specified panel '#{panel.id}' from dashboard '#{panel.dashboard.id} could not be "\
|
56
|
+
'rendered to an image.')
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Raised if no SQL query is specified in a {AbstractSqlQuery} object.
|
61
|
+
class MissingSqlQueryError < GrafanaError
|
62
|
+
def initialize
|
63
|
+
super('No SQL statement has been specified.')
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/grafana/grafana.rb
CHANGED
@@ -1,131 +1,130 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
#
|
4
|
-
#
|
5
|
-
#
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
#
|
29
|
-
#
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
'
|
46
|
-
end
|
47
|
-
|
48
|
-
# Returns the ID of a datasource, which has been queried by the datasource name.
|
49
|
-
#
|
50
|
-
# @return [Integer] ID for the specified datasource name
|
51
|
-
def datasource_id(datasource_name)
|
52
|
-
id = datasources[datasource_name]
|
53
|
-
raise DatasourceDoesNotExistError.new('name', datasource_name) unless id
|
54
|
-
|
55
|
-
id
|
56
|
-
end
|
57
|
-
|
58
|
-
# Returns if the given datasource ID exists for the grafana instance.
|
59
|
-
#
|
60
|
-
# @return [Boolean] true if exists, false otherwise
|
61
|
-
def datasource_id_exists?(datasource_id)
|
62
|
-
datasources.value?(datasource_id)
|
63
|
-
end
|
64
|
-
|
65
|
-
# @param dashboard_uid [String] UID of the searched {Dashboard}
|
66
|
-
# @return [Dashboard] dashboard object, if it has been found
|
67
|
-
def dashboard(dashboard_uid)
|
68
|
-
return @dashboards[dashboard_uid] unless @dashboards[dashboard_uid].nil?
|
69
|
-
|
70
|
-
response = execute_http_request(
|
71
|
-
model = JSON.parse(response.body)['dashboard']
|
72
|
-
|
73
|
-
raise DashboardDoesNotExistError, dashboard_uid if model.nil?
|
74
|
-
|
75
|
-
# cache dashboard for reuse
|
76
|
-
@dashboards[dashboard_uid] = Dashboard.new(model, self)
|
77
|
-
|
78
|
-
@dashboards[dashboard_uid]
|
79
|
-
end
|
80
|
-
|
81
|
-
# Runs a specific HTTP request against the current grafana instance.
|
82
|
-
#
|
83
|
-
# Default (can be overridden, by specifying the options Hash):
|
84
|
-
# accept: 'application/json'
|
85
|
-
# request: Net::HTTP::Get
|
86
|
-
# content_type: 'application/json'
|
87
|
-
#
|
88
|
-
# @param relative_uri [String] relative URL with a leading slash, which shall be queried
|
89
|
-
# @param options [Hash] options, which shall be merged to the request.
|
90
|
-
# @param timeout [Integer] number of seconds to wait, before the http request is cancelled, defaults to 60 seconds
|
91
|
-
def execute_http_request(relative_uri, options = {}, timeout = nil)
|
92
|
-
uri = URI.parse(@base_uri + relative_uri)
|
93
|
-
default_options = { accept: 'application/json', request: Net::HTTP::Get, content_type: 'application/json' }
|
94
|
-
options = default_options.merge(options)
|
95
|
-
|
96
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
97
|
-
if @base_uri =~ /^https/
|
98
|
-
http.use_ssl = true
|
99
|
-
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
100
|
-
end
|
101
|
-
http.read_timeout = timeout.to_i if timeout
|
102
|
-
|
103
|
-
request = options[:request].new(uri.request_uri)
|
104
|
-
request['Accept'] = options[:accept]
|
105
|
-
request['Content-Type'] = options[:content_type]
|
106
|
-
request['Authorization'] =
|
107
|
-
request.body = options[:body]
|
108
|
-
|
109
|
-
@logger.debug("Requesting #{relative_uri} with '#{options[:body]}' and timeout '#{http.read_timeout}'")
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
response
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Contains all objects for creating structured objects for interfacing grafana.
|
4
|
+
#
|
5
|
+
# The intention is, that these represent the business logic contained within grafana
|
6
|
+
# in an appropriate object model for the reporter to work with.
|
7
|
+
#
|
8
|
+
# For details, see also {https://grafana.com/docs/grafana/latest/http_api Grafana API}.
|
9
|
+
module Grafana
|
10
|
+
# Main class for handling the interaction with one specific Grafana instance.
|
11
|
+
class Grafana
|
12
|
+
# @param base_uri [String] full URI pointing to the specific grafana instance without
|
13
|
+
# trailing slash, e.g. +https://localhost:3000+.
|
14
|
+
# @param key [String] API key for the grafana instance, if required
|
15
|
+
# @param opts [Hash] additional options.
|
16
|
+
# Currently supporting +:logger+ and +:datasources+.
|
17
|
+
# +:datasources+ need to be an Hash with datasource name as key and datasource id as value.
|
18
|
+
# If not specified, the datasources will be queried from the grafana interface.
|
19
|
+
# Specifying +:datasources+> here can be used, so that the interface can be used without grafana Admin privileges.
|
20
|
+
def initialize(base_uri, key = nil, opts = {})
|
21
|
+
@base_uri = base_uri
|
22
|
+
@key = key
|
23
|
+
@dashboards = {}
|
24
|
+
@logger = opts[:logger] || ::Logger.new(nil)
|
25
|
+
@datasources = opts[:datasources]
|
26
|
+
end
|
27
|
+
|
28
|
+
# Used to test a connection to the grafana instance.
|
29
|
+
#
|
30
|
+
# Running this function also determines, if the API configured here has Admin or NON-Admin privileges,
|
31
|
+
# or even fails on connecting to grafana.
|
32
|
+
#
|
33
|
+
# @return [String] +Admin+, +NON-Admin+ or +Failed+ is returned, depending on the test results
|
34
|
+
def test_connection
|
35
|
+
if execute_http_request('/api/datasources').is_a?(Net::HTTPOK)
|
36
|
+
# we have admin rights
|
37
|
+
@logger.info('Reporter is running with Admin privileges on grafana.')
|
38
|
+
return 'Admin'
|
39
|
+
end
|
40
|
+
# check if we have lower rights
|
41
|
+
return 'Failed' unless execute_http_request('/api/dashboards/home').is_a?(Net::HTTPOK)
|
42
|
+
|
43
|
+
@logger.warn('Reporter is running with NON-Admin privileges on grafana. Make sure that necessary'\
|
44
|
+
'datasources are specified in CONFIG_FILE, otherwise operation will fail')
|
45
|
+
'NON-Admin'
|
46
|
+
end
|
47
|
+
|
48
|
+
# Returns the ID of a datasource, which has been queried by the datasource name.
|
49
|
+
#
|
50
|
+
# @return [Integer] ID for the specified datasource name
|
51
|
+
def datasource_id(datasource_name)
|
52
|
+
id = datasources[datasource_name]
|
53
|
+
raise DatasourceDoesNotExistError.new('name', datasource_name) unless id
|
54
|
+
|
55
|
+
id
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns if the given datasource ID exists for the grafana instance.
|
59
|
+
#
|
60
|
+
# @return [Boolean] true if exists, false otherwise
|
61
|
+
def datasource_id_exists?(datasource_id)
|
62
|
+
datasources.value?(datasource_id)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @param dashboard_uid [String] UID of the searched {Dashboard}
|
66
|
+
# @return [Dashboard] dashboard object, if it has been found
|
67
|
+
def dashboard(dashboard_uid)
|
68
|
+
return @dashboards[dashboard_uid] unless @dashboards[dashboard_uid].nil?
|
69
|
+
|
70
|
+
response = execute_http_request("/api/dashboards/uid/#{dashboard_uid}")
|
71
|
+
model = JSON.parse(response.body)['dashboard']
|
72
|
+
|
73
|
+
raise DashboardDoesNotExistError, dashboard_uid if model.nil?
|
74
|
+
|
75
|
+
# cache dashboard for reuse
|
76
|
+
@dashboards[dashboard_uid] = Dashboard.new(model, self)
|
77
|
+
|
78
|
+
@dashboards[dashboard_uid]
|
79
|
+
end
|
80
|
+
|
81
|
+
# Runs a specific HTTP request against the current grafana instance.
|
82
|
+
#
|
83
|
+
# Default (can be overridden, by specifying the options Hash):
|
84
|
+
# accept: 'application/json'
|
85
|
+
# request: Net::HTTP::Get
|
86
|
+
# content_type: 'application/json'
|
87
|
+
#
|
88
|
+
# @param relative_uri [String] relative URL with a leading slash, which shall be queried
|
89
|
+
# @param options [Hash] options, which shall be merged to the request.
|
90
|
+
# @param timeout [Integer] number of seconds to wait, before the http request is cancelled, defaults to 60 seconds
|
91
|
+
def execute_http_request(relative_uri, options = {}, timeout = nil)
|
92
|
+
uri = URI.parse(@base_uri + relative_uri)
|
93
|
+
default_options = { accept: 'application/json', request: Net::HTTP::Get, content_type: 'application/json' }
|
94
|
+
options = default_options.merge(options)
|
95
|
+
|
96
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
97
|
+
if @base_uri =~ /^https/
|
98
|
+
http.use_ssl = true
|
99
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
|
100
|
+
end
|
101
|
+
http.read_timeout = timeout.to_i if timeout
|
102
|
+
|
103
|
+
request = options[:request].new(uri.request_uri)
|
104
|
+
request['Accept'] = options[:accept]
|
105
|
+
request['Content-Type'] = options[:content_type]
|
106
|
+
request['Authorization'] = "Bearer #{@key}" unless @key.nil?
|
107
|
+
request.body = options[:body]
|
108
|
+
|
109
|
+
@logger.debug("Requesting #{relative_uri} with '#{options[:body]}' and timeout '#{http.read_timeout}'")
|
110
|
+
http.request(request)
|
111
|
+
end
|
112
|
+
|
113
|
+
private
|
114
|
+
|
115
|
+
def datasources
|
116
|
+
if @datasources.nil?
|
117
|
+
# load datasources from grafana directly, if allowed
|
118
|
+
response = execute_http_request('/api/datasources')
|
119
|
+
if response['message'].nil?
|
120
|
+
json = JSON.parse(response.body)
|
121
|
+
# only store needed values
|
122
|
+
@datasources = json.map { |item| [item['name'], item['id']] }.to_h
|
123
|
+
else
|
124
|
+
@datasources = {}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
@datasources
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/grafana/panel.rb
CHANGED
@@ -1,39 +1,41 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
query_item
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Representation of one specific panel in a {Dashboard} instance.
|
5
|
+
class Panel
|
6
|
+
# @return [Dashboard] parent {Dashboard} object
|
7
|
+
attr_reader :dashboard
|
8
|
+
|
9
|
+
# @param model [Hash] converted JSON Hash of the panel
|
10
|
+
# @param dashboard [Dashboard] parent {Dashboard} object
|
11
|
+
def initialize(model, dashboard)
|
12
|
+
@model = model
|
13
|
+
@dashboard = dashboard
|
14
|
+
end
|
15
|
+
|
16
|
+
# @return [String] content of the requested field or +''+ if not found
|
17
|
+
def field(field)
|
18
|
+
return @model[field] if @model.key?(field)
|
19
|
+
|
20
|
+
''
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [String] panel ID
|
24
|
+
def id
|
25
|
+
@model['id']
|
26
|
+
end
|
27
|
+
|
28
|
+
# @return [String] SQL query string for the requested query letter
|
29
|
+
def query(query_letter)
|
30
|
+
query_item = @model['targets'].select { |item| item['refId'].to_s == query_letter.to_s }.first
|
31
|
+
raise QueryLetterDoesNotExistError.new(query_letter, self) if query_item.nil?
|
32
|
+
|
33
|
+
query_item['rawSql']
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [String] relative rendering URL for the panel, to create an image out of it
|
37
|
+
def render_url
|
38
|
+
"/render/d-solo/#{@dashboard.id}?panelId=#{@model['id']}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|