ruby-grafana-reporter 0.1.7 → 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.
- checksums.yaml +4 -4
- data/README.md +86 -245
- data/bin/ruby-grafana-reporter +3 -2
- data/lib/VERSION.rb +6 -3
- data/lib/grafana/abstract_datasource.rb +116 -0
- data/lib/grafana/dashboard.rb +75 -66
- data/lib/grafana/errors.rb +81 -61
- data/lib/grafana/grafana.rb +130 -131
- 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 +25 -0
- data/lib/grafana/graphite_datasource.rb +44 -0
- data/lib/grafana/image_rendering_datasource.rb +44 -0
- data/lib/grafana/panel.rb +47 -39
- data/lib/grafana/prometheus_datasource.rb +39 -0
- data/lib/grafana/sql_datasource.rb +65 -0
- data/lib/grafana/variable.rb +218 -259
- data/lib/grafana/webrequest.rb +71 -0
- data/lib/grafana_reporter/abstract_query.rb +401 -0
- data/lib/grafana_reporter/abstract_report.rb +163 -109
- data/lib/grafana_reporter/alerts_table_query.rb +44 -0
- data/lib/grafana_reporter/annotations_table_query.rb +43 -0
- data/lib/grafana_reporter/application/application.rb +162 -229
- data/lib/grafana_reporter/application/errors.rb +33 -30
- data/lib/grafana_reporter/application/webservice.rb +242 -0
- data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +90 -0
- data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +89 -0
- data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +76 -0
- data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +77 -0
- data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +72 -0
- data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +98 -0
- data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
- data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +23 -0
- data/lib/grafana_reporter/asciidoctor/report.rb +172 -159
- 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 +92 -0
- data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +88 -0
- data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
- data/lib/grafana_reporter/configuration.rb +310 -326
- data/lib/grafana_reporter/console_configuration_wizard.rb +319 -0
- data/lib/grafana_reporter/demo_report_wizard.rb +87 -0
- data/lib/grafana_reporter/errors.rb +81 -38
- data/lib/grafana_reporter/help.rb +447 -0
- data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
- data/lib/grafana_reporter/panel_image_query.rb +29 -0
- data/lib/grafana_reporter/panel_property_query.rb +22 -0
- data/lib/grafana_reporter/query_value_query.rb +79 -0
- data/lib/grafana_reporter/report_webhook.rb +35 -0
- data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +29 -27
- metadata +48 -60
- data/lib/grafana/abstract_panel_query.rb +0 -20
- data/lib/grafana/abstract_query.rb +0 -127
- data/lib/grafana/abstract_sql_query.rb +0 -42
- data/lib/grafana/panel_image_query.rb +0 -49
- data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -99
- data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
- data/lib/grafana_reporter/asciidoctor/errors.rb +0 -37
- data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -86
- data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -86
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -67
- data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -65
- data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -58
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -75
- data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -70
- data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -18
- data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -41
- data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -202
- data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -67
- data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -65
- data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -57
- data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -32
- data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -23
- data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -43
- data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -36
- data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -309
- data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -34
- data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -32
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the datasource interface to grafana alerts.
|
5
|
+
class GrafanaAlertsDatasource < 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
|
+
# query:
|
14
|
+
# state:
|
15
|
+
# folderId:
|
16
|
+
# dashboardQuery:
|
17
|
+
# dashboardTag:
|
18
|
+
# }
|
19
|
+
# @see AbstractDatasource#request
|
20
|
+
def request(query_description)
|
21
|
+
webrequest = query_description[:prepared_request]
|
22
|
+
webrequest.relative_url = "/api/alerts#{url_parameters(query_description)}"
|
23
|
+
|
24
|
+
result = webrequest.execute(query_description[:timeout])
|
25
|
+
|
26
|
+
json = JSON.parse(result.body)
|
27
|
+
|
28
|
+
content = []
|
29
|
+
begin
|
30
|
+
json.each { |item| content << item.fetch_values(*query_description[:raw_query]['columns'].split(',')) }
|
31
|
+
rescue KeyError => e
|
32
|
+
raise MalformedAttributeContentError.new(e.message, 'columns', query_description[:raw_query]['columns'])
|
33
|
+
end
|
34
|
+
|
35
|
+
result = {}
|
36
|
+
result[:header] = [query_description[:raw_query]['columns'].split(',')]
|
37
|
+
result[:content] = content
|
38
|
+
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def url_parameters(query_desc)
|
45
|
+
url_vars = {}
|
46
|
+
url_vars.merge!(query_desc[:raw_query].select do |k, _v|
|
47
|
+
k =~ /^(?:limit|dashboardId|panelId|query|state|folderId|dashboardQuery|dashboardTag)/
|
48
|
+
end)
|
49
|
+
url_vars['from'] = query_desc[:from] if query_desc[:from]
|
50
|
+
url_vars['to'] = query_desc[:to] if query_desc[:to]
|
51
|
+
url_params = URI.encode_www_form(url_vars.map { |k, v| [k, v.to_s] })
|
52
|
+
return '' if url_params.empty?
|
53
|
+
|
54
|
+
"?#{url_params}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -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,25 @@
|
|
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
|
+
{
|
20
|
+
header: [query_description[:raw_query][:property_name]],
|
21
|
+
content: [replace_variables(panel.field(property_name), query_description[:variables])]
|
22
|
+
}
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the interface to graphite datasources.
|
5
|
+
class GraphiteDatasource < AbstractDatasource
|
6
|
+
# +:raw_query+ needs to contain a Graphite query as String
|
7
|
+
# @see AbstractDatasource#request
|
8
|
+
def request(query_description)
|
9
|
+
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
10
|
+
|
11
|
+
request = {
|
12
|
+
body: URI.encode_www_form('from': DateTime.strptime(query_description[:from], '%Q').strftime('%H:%M_%Y%m%d'),
|
13
|
+
'until': DateTime.strptime(query_description[:to], '%Q').strftime('%H:%M_%Y%m%d'),
|
14
|
+
'format': 'json',
|
15
|
+
'target': replace_variables(query_description[:raw_query], query_description[:variables])),
|
16
|
+
content_type: 'application/x-www-form-urlencoded',
|
17
|
+
request: Net::HTTP::Post
|
18
|
+
}
|
19
|
+
|
20
|
+
webrequest = query_description[:prepared_request]
|
21
|
+
webrequest.relative_url = "/api/datasources/proxy/#{id}/render"
|
22
|
+
webrequest.options.merge!(request)
|
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
|
+
panel_query_target['target']
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# @see AbstractDatasource#preformat_response
|
36
|
+
def preformat_response(response_body)
|
37
|
+
# TODO: support multiple metrics as return types
|
38
|
+
{
|
39
|
+
header: %w[value time],
|
40
|
+
content: JSON.parse(response_body).first['datapoints']
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
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
|
data/lib/grafana/panel.rb
CHANGED
@@ -1,39 +1,47 @@
|
|
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
|
-
end
|
33
|
-
|
34
|
-
# @return [String]
|
35
|
-
def
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
+
attr_reader :model
|
9
|
+
|
10
|
+
# @param model [Hash] converted JSON Hash of the panel
|
11
|
+
# @param dashboard [Dashboard] parent {Dashboard} object
|
12
|
+
def initialize(model, dashboard)
|
13
|
+
@model = model
|
14
|
+
@dashboard = dashboard
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [String] content of the requested field or +''+ if not found
|
18
|
+
def field(field)
|
19
|
+
return @model[field] if @model.key?(field)
|
20
|
+
|
21
|
+
''
|
22
|
+
end
|
23
|
+
|
24
|
+
# @return [String] panel ID
|
25
|
+
def id
|
26
|
+
@model['id']
|
27
|
+
end
|
28
|
+
|
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
|
35
|
+
def query(query_letter)
|
36
|
+
query_item = @model['targets'].select { |item| item['refId'].to_s == query_letter.to_s }.first
|
37
|
+
raise QueryLetterDoesNotExistError.new(query_letter, self) unless query_item
|
38
|
+
|
39
|
+
datasource.raw_query_from_panel_model(query_item)
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [String] relative rendering URL for the panel, to create an image out of it
|
43
|
+
def render_url
|
44
|
+
"/render/d-solo/#{@dashboard.id}?panelId=#{@model['id']}"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# Implements the interface to Prometheus datasources.
|
5
|
+
class PrometheusDatasource < AbstractDatasource
|
6
|
+
# +:raw_query+ needs to contain a Prometheus query as String
|
7
|
+
# @see AbstractDatasource#request
|
8
|
+
def request(query_description)
|
9
|
+
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
10
|
+
|
11
|
+
url = "/api/datasources/proxy/#{id}/api/v1/query_range?"\
|
12
|
+
"start=#{query_description[:from]}&end=#{query_description[:to]}"\
|
13
|
+
"&query=#{replace_variables(query_description[:raw_query], query_description[:variables])}"
|
14
|
+
|
15
|
+
webrequest = query_description[:prepared_request]
|
16
|
+
webrequest.relative_url = url
|
17
|
+
webrequest.options.merge!({ request: Net::HTTP::Get })
|
18
|
+
|
19
|
+
result = webrequest.execute(query_description[:timeout])
|
20
|
+
preformat_response(result.body)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @see AbstractDatasource#raw_query_from_panel_model
|
24
|
+
def raw_query_from_panel_model(panel_query_target)
|
25
|
+
panel_query_target['expr']
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# @see AbstractDatasource#preformat_response
|
31
|
+
def preformat_response(response_body)
|
32
|
+
# TODO: support multiple metrics as return types
|
33
|
+
{
|
34
|
+
header: %w[time value],
|
35
|
+
content: JSON.parse(response_body)['data']['result'].first['values']
|
36
|
+
}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,65 @@
|
|
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
|
+
# +:raw_query+ needs to contain a SQL query as String in the respective database dialect
|
7
|
+
# @see AbstractDatasource#request
|
8
|
+
def request(query_description)
|
9
|
+
raise MissingSqlQueryError if query_description[:raw_query].nil?
|
10
|
+
|
11
|
+
sql = replace_variables(query_description[:raw_query], query_description[:variables])
|
12
|
+
request = {
|
13
|
+
body: {
|
14
|
+
from: query_description[:from],
|
15
|
+
to: query_description[:to],
|
16
|
+
queries: [rawSql: prepare_sql(sql), datasourceId: id, format: 'table']
|
17
|
+
}.to_json,
|
18
|
+
request: Net::HTTP::Post
|
19
|
+
}
|
20
|
+
|
21
|
+
webrequest = query_description[:prepared_request]
|
22
|
+
webrequest.relative_url = '/api/tsdb/query'
|
23
|
+
webrequest.options.merge!(request)
|
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['rawSql']
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def preformat_response(response_body)
|
37
|
+
results = {}
|
38
|
+
results.default = []
|
39
|
+
|
40
|
+
JSON.parse(response_body)['results'].each_value do |query_result|
|
41
|
+
if query_result.key?('error')
|
42
|
+
results[:header] = results[:header] << ['SQL Error']
|
43
|
+
results[:content] = [[query_result['error']]]
|
44
|
+
|
45
|
+
elsif query_result['tables']
|
46
|
+
query_result['tables'].each do |table|
|
47
|
+
results[:header] = results[:header] << table['columns'].map { |header| header['text'] }
|
48
|
+
results[:content] = table['rows']
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
results
|
55
|
+
end
|
56
|
+
|
57
|
+
def prepare_sql(sql)
|
58
|
+
# remove comments in query
|
59
|
+
sql.gsub!(/--[^\r\n]*(?:[\r\n]+|$)/, ' ')
|
60
|
+
sql.gsub!(/\r\n/, ' ')
|
61
|
+
sql.gsub!(/\n/, ' ')
|
62
|
+
sql
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/grafana/variable.rb
CHANGED
@@ -1,259 +1,218 @@
|
|
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
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
"
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
when ''
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
end
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
when 'ww'
|
220
|
-
'%U'
|
221
|
-
when 'W'
|
222
|
-
'%-V'
|
223
|
-
when 'WW'
|
224
|
-
'%V'
|
225
|
-
when 'YY'
|
226
|
-
'%y'
|
227
|
-
when 'YYYY'
|
228
|
-
'%Y'
|
229
|
-
when 'A'
|
230
|
-
'%p'
|
231
|
-
when 'a'
|
232
|
-
'%P'
|
233
|
-
when 'H'
|
234
|
-
'%-H'
|
235
|
-
when 'HH'
|
236
|
-
'%H'
|
237
|
-
when 'h'
|
238
|
-
'%-I'
|
239
|
-
when 'hh'
|
240
|
-
'%I'
|
241
|
-
when 'm'
|
242
|
-
'%-M'
|
243
|
-
when 'mm'
|
244
|
-
'%M'
|
245
|
-
when 's'
|
246
|
-
'%-S'
|
247
|
-
when 'ss'
|
248
|
-
'%S'
|
249
|
-
when 'X'
|
250
|
-
'%s'
|
251
|
-
else
|
252
|
-
match
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
Time.at((Float(value) / 1000).to_i).strftime(format_string)
|
257
|
-
end
|
258
|
-
end
|
259
|
-
end
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grafana
|
4
|
+
# This class contains a representation of
|
5
|
+
# {https://grafana.com/docs/grafana/latest/variables/templates-and-variables grafana variables},
|
6
|
+
# aka grafana templates.
|
7
|
+
#
|
8
|
+
# The main need therefore rises in order to replace variables properly in different
|
9
|
+
# texts, e.g. SQL statements or results.
|
10
|
+
class Variable
|
11
|
+
attr_reader :name, :text, :raw_value
|
12
|
+
|
13
|
+
# Translation table to support {https://momentjs.com/docs/#/displaying/}.
|
14
|
+
DATE_MATCHES = { 'M' => '%-m', 'MM' => '%m', 'MMM' => '%b', 'MMMM' => '%B',
|
15
|
+
'D' => '%-d', 'DD' => '%d', 'DDD' => '%-j', 'DDDD' => '%j',
|
16
|
+
'd' => '%w', 'ddd' => '%a', 'dddd' => '%A',
|
17
|
+
'YY' => '%y', 'YYYY' => '%Y',
|
18
|
+
'h' => '%-I', 'hh' => '%I',
|
19
|
+
'H' => '%-H', 'HH' => '%H',
|
20
|
+
'm' => '%-M', 'mm' => '%M',
|
21
|
+
's' => '%-S', 'ss' => '%S',
|
22
|
+
'w' => '%-U', 'ww' => '%U',
|
23
|
+
'W' => '%-V', 'WW' => '%V',
|
24
|
+
'a' => '%P',
|
25
|
+
'A' => '%p',
|
26
|
+
'e' => '%w',
|
27
|
+
'E' => '%u',
|
28
|
+
'X' => '%s' }.freeze
|
29
|
+
|
30
|
+
# @param config_or_value [Hash, Object] configuration hash of a variable out of an {Dashboard} instance
|
31
|
+
# or a value of any kind.
|
32
|
+
def initialize(config_or_value)
|
33
|
+
if config_or_value.is_a? Hash
|
34
|
+
@config = config_or_value
|
35
|
+
@name = @config['name']
|
36
|
+
unless @config['current'].nil?
|
37
|
+
@raw_value = @config['current']['value']
|
38
|
+
@text = @config['current']['text']
|
39
|
+
end
|
40
|
+
else
|
41
|
+
@config = {}
|
42
|
+
@raw_value = config_or_value
|
43
|
+
@text = config_or_value.to_s
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Returns the stored value formatted according the given format.
|
48
|
+
#
|
49
|
+
# Supported formats are: +csv+, +distributed+, +doublequote+, +json+, +percentencode+, +pipe+, +raw+,
|
50
|
+
# +regex+, +singlequote+, +sqlstring+, +lucene+, +date+ or +glob+ (default)
|
51
|
+
#
|
52
|
+
# For details see {https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options
|
53
|
+
# Grafana Advanced variable format options}.
|
54
|
+
#
|
55
|
+
# For details of +date+ format, see
|
56
|
+
# {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to
|
57
|
+
# Grafana global variables $__from and $__to}.
|
58
|
+
# Please note that input for +date+ format is unixtime in milliseconds.
|
59
|
+
#
|
60
|
+
# @param format [String] desired format
|
61
|
+
# @return [String] value of stored variable according the specified format
|
62
|
+
def value_formatted(format = '')
|
63
|
+
value = @raw_value
|
64
|
+
|
65
|
+
# handle value 'All' properly
|
66
|
+
# TODO: fix check for selection of All properly
|
67
|
+
if (value == 'All') || (@text == 'All')
|
68
|
+
if !@config['options'].empty?
|
69
|
+
value = @config['options'].map { |item| item['value'] }
|
70
|
+
elsif !@config['query'].empty?
|
71
|
+
# TODO: replace variables in this query, too
|
72
|
+
return @config['query']
|
73
|
+
# TODO: handle 'All' value properly for query attributes
|
74
|
+
else
|
75
|
+
# TODO: how to handle All selection properly at this point?
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
case format
|
80
|
+
when 'csv'
|
81
|
+
return value.join(',').to_s if multi?
|
82
|
+
|
83
|
+
value.to_s
|
84
|
+
|
85
|
+
when 'distributed'
|
86
|
+
return value.join(",#{name}=") if multi?
|
87
|
+
|
88
|
+
value
|
89
|
+
when 'doublequote'
|
90
|
+
if multi?
|
91
|
+
value = value.map { |item| "\"#{item.gsub(/\\/, '\\\\').gsub(/"/, '\\"')}\"" }
|
92
|
+
return value.join(',')
|
93
|
+
end
|
94
|
+
"\"#{value.gsub(/"/, '\\"')}\""
|
95
|
+
|
96
|
+
when 'json'
|
97
|
+
if multi?
|
98
|
+
value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\\0')}\"" }
|
99
|
+
return "[#{value.join(',')}]"
|
100
|
+
end
|
101
|
+
"\"#{value.gsub(/"/, '\\"')}\""
|
102
|
+
|
103
|
+
when 'percentencode'
|
104
|
+
value = "{#{value.join(',')}}" if multi?
|
105
|
+
ERB::Util.url_encode(value)
|
106
|
+
|
107
|
+
when 'pipe'
|
108
|
+
return value.join('|') if multi?
|
109
|
+
|
110
|
+
value
|
111
|
+
|
112
|
+
when 'raw'
|
113
|
+
return "{#{value.join(',')}}" if multi?
|
114
|
+
|
115
|
+
value
|
116
|
+
|
117
|
+
when 'regex'
|
118
|
+
if multi?
|
119
|
+
value = value.map { |item| item.gsub(%r{[/$.|\\]}, '\\\\\0') }
|
120
|
+
return "(#{value.join('|')})"
|
121
|
+
end
|
122
|
+
value.gsub(%r{[/$.|\\]}, '\\\\\0')
|
123
|
+
|
124
|
+
when 'singlequote'
|
125
|
+
if multi?
|
126
|
+
value = value.map { |item| "'#{item.gsub(/'/, '\\\\\0')}'" }
|
127
|
+
return value.join(',')
|
128
|
+
end
|
129
|
+
"'#{value.gsub(/'/, '\\\\\0')}'"
|
130
|
+
|
131
|
+
when 'sqlstring'
|
132
|
+
if multi?
|
133
|
+
value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
|
134
|
+
return value.join(',')
|
135
|
+
end
|
136
|
+
"'#{value.gsub(/'/, "''")}'"
|
137
|
+
|
138
|
+
when 'lucene'
|
139
|
+
if multi?
|
140
|
+
value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\\0')}\"" }
|
141
|
+
return "(#{value.join(' OR ')})"
|
142
|
+
end
|
143
|
+
value.gsub(%r{[" |=/\\]}, '\\\\\0')
|
144
|
+
|
145
|
+
when /^date(?::(?<format>.*))?$/
|
146
|
+
# TODO: validate how grafana handles multivariables with date format
|
147
|
+
get_date_formatted(value, Regexp.last_match(1))
|
148
|
+
|
149
|
+
when ''
|
150
|
+
# default
|
151
|
+
if multi?
|
152
|
+
value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
|
153
|
+
return value.join(',')
|
154
|
+
end
|
155
|
+
value.gsub(/'/, "''")
|
156
|
+
|
157
|
+
else
|
158
|
+
# glob and all unknown
|
159
|
+
# TODO add check for array value properly for all cases
|
160
|
+
return "{#{value.join(',')}}" if multi? && value.is_a?(Array)
|
161
|
+
|
162
|
+
value
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
# @return [Boolean] true, if the value can contain multiple selections, i.e. is an Array
|
167
|
+
def multi?
|
168
|
+
return @config['multi'] unless @config['multi'].nil?
|
169
|
+
|
170
|
+
@raw_value.is_a? Array
|
171
|
+
end
|
172
|
+
|
173
|
+
# @return [Object] raw value of the variable
|
174
|
+
def raw_value=(new_val)
|
175
|
+
@raw_value = new_val
|
176
|
+
@raw_value = @raw_value.to_s unless @raw_value.is_a?(Array)
|
177
|
+
new_text = @raw_value
|
178
|
+
if @config['options']
|
179
|
+
val = @config['options'].select { |item| item['value'] == @raw_value }
|
180
|
+
new_text = val.first['text'] unless val.empty?
|
181
|
+
end
|
182
|
+
@text = new_text
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
|
187
|
+
# Realize time formatting according
|
188
|
+
# {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to}
|
189
|
+
# and {https://momentjs.com/docs/#/displaying/}.
|
190
|
+
def get_date_formatted(value, format)
|
191
|
+
return (Float(value) / 1000).to_i.to_s if format == 'seconds'
|
192
|
+
return Time.at((Float(value) / 1000).to_i).utc.iso8601(3) if !format || (format == 'iso')
|
193
|
+
|
194
|
+
# build array of known matches
|
195
|
+
matches = []
|
196
|
+
work_string = format
|
197
|
+
until work_string.empty?
|
198
|
+
tmp = work_string.scan(/^(?:M{1,4}|D{1,4}|d{1,4}|e|E|w{1,2}|W{1,2}|Y{4}|Y{2}|A|a|H{1,2}|
|
199
|
+
h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/x)
|
200
|
+
if tmp.empty?
|
201
|
+
matches << work_string[0]
|
202
|
+
work_string.sub!(/^#{work_string[0]}/, '')
|
203
|
+
else
|
204
|
+
matches << tmp[0]
|
205
|
+
work_string.sub!(/^#{tmp[0]}/, '')
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
format_string = ''.dup
|
210
|
+
matches.each do |match|
|
211
|
+
replacement = DATE_MATCHES[match]
|
212
|
+
format_string << (replacement || match)
|
213
|
+
end
|
214
|
+
|
215
|
+
Time.at((Float(value) / 1000).to_i).strftime(format_string)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|