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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +86 -245
  3. data/bin/ruby-grafana-reporter +3 -2
  4. data/lib/VERSION.rb +6 -3
  5. data/lib/grafana/abstract_datasource.rb +116 -0
  6. data/lib/grafana/dashboard.rb +75 -66
  7. data/lib/grafana/errors.rb +81 -61
  8. data/lib/grafana/grafana.rb +130 -131
  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 +47 -39
  15. data/lib/grafana/prometheus_datasource.rb +39 -0
  16. data/lib/grafana/sql_datasource.rb +65 -0
  17. data/lib/grafana/variable.rb +218 -259
  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 +163 -109
  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 +162 -229
  24. data/lib/grafana_reporter/application/errors.rb +33 -30
  25. data/lib/grafana_reporter/application/webservice.rb +242 -0
  26. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +90 -0
  27. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +89 -0
  28. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +76 -0
  29. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +77 -0
  30. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +72 -0
  31. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +98 -0
  32. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +93 -0
  33. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +23 -0
  34. data/lib/grafana_reporter/asciidoctor/report.rb +172 -159
  35. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  36. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  37. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +92 -0
  38. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +88 -0
  39. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  40. data/lib/grafana_reporter/configuration.rb +310 -326
  41. data/lib/grafana_reporter/console_configuration_wizard.rb +319 -0
  42. data/lib/grafana_reporter/demo_report_wizard.rb +87 -0
  43. data/lib/grafana_reporter/errors.rb +81 -38
  44. data/lib/grafana_reporter/help.rb +447 -0
  45. data/lib/grafana_reporter/logger/two_way_logger.rb +58 -52
  46. data/lib/grafana_reporter/panel_image_query.rb +29 -0
  47. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  48. data/lib/grafana_reporter/query_value_query.rb +79 -0
  49. data/lib/grafana_reporter/report_webhook.rb +35 -0
  50. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +29 -27
  51. metadata +48 -60
  52. data/lib/grafana/abstract_panel_query.rb +0 -20
  53. data/lib/grafana/abstract_query.rb +0 -127
  54. data/lib/grafana/abstract_sql_query.rb +0 -42
  55. data/lib/grafana/panel_image_query.rb +0 -49
  56. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -99
  57. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -96
  58. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -37
  59. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -86
  60. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -86
  61. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -67
  62. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -65
  63. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -58
  64. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -75
  65. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -70
  66. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -18
  67. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -41
  68. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -202
  69. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -67
  70. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -65
  71. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -57
  72. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -32
  73. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -23
  74. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -43
  75. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -36
  76. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -309
  77. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -34
  78. 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
- module Grafana
2
- # Representation of one specific panel in a {Dashboard} instance.
3
- class Panel
4
- # @return [Dashboard] parent {Dashboard} object
5
- attr_reader :dashboard
6
-
7
- # @param model [Hash] converted JSON Hash of the panel
8
- # @param dashboard [Dashboard] parent {Dashboard} object
9
- def initialize(model, dashboard)
10
- @model = model
11
- @dashboard = dashboard
12
- end
13
-
14
- # @return [String] content of the requested field or +''+ if not found
15
- def field(field)
16
- return @model[field] if @model.key?(field)
17
-
18
- ''
19
- end
20
-
21
- # @return [String] panel ID
22
- def id
23
- @model['id']
24
- end
25
-
26
- # @return [String] SQL query string for the requested query letter
27
- def query(query_letter)
28
- query_item = @model['targets'].select { |item| item['refId'].to_s == query_letter.to_s }.first
29
- raise QueryLetterDoesNotExistError.new(query_letter, self) if query_item.nil?
30
-
31
- query_item['rawSql']
32
- end
33
-
34
- # @return [String] relative rendering URL for the panel, to create an image out of it
35
- def render_url
36
- "/render/d-solo/#{@dashboard.id}?panelId=#{@model['id']}"
37
- end
38
- end
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
+ 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
@@ -1,259 +1,218 @@
1
- module Grafana
2
- # This class contains a representation of
3
- # {https://grafana.com/docs/grafana/latest/variables/templates-and-variables grafana variables},
4
- # aka grafana templates.
5
- #
6
- # The main need therefore rises in order to replace variables properly in different
7
- # texts, e.g. SQL statements or results.
8
- class Variable
9
- attr_reader :name, :text, :raw_value
10
-
11
- # @param config_or_value [Hash, Object] configuration hash of a variable out of an {Dashboard} instance or a value of any kind.
12
- def initialize(config_or_value)
13
- if config_or_value.is_a? Hash
14
- @config = config_or_value
15
- @name = @config['name']
16
- unless @config['current'].nil?
17
- @raw_value = @config['current']['value']
18
- @text = @config['current']['text']
19
- end
20
- else
21
- @config = {}
22
- @raw_value = config_or_value
23
- @text = config_or_value.to_s
24
- end
25
- end
26
-
27
- # Returns the stored value formatted according the given format.
28
- #
29
- # Supported formats are: +csv+, +distributed+, +doublequote+, +json+, +percentencode+, +pipe+, +raw+, +regex+, +singlequote+, +sqlstring+, +lucene+, +date+ or +glob+ (default)
30
- #
31
- # For details see {https://grafana.com/docs/grafana/latest/variables/advanced-variable-format-options Grafana Advanced variable format options}.
32
- #
33
- # For details of +date+ format, see {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to}. Please note that input for +date+ format is unixtime in milliseconds.
34
- #
35
- # @param format [String] desired format
36
- # @return [String] value of stored variable according the specified format
37
- def value_formatted(format = '')
38
- value = @raw_value
39
-
40
- # handle value 'All' properly
41
- # TODO fix check for selection of All properly
42
- if value == 'All' or @text == 'All'
43
- if !@config['options'].empty?
44
- value = @config['options'].map { |item| item['value'] }
45
- elsif !@config['query'].empty?
46
- # TODO: replace variables in this query, too
47
- return @config['query']
48
- # TODO handle 'All' value properly for query attributes
49
- else
50
- # TODO how to handle All selection properly at this point?
51
- end
52
- end
53
-
54
- case format
55
- when 'csv'
56
- return value.join(',').to_s if multi?
57
-
58
- value.to_s
59
-
60
- when 'distributed'
61
- return value.join(",#{name}=") if multi?
62
-
63
- value
64
- when 'doublequote'
65
- if multi?
66
- value = value.map { |item| "\"#{item.gsub(/[\\]/, '\\\\').gsub(/"/, '\\"')}\"" }
67
- return value.join(',')
68
- end
69
- "\"#{value.gsub(/"/, '\\"')}\""
70
-
71
- when 'json'
72
- if multi?
73
- value = value.map { |item| "\"#{item.gsub(/["\\]/, '\\\\' + '\0')}\"" }
74
- return "[#{value.join(',')}]"
75
- end
76
- "\"#{value.gsub(/"/, '\\"')}\""
77
-
78
- when 'percentencode'
79
- value = "{#{value.join(',')}}" if multi?
80
- ERB::Util.url_encode(value)
81
-
82
- when 'pipe'
83
- return value.join('|') if multi?
84
-
85
- value
86
-
87
- when 'raw'
88
- return "{#{value.join(',')}}" if multi?
89
-
90
- value
91
-
92
- when 'regex'
93
- if multi?
94
- value = value.map { |item| item.gsub(%r{[/$\.\|\\]}, '\\\\' + '\0') }
95
- return "(#{value.join('|')})"
96
- end
97
- value.gsub(%r{[/$\.\|\\]}, '\\\\' + '\0')
98
-
99
- when 'singlequote'
100
- if multi?
101
- value = value.map { |item| "'#{item.gsub(/[']/, '\\\\' + '\0')}'" }
102
- return value.join(',')
103
- end
104
- "'#{value.gsub(/[']/, '\\\\' + '\0')}'"
105
-
106
- when 'sqlstring'
107
- if multi?
108
- value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
109
- return value.join(',')
110
- end
111
- "'#{value.gsub(/'/, "''")}'"
112
-
113
- when 'lucene'
114
- if multi?
115
- value = value.map { |item| "\"#{item.gsub(%r{[" |=/\\]}, '\\\\' + '\0')}\"" }
116
- return "(#{value.join(' OR ')})"
117
- end
118
- value.gsub(%r{[" |=/\\]}, '\\\\' + '\0')
119
-
120
- when /^date(?:[:](?<format>.*))?$/
121
- #TODO validate how grafana handles multivariables with date format
122
- get_date_formatted(value, $1)
123
-
124
- when ''
125
- # default
126
- if multi?
127
- value = value.map { |item| "'#{item.gsub(/'/, "''")}'" }
128
- return value.join(',')
129
- end
130
- value.gsub(/'/, "''")
131
-
132
- else
133
- # glob and all unknown
134
- #TODO add check for array value properly for all cases
135
- return "{#{value.join(',')}}" if multi? and value.is_a?(Array)
136
-
137
- value
138
- end
139
- end
140
-
141
- # @return [Boolean] true, if the value can contain multiple selections, i.e. is an Array
142
- def multi?
143
- return @config['multi'] unless @config['multi'].nil?
144
-
145
- @raw_value.is_a? Array
146
- end
147
-
148
- # @return [Object] raw value of the variable
149
- def raw_value=(new_val)
150
- @raw_value = new_val
151
- @raw_value = @raw_value.to_s unless @raw_value.is_a?(Array)
152
- new_text = @raw_value
153
- if @config['options']
154
- val = @config['options'].select { |item| item['value'] == @raw_value }
155
- new_text = val.first['text'] unless val.empty?
156
- end
157
- @text = new_text
158
- end
159
-
160
- private
161
-
162
- # Realize time formatting according
163
- # {https://grafana.com/docs/grafana/latest/variables/variable-types/global-variables/#__from-and-__to}
164
- # and {https://momentjs.com/docs/#/displaying/}.
165
- def get_date_formatted(value, format)
166
- return (Float(value) / 1000).to_i.to_s if format == 'seconds'
167
- return Time.at((Float(value) / 1000).to_i).utc.iso8601(3) if !format or format == 'iso'
168
-
169
- # build array of known matches
170
- matches = []
171
- work_string = format
172
- while work_string.length > 0
173
- 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}|h{1,2}|k{1,2}|m{1,2}|s{1,2}|S+|X)/)
174
- unless tmp.empty?
175
- matches << tmp[0]
176
- work_string.delete_prefix!(tmp[0])
177
- else
178
- matches << work_string[0]
179
- work_string.delete_prefix!(work_string[0])
180
- end
181
- end
182
-
183
- #TODO move case when to hash
184
- format_string = ""
185
- matches.each do |match|
186
- format_string += case match
187
- when 'M'
188
- '%-m'
189
- when 'MM'
190
- '%m'
191
- when 'MMM'
192
- '%b'
193
- when 'MMMM'
194
- '%B'
195
- when 'D'
196
- '%-d'
197
- when 'DD'
198
- '%d'
199
- when 'DDD'
200
- '%-j'
201
- when 'DDDD'
202
- '%j'
203
- when 'YY'
204
- '%y'
205
- when 'YYYY'
206
- '%Y'
207
- when 'd'
208
- '%w'
209
- when 'ddd'
210
- '%a'
211
- when 'dddd'
212
- '%A'
213
- when 'e'
214
- '%w'
215
- when 'E'
216
- '%u'
217
- when 'w'
218
- '%-U'
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