ruby-grafana-reporter 0.3.0 → 0.4.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 +4 -4
- data/README.md +337 -170
- data/bin/ruby-grafana-reporter +5 -5
- data/lib/VERSION.rb +3 -2
- data/lib/grafana/abstract_datasource.rb +149 -0
- data/lib/grafana/dashboard.rb +1 -3
- data/lib/grafana/errors.rb +20 -5
- data/lib/grafana/grafana.rb +52 -57
- data/lib/grafana/grafana_alerts_datasource.rb +57 -0
- data/lib/grafana/grafana_annotations_datasource.rb +56 -0
- data/lib/grafana/grafana_property_datasource.rb +37 -0
- data/lib/grafana/graphite_datasource.rb +72 -0
- data/lib/grafana/image_rendering_datasource.rb +44 -0
- data/lib/grafana/influxdb_datasource.rb +70 -0
- data/lib/grafana/panel.rb +10 -4
- data/lib/grafana/prometheus_datasource.rb +67 -0
- data/lib/grafana/sql_datasource.rb +70 -0
- data/lib/grafana/unsupported_datasource.rb +7 -0
- data/lib/grafana/variable.rb +27 -21
- data/lib/grafana/webrequest.rb +71 -0
- data/lib/grafana_reporter/abstract_query.rb +478 -0
- data/lib/grafana_reporter/abstract_report.rb +152 -18
- data/lib/grafana_reporter/abstract_table_format_strategy.rb +34 -0
- data/lib/grafana_reporter/alerts_table_query.rb +43 -0
- data/lib/grafana_reporter/annotations_table_query.rb +42 -0
- data/lib/grafana_reporter/application/application.rb +28 -25
- data/lib/grafana_reporter/application/webservice.rb +80 -39
- data/lib/grafana_reporter/asciidoctor/adoc_plain_table_format_strategy.rb +25 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +92 -0
- data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +91 -0
- data/lib/grafana_reporter/asciidoctor/help.rb +336 -313
- data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +78 -0
- data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +80 -0
- data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +74 -0
- data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +99 -0
- data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
- data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +50 -0
- data/lib/grafana_reporter/asciidoctor/report.rb +41 -82
- data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
- data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
- data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +94 -0
- data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +90 -0
- data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
- data/lib/grafana_reporter/configuration.rb +26 -8
- data/lib/grafana_reporter/console_configuration_wizard.rb +109 -67
- data/lib/grafana_reporter/csv_table_format_strategy.rb +23 -0
- data/lib/grafana_reporter/demo_report_wizard.rb +104 -0
- data/lib/grafana_reporter/erb/demo_report_builder.rb +46 -0
- data/lib/grafana_reporter/erb/report.rb +36 -0
- data/lib/grafana_reporter/erb/report_jail.rb +21 -0
- data/lib/grafana_reporter/errors.rb +57 -0
- data/lib/grafana_reporter/logger/{two_way_logger.rb → two_way_delegate_logger.rb} +1 -1
- data/lib/grafana_reporter/panel_image_query.rb +25 -0
- data/lib/grafana_reporter/panel_property_query.rb +22 -0
- data/lib/grafana_reporter/query_value_query.rb +61 -0
- data/lib/grafana_reporter/report_webhook.rb +39 -0
- data/lib/ruby_grafana_extension.rb +8 -0
- data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +1 -3
- metadata +49 -38
- data/lib/grafana/abstract_panel_query.rb +0 -22
- data/lib/grafana/abstract_query.rb +0 -132
- data/lib/grafana/abstract_sql_query.rb +0 -51
- data/lib/grafana/panel_image_query.rb +0 -52
- data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -101
- data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
- data/lib/grafana_reporter/asciidoctor/errors.rb +0 -40
- data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -92
- data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -91
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -69
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -68
- data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -61
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -78
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -73
- data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -20
- data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -43
- data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -30
- data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -70
- data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -66
- data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -88
- data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -36
- data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -28
- data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -44
- data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -40
- data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -312
- data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -42
- data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -44
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the datasource interface to grafana annotations.
|
5
|
+
class GrafanaAnnotationsDatasource < AbstractDatasource
|
6
|
+
# +:raw_query+ needs to contain a Hash with the following structure:
|
7
|
+
#
|
8
|
+
# {
|
9
|
+
# dashboardId: Dashboard ID as String or nil
|
10
|
+
# panelId: Panel ID as String or nil
|
11
|
+
# columns:
|
12
|
+
# limit:
|
13
|
+
# alertId:
|
14
|
+
# userId:
|
15
|
+
# type:
|
16
|
+
# tags:
|
17
|
+
# }
|
18
|
+
# @see AbstractDatasource#request
|
19
|
+
def request(query_description)
|
20
|
+
webrequest = query_description[:prepared_request]
|
21
|
+
webrequest.relative_url = "/api/annotations#{url_parameters(query_description)}"
|
22
|
+
|
23
|
+
result = webrequest.execute(query_description[:timeout])
|
24
|
+
|
25
|
+
json = JSON.parse(result.body)
|
26
|
+
|
27
|
+
content = []
|
28
|
+
begin
|
29
|
+
json.each { |item| content << item.fetch_values(*query_description[:raw_query]['columns'].split(',')) }
|
30
|
+
rescue KeyError => e
|
31
|
+
raise MalformedAttributeContentError.new(e.message, 'columns', query_description[:raw_query]['columns'])
|
32
|
+
end
|
33
|
+
|
34
|
+
result = {}
|
35
|
+
result[:header] = [query_description[:raw_query]['columns'].split(',')]
|
36
|
+
result[:content] = content
|
37
|
+
|
38
|
+
result
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def url_parameters(query_desc)
|
44
|
+
url_vars = {}
|
45
|
+
url_vars.merge!(query_desc[:raw_query].select do |k, _v|
|
46
|
+
k =~ /^(?:limit|alertId|dashboardId|panelId|userId|type|tags)/
|
47
|
+
end)
|
48
|
+
url_vars['from'] = query_desc[:from] if query_desc[:from]
|
49
|
+
url_vars['to'] = query_desc[:to] if query_desc[:to]
|
50
|
+
url_params = URI.encode_www_form(url_vars.map { |k, v| [k, v.to_s] })
|
51
|
+
return '' if url_params.empty?
|
52
|
+
|
53
|
+
"?#{url_params}"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the datasource interface to grafana model properties.
|
5
|
+
class GrafanaPropertyDatasource < AbstractDatasource
|
6
|
+
# +:raw_query+ needs to contain a Hash with the following structure:
|
7
|
+
#
|
8
|
+
# {
|
9
|
+
# property_name: Name of the queried property as String
|
10
|
+
# panel: {Panel} object to query
|
11
|
+
# }
|
12
|
+
# @see AbstractDatasource#request
|
13
|
+
def request(query_description)
|
14
|
+
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
15
|
+
|
16
|
+
panel = query_description[:raw_query][:panel]
|
17
|
+
property_name = query_description[:raw_query][:property_name]
|
18
|
+
|
19
|
+
return "Panel property '#{property_name}' does not exist for panel '#{panel.id}'" unless panel.field(property_name)
|
20
|
+
|
21
|
+
{
|
22
|
+
header: [query_description[:raw_query][:property_name]],
|
23
|
+
content: [replace_variables(panel.field(property_name), query_description[:variables])]
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
# @see AbstractDatasource#default_variable_format
|
28
|
+
def default_variable_format
|
29
|
+
'glob'
|
30
|
+
end
|
31
|
+
|
32
|
+
# @see AbstractDatasource#name
|
33
|
+
def name
|
34
|
+
self.class.to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the interface to graphite datasources.
|
5
|
+
class GraphiteDatasource < AbstractDatasource
|
6
|
+
# @see AbstractDatasource#handles?
|
7
|
+
def self.handles?(model)
|
8
|
+
tmp = new(model)
|
9
|
+
tmp.type == 'graphite'
|
10
|
+
end
|
11
|
+
|
12
|
+
# +:raw_query+ needs to contain a Graphite query as String
|
13
|
+
# @see AbstractDatasource#request
|
14
|
+
def request(query_description)
|
15
|
+
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
16
|
+
|
17
|
+
request = {
|
18
|
+
body: URI.encode_www_form('from': DateTime.strptime(query_description[:from], '%Q').strftime('%H:%M_%Y%m%d'),
|
19
|
+
'until': DateTime.strptime(query_description[:to], '%Q').strftime('%H:%M_%Y%m%d'),
|
20
|
+
'format': 'json',
|
21
|
+
'target': replace_variables(query_description[:raw_query], query_description[:variables])),
|
22
|
+
content_type: 'application/x-www-form-urlencoded',
|
23
|
+
request: Net::HTTP::Post
|
24
|
+
}
|
25
|
+
|
26
|
+
webrequest = query_description[:prepared_request]
|
27
|
+
webrequest.relative_url = "/api/datasources/proxy/#{id}/render"
|
28
|
+
webrequest.options.merge!(request)
|
29
|
+
|
30
|
+
result = webrequest.execute(query_description[:timeout])
|
31
|
+
preformat_response(result.body)
|
32
|
+
end
|
33
|
+
|
34
|
+
# @see AbstractDatasource#raw_query_from_panel_model
|
35
|
+
def raw_query_from_panel_model(panel_query_target)
|
36
|
+
panel_query_target['target']
|
37
|
+
end
|
38
|
+
|
39
|
+
# @see AbstractDatasource#default_variable_format
|
40
|
+
def default_variable_format
|
41
|
+
'glob'
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# @see AbstractDatasource#preformat_response
|
47
|
+
def preformat_response(response_body)
|
48
|
+
json = JSON.parse(response_body)
|
49
|
+
|
50
|
+
header = ['time']
|
51
|
+
content = {}
|
52
|
+
|
53
|
+
# keep sorting, if json has only one target item, otherwise merge results and return
|
54
|
+
# as a time sorted array
|
55
|
+
return { header: header << json.first['target'], content: json.first['datapoints'].map! { |item| [item[1], item[0]] } } if json.length == 1
|
56
|
+
|
57
|
+
# TODO: show warning if results may be sorted different
|
58
|
+
json.each_index do |i|
|
59
|
+
header << json[i]['target']
|
60
|
+
tmp = json[i]['datapoints'].map! { |item| [item[1], item[0]] }.to_h
|
61
|
+
tmp.each_key { |key| content[key] = Array.new(json.length) unless content[key] }
|
62
|
+
|
63
|
+
content.merge!(tmp) do |_key, old, new|
|
64
|
+
old[i] = new
|
65
|
+
old
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
{ header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the interface to image rendering datasources.
|
5
|
+
class ImageRenderingDatasource < AbstractDatasource
|
6
|
+
# +:raw_query+ needs to contain a Hash with the following structure:
|
7
|
+
#
|
8
|
+
# {
|
9
|
+
# panel: {Panel} which shall be rendered
|
10
|
+
# }
|
11
|
+
# @see AbstractDatasource#request
|
12
|
+
def request(query_description)
|
13
|
+
webrequest = query_description[:prepared_request]
|
14
|
+
webrequest.relative_url = query_description[:raw_query][:panel].render_url + url_params(query_description)
|
15
|
+
webrequest.options.merge!({ accept: 'image/png' })
|
16
|
+
|
17
|
+
result = webrequest.execute
|
18
|
+
|
19
|
+
{ header: ['image'], content: [result.body] }
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def url_params(query_desc)
|
25
|
+
url_vars = query_desc[:variables].select { |k, _v| k =~ /^(?:timeout|height|width|theme|fullscreen|var-.+)$/ }
|
26
|
+
url_vars = default_vars.merge(url_vars)
|
27
|
+
url_vars['from'] = Variable.new(query_desc[:from])
|
28
|
+
url_vars['to'] = Variable.new(query_desc[:to])
|
29
|
+
result = URI.encode_www_form(url_vars.map { |k, v| [k, v.raw_value.to_s] })
|
30
|
+
|
31
|
+
return '' if result.empty?
|
32
|
+
|
33
|
+
"&#{result}"
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_vars
|
37
|
+
{
|
38
|
+
'fullscreen' => Variable.new(true),
|
39
|
+
'theme' => Variable.new('light'),
|
40
|
+
'timeout' => Variable.new(60)
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the interface to Prometheus datasources.
|
5
|
+
class InfluxDbDatasource < AbstractDatasource
|
6
|
+
# @see AbstractDatasource#handles?
|
7
|
+
def self.handles?(model)
|
8
|
+
tmp = new(model)
|
9
|
+
tmp.type == 'influxdb'
|
10
|
+
end
|
11
|
+
|
12
|
+
# +:database+ needs to contain the InfluxDb database name
|
13
|
+
# +:raw_query+ needs to contain a InfluxDb query as String
|
14
|
+
# @see AbstractDatasource#request
|
15
|
+
def request(query_description)
|
16
|
+
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
17
|
+
|
18
|
+
url = "/api/datasources/proxy/#{id}/query?db=#{@model['database']}&q=#{ERB::Util.url_encode(query_description[:raw_query])}&epoch=ms"
|
19
|
+
|
20
|
+
webrequest = query_description[:prepared_request]
|
21
|
+
webrequest.relative_url = url
|
22
|
+
webrequest.options.merge!({ request: Net::HTTP::Get })
|
23
|
+
|
24
|
+
result = webrequest.execute(query_description[:timeout])
|
25
|
+
preformat_response(result.body)
|
26
|
+
end
|
27
|
+
|
28
|
+
# @see AbstractDatasource#raw_query_from_panel_model
|
29
|
+
def raw_query_from_panel_model(panel_query_target)
|
30
|
+
return panel_query_target['query'] if panel_query_target['rawQuery']
|
31
|
+
|
32
|
+
# TODO: support composed queries
|
33
|
+
raise ComposedQueryNotSupportedError, self
|
34
|
+
end
|
35
|
+
|
36
|
+
# @see AbstractDatasource#default_variable_format
|
37
|
+
def default_variable_format
|
38
|
+
'regex'
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
# @see AbstractDatasource#preformat_response
|
44
|
+
def preformat_response(response_body)
|
45
|
+
# TODO: how to handle multiple query results?
|
46
|
+
json = JSON.parse(response_body)['results'].first['series']
|
47
|
+
|
48
|
+
header = ['time']
|
49
|
+
content = {}
|
50
|
+
|
51
|
+
# keep sorting, if json has only one target item, otherwise merge results and return
|
52
|
+
# as a time sorted array
|
53
|
+
return { header: header << "#{json.first['name']} #{json.first['columns'][1]} (#{json.first['tags']})", content: json.first['values'] } if json.length == 1
|
54
|
+
|
55
|
+
# TODO: show warning here, as results may be sorted different
|
56
|
+
json.each_index do |i|
|
57
|
+
header << "#{json[i]['name']} #{json[i]['columns'][1]} (#{json[i]['tags']})"
|
58
|
+
tmp = json[i]['values'].to_h
|
59
|
+
tmp.each_key { |key| content[key] = Array.new(json.length) unless content[key] }
|
60
|
+
|
61
|
+
content.merge!(tmp) do |_key, old, new|
|
62
|
+
old[i] = new
|
63
|
+
old
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
{ header: header, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
data/lib/grafana/panel.rb
CHANGED
@@ -5,6 +5,7 @@ module Grafana
|
|
5
5
|
class Panel
|
6
6
|
# @return [Dashboard] parent {Dashboard} object
|
7
7
|
attr_reader :dashboard
|
8
|
+
attr_reader :model
|
8
9
|
|
9
10
|
# @param model [Hash] converted JSON Hash of the panel
|
10
11
|
# @param dashboard [Dashboard] parent {Dashboard} object
|
@@ -17,7 +18,7 @@ module Grafana
|
|
17
18
|
def field(field)
|
18
19
|
return @model[field] if @model.key?(field)
|
19
20
|
|
20
|
-
|
21
|
+
nil
|
21
22
|
end
|
22
23
|
|
23
24
|
# @return [String] panel ID
|
@@ -25,12 +26,17 @@ module Grafana
|
|
25
26
|
@model['id']
|
26
27
|
end
|
27
28
|
|
28
|
-
# @return [
|
29
|
+
# @return [Datasource] datasource object specified for the current panel
|
30
|
+
def datasource
|
31
|
+
dashboard.grafana.datasource_by_name(@model['datasource'])
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [String] query string for the requested query letter
|
29
35
|
def query(query_letter)
|
30
36
|
query_item = @model['targets'].select { |item| item['refId'].to_s == query_letter.to_s }.first
|
31
|
-
raise QueryLetterDoesNotExistError.new(query_letter, self)
|
37
|
+
raise QueryLetterDoesNotExistError.new(query_letter, self) unless query_item
|
32
38
|
|
33
|
-
query_item
|
39
|
+
datasource.raw_query_from_panel_model(query_item)
|
34
40
|
end
|
35
41
|
|
36
42
|
# @return [String] relative rendering URL for the panel, to create an image out of it
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the interface to Prometheus datasources.
|
5
|
+
class PrometheusDatasource < AbstractDatasource
|
6
|
+
# @see AbstractDatasource#handles?
|
7
|
+
def self.handles?(model)
|
8
|
+
tmp = new(model)
|
9
|
+
tmp.type == 'prometheus'
|
10
|
+
end
|
11
|
+
|
12
|
+
# +:raw_query+ needs to contain a Prometheus query as String
|
13
|
+
# @see AbstractDatasource#request
|
14
|
+
def request(query_description)
|
15
|
+
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
16
|
+
|
17
|
+
url = "/api/datasources/proxy/#{id}/api/v1/query_range?"\
|
18
|
+
"start=#{query_description[:from]}&end=#{query_description[:to]}"\
|
19
|
+
"&query=#{replace_variables(query_description[:raw_query], query_description[:variables])}"
|
20
|
+
|
21
|
+
webrequest = query_description[:prepared_request]
|
22
|
+
webrequest.relative_url = url
|
23
|
+
webrequest.options.merge!({ request: Net::HTTP::Get })
|
24
|
+
|
25
|
+
result = webrequest.execute(query_description[:timeout])
|
26
|
+
preformat_response(result.body)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @see AbstractDatasource#raw_query_from_panel_model
|
30
|
+
def raw_query_from_panel_model(panel_query_target)
|
31
|
+
panel_query_target['expr']
|
32
|
+
end
|
33
|
+
|
34
|
+
# @see AbstractDatasource#default_variable_format
|
35
|
+
def default_variable_format
|
36
|
+
'regex'
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# @see AbstractDatasource#preformat_response
|
42
|
+
def preformat_response(response_body)
|
43
|
+
json = JSON.parse(response_body)['data']['result']
|
44
|
+
|
45
|
+
headers = ['time']
|
46
|
+
content = {}
|
47
|
+
|
48
|
+
# keep sorting, if json has only one target item, otherwise merge results and return
|
49
|
+
# as a time sorted array
|
50
|
+
return { header: headers << json.first['metric']['mode'], content: json.first['values'] } if json.length == 1
|
51
|
+
|
52
|
+
# TODO: show warning if results may be sorted different
|
53
|
+
json.each_index do |i|
|
54
|
+
headers += [json[i]['metric']['mode']]
|
55
|
+
tmp = json[i]['values'].to_h
|
56
|
+
tmp.each_key { |key| content[key] = Array.new(json.length) unless content[key] }
|
57
|
+
|
58
|
+
content.merge!(tmp) do |_key, old, new|
|
59
|
+
old[i] = new
|
60
|
+
old
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
{ header: headers, content: content.to_a.map(&:flatten).sort { |a, b| a[0] <=> b[0] } }
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the interface to all SQL based datasources (tested with PostgreSQL and MariaDB/MySQL).
|
5
|
+
class SqlDatasource < AbstractDatasource
|
6
|
+
# @see AbstractDatasource#handles?
|
7
|
+
def self.handles?(model)
|
8
|
+
tmp = new(model)
|
9
|
+
tmp.category == 'sql'
|
10
|
+
end
|
11
|
+
|
12
|
+
# +:raw_query+ needs to contain a SQL query as String in the respective database dialect
|
13
|
+
# @see AbstractDatasource#request
|
14
|
+
def request(query_description)
|
15
|
+
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
16
|
+
|
17
|
+
sql = replace_variables(query_description[:raw_query], query_description[:variables])
|
18
|
+
request = {
|
19
|
+
body: {
|
20
|
+
from: query_description[:from],
|
21
|
+
to: query_description[:to],
|
22
|
+
queries: [rawSql: sql, datasourceId: id, format: 'table']
|
23
|
+
}.to_json,
|
24
|
+
request: Net::HTTP::Post
|
25
|
+
}
|
26
|
+
|
27
|
+
webrequest = query_description[:prepared_request]
|
28
|
+
webrequest.relative_url = '/api/tsdb/query'
|
29
|
+
webrequest.options.merge!(request)
|
30
|
+
|
31
|
+
result = webrequest.execute(query_description[:timeout])
|
32
|
+
preformat_response(result.body)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Currently all composed SQL queries are saved in the dashboard as rawSql, so no conversion
|
36
|
+
# necessary here.
|
37
|
+
# @see AbstractDatasource#raw_query_from_panel_model
|
38
|
+
def raw_query_from_panel_model(panel_query_target)
|
39
|
+
panel_query_target['rawSql']
|
40
|
+
end
|
41
|
+
|
42
|
+
# @see AbstractDatasource#default_variable_format
|
43
|
+
def default_variable_format
|
44
|
+
'glob'
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def preformat_response(response_body)
|
50
|
+
results = {}
|
51
|
+
results.default = []
|
52
|
+
|
53
|
+
JSON.parse(response_body)['results'].each_value do |query_result|
|
54
|
+
if query_result.key?('error')
|
55
|
+
results[:header] = results[:header] + ['SQL Error']
|
56
|
+
results[:content] = [[query_result['error']]]
|
57
|
+
|
58
|
+
elsif query_result['tables']
|
59
|
+
query_result['tables'].each do |table|
|
60
|
+
results[:header] = results[:header] + table['columns'].map { |header| header['text'] }
|
61
|
+
results[:content] = table['rows']
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
results
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|