ruby-grafana-reporter 0.2.2 → 0.4.3

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 (83) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +126 -88
  3. data/bin/ruby-grafana-reporter +2 -2
  4. data/lib/VERSION.rb +3 -2
  5. data/lib/grafana/abstract_datasource.rb +146 -0
  6. data/lib/grafana/dashboard.rb +1 -3
  7. data/lib/grafana/errors.rb +18 -3
  8. data/lib/grafana/grafana.rb +64 -66
  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 +30 -0
  12. data/lib/grafana/graphite_datasource.rb +72 -0
  13. data/lib/grafana/image_rendering_datasource.rb +44 -0
  14. data/lib/grafana/influxdb_datasource.rb +70 -0
  15. data/lib/grafana/panel.rb +9 -3
  16. data/lib/grafana/prometheus_datasource.rb +67 -0
  17. data/lib/grafana/sql_datasource.rb +78 -0
  18. data/lib/grafana/unsupported_datasource.rb +7 -0
  19. data/lib/grafana/variable.rb +1 -1
  20. data/lib/grafana/webrequest.rb +71 -0
  21. data/lib/grafana_reporter/abstract_query.rb +460 -0
  22. data/lib/grafana_reporter/abstract_report.rb +139 -18
  23. data/lib/grafana_reporter/alerts_table_query.rb +39 -0
  24. data/lib/grafana_reporter/annotations_table_query.rb +38 -0
  25. data/lib/grafana_reporter/application/application.rb +34 -286
  26. data/lib/grafana_reporter/application/webservice.rb +50 -15
  27. data/lib/grafana_reporter/asciidoctor/alerts_table_include_processor.rb +91 -0
  28. data/lib/grafana_reporter/asciidoctor/annotations_table_include_processor.rb +90 -0
  29. data/lib/grafana_reporter/asciidoctor/panel_image_block_macro.rb +74 -0
  30. data/lib/grafana_reporter/asciidoctor/panel_image_inline_macro.rb +76 -0
  31. data/lib/grafana_reporter/asciidoctor/panel_property_inline_macro.rb +70 -0
  32. data/lib/grafana_reporter/asciidoctor/panel_query_table_include_processor.rb +95 -0
  33. data/lib/grafana_reporter/asciidoctor/panel_query_value_inline_macro.rb +90 -0
  34. data/lib/grafana_reporter/asciidoctor/processor_mixin.rb +49 -0
  35. data/lib/grafana_reporter/asciidoctor/report.rb +32 -76
  36. data/lib/grafana_reporter/asciidoctor/show_environment_include_processor.rb +46 -0
  37. data/lib/grafana_reporter/asciidoctor/show_help_include_processor.rb +35 -0
  38. data/lib/grafana_reporter/asciidoctor/sql_table_include_processor.rb +90 -0
  39. data/lib/grafana_reporter/asciidoctor/sql_value_inline_macro.rb +86 -0
  40. data/lib/grafana_reporter/asciidoctor/value_as_variable_include_processor.rb +90 -0
  41. data/lib/grafana_reporter/configuration.rb +59 -52
  42. data/lib/grafana_reporter/console_configuration_wizard.rb +311 -0
  43. data/lib/grafana_reporter/demo_report_wizard.rb +105 -0
  44. data/lib/grafana_reporter/erb/report.rb +30 -0
  45. data/lib/grafana_reporter/erb/report_jail.rb +21 -0
  46. data/lib/grafana_reporter/errors.rb +55 -0
  47. data/lib/grafana_reporter/help.rb +443 -0
  48. data/lib/grafana_reporter/logger/{two_way_logger.rb → two_way_delegate_logger.rb} +1 -1
  49. data/lib/grafana_reporter/panel_image_query.rb +25 -0
  50. data/lib/grafana_reporter/panel_property_query.rb +22 -0
  51. data/lib/grafana_reporter/query_value_query.rb +61 -0
  52. data/lib/grafana_reporter/report_webhook.rb +35 -0
  53. data/lib/ruby_grafana_extension.rb +8 -0
  54. data/lib/{ruby-grafana-reporter.rb → ruby_grafana_reporter.rb} +1 -0
  55. metadata +47 -39
  56. data/lib/grafana/abstract_panel_query.rb +0 -22
  57. data/lib/grafana/abstract_query.rb +0 -132
  58. data/lib/grafana/abstract_sql_query.rb +0 -51
  59. data/lib/grafana/panel_image_query.rb +0 -52
  60. data/lib/grafana_reporter/asciidoctor/alerts_table_query.rb +0 -104
  61. data/lib/grafana_reporter/asciidoctor/annotations_table_query.rb +0 -99
  62. data/lib/grafana_reporter/asciidoctor/errors.rb +0 -40
  63. data/lib/grafana_reporter/asciidoctor/extensions/alerts_table_include_processor.rb +0 -92
  64. data/lib/grafana_reporter/asciidoctor/extensions/annotations_table_include_processor.rb +0 -91
  65. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_block_macro.rb +0 -69
  66. data/lib/grafana_reporter/asciidoctor/extensions/panel_image_inline_macro.rb +0 -68
  67. data/lib/grafana_reporter/asciidoctor/extensions/panel_property_inline_macro.rb +0 -61
  68. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_table_include_processor.rb +0 -78
  69. data/lib/grafana_reporter/asciidoctor/extensions/panel_query_value_inline_macro.rb +0 -73
  70. data/lib/grafana_reporter/asciidoctor/extensions/processor_mixin.rb +0 -20
  71. data/lib/grafana_reporter/asciidoctor/extensions/show_environment_include_processor.rb +0 -43
  72. data/lib/grafana_reporter/asciidoctor/extensions/show_help_include_processor.rb +0 -30
  73. data/lib/grafana_reporter/asciidoctor/extensions/sql_table_include_processor.rb +0 -70
  74. data/lib/grafana_reporter/asciidoctor/extensions/sql_value_inline_macro.rb +0 -66
  75. data/lib/grafana_reporter/asciidoctor/extensions/value_as_variable_include_processor.rb +0 -86
  76. data/lib/grafana_reporter/asciidoctor/help.rb +0 -435
  77. data/lib/grafana_reporter/asciidoctor/panel_first_value_query.rb +0 -34
  78. data/lib/grafana_reporter/asciidoctor/panel_image_query.rb +0 -26
  79. data/lib/grafana_reporter/asciidoctor/panel_property_query.rb +0 -44
  80. data/lib/grafana_reporter/asciidoctor/panel_table_query.rb +0 -38
  81. data/lib/grafana_reporter/asciidoctor/query_mixin.rb +0 -301
  82. data/lib/grafana_reporter/asciidoctor/sql_first_value_query.rb +0 -42
  83. data/lib/grafana_reporter/asciidoctor/sql_table_query.rb +0 -44
@@ -53,9 +53,7 @@ module Grafana
53
53
  list = @model['templating']['list']
54
54
  return unless list.is_a? Array
55
55
 
56
- list.each do |item|
57
- @variables << Variable.new(item)
58
- end
56
+ list.each { |item| @variables << Variable.new(item) }
59
57
  end
60
58
 
61
59
  # read panels
@@ -49,18 +49,33 @@ module Grafana
49
49
  #
50
50
  # Most likely this happens, because the image renderer is not configures properly in grafana,
51
51
  # or the panel rendering ran into a timeout.
52
+ # @param panel [Panel] panel object, which could not be rendered
52
53
  class ImageCouldNotBeRenderedError < GrafanaError
53
- # @param panel [Panel] panel object, which could not be rendered
54
54
  def initialize(panel)
55
- super("The specified panel '#{panel.id}' from dashboard '#{panel.dashboard.id} could not be "\
55
+ super("The specified panel '#{panel.id}' from dashboard '#{panel.dashboard.id}' could not be "\
56
56
  'rendered to an image.')
57
57
  end
58
58
  end
59
59
 
60
- # Raised if no SQL query is specified in a {AbstractSqlQuery} object.
60
+ # Raised if no SQL query is specified.
61
61
  class MissingSqlQueryError < GrafanaError
62
62
  def initialize
63
63
  super('No SQL statement has been specified.')
64
64
  end
65
65
  end
66
+
67
+ # Raised if a datasource shall be queried, which is not (yet) supported by the reporter
68
+ class InvalidDatasourceQueryProvidedError < GrafanaError
69
+ def initialize(query)
70
+ super("The datasource query provided, does not look like a grafana datasource target (received: #{query}).")
71
+ end
72
+ end
73
+
74
+ # Raised if a datasource implementation cannot handle a query, which is composed
75
+ # in the grafana visual editor.
76
+ class ComposedQueryNotSupportedError < GrafanaError
77
+ def initialize(class_obj)
78
+ super("Composed queries are not yet supported for datasource '#{class_obj}'.")
79
+ end
80
+ end
66
81
  end
@@ -9,20 +9,20 @@
9
9
  module Grafana
10
10
  # Main class for handling the interaction with one specific Grafana instance.
11
11
  class Grafana
12
+ attr_reader :logger
13
+
12
14
  # @param base_uri [String] full URI pointing to the specific grafana instance without
13
15
  # trailing slash, e.g. +https://localhost:3000+.
14
16
  # @param key [String] API key for the grafana instance, if required
15
17
  # @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.
18
+ # Currently supporting +:logger+.
20
19
  def initialize(base_uri, key = nil, opts = {})
21
20
  @base_uri = base_uri
22
21
  @key = key
23
22
  @dashboards = {}
24
23
  @logger = opts[:logger] || ::Logger.new(nil)
25
- @datasources = opts[:datasources]
24
+
25
+ initialize_datasources unless @base_uri.empty?
26
26
  end
27
27
 
28
28
  # Used to test a connection to the grafana instance.
@@ -32,34 +32,54 @@ module Grafana
32
32
  #
33
33
  # @return [String] +Admin+, +NON-Admin+ or +Failed+ is returned, depending on the test results
34
34
  def test_connection
35
- if execute_http_request('/api/datasources').is_a?(Net::HTTPOK)
35
+ if prepare_request({ relative_url: '/api/datasources' }).execute.is_a?(Net::HTTPOK)
36
36
  # we have admin rights
37
- @logger.info('Reporter is running with Admin privileges on grafana.')
37
+ @logger.warn('Reporter is running with Admin privileges on grafana. This is a potential security risk.')
38
38
  return 'Admin'
39
39
  end
40
40
  # check if we have lower rights
41
- return 'Failed' unless execute_http_request('/api/dashboards/home').is_a?(Net::HTTPOK)
41
+ return 'Failed' unless prepare_request({ relative_url: '/api/dashboards/home' }).execute.is_a?(Net::HTTPOK)
42
42
 
43
- @logger.info('Reporter is running with NON-Admin privileges on grafana. Make sure that necessary '\
44
- 'datasources are specified in CONFIG_FILE, otherwise operation will fail')
43
+ @logger.info('Reporter is running with NON-Admin privileges on grafana.')
45
44
  'NON-Admin'
46
45
  end
47
46
 
48
- # Returns the ID of a datasource, which has been queried by the datasource name.
47
+ # Returns the datasource, which has been queried by the datasource name.
49
48
  #
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
49
+ # @param datasource_name [String] name of the searched datasource
50
+ # @return [Datasource] Datasource for the specified datasource name
51
+ def datasource_by_name(datasource_name)
52
+ datasource_name = 'default' if datasource_name.to_s.empty?
53
+ # TODO: add support for grafana builtin datasource types
54
+ return UnsupportedDatasource.new(nil) if datasource_name.to_s =~ /-- (?:Mixed|Dashboard|Grafana) --/
55
+ raise DatasourceDoesNotExistError.new('name', datasource_name) unless @datasources[datasource_name]
56
+
57
+ @datasources[datasource_name]
56
58
  end
57
59
 
58
- # Returns if the given datasource ID exists for the grafana instance.
60
+ # Returns the datasource, which has been queried by the datasource id.
59
61
  #
60
- # @return [Boolean] true if exists, false otherwise
61
- def datasource_id_exists?(datasource_id)
62
- datasources.value?(datasource_id)
62
+ # @param datasource_id [Integer] id of the searched datasource
63
+ # @return [Datasource] Datasource for the specified datasource id
64
+ def datasource_by_id(datasource_id)
65
+ datasource = @datasources.select { |_name, ds| ds.id == datasource_id.to_i }.values.first
66
+ raise DatasourceDoesNotExistError.new('id', datasource_id) unless datasource
67
+
68
+ datasource
69
+ end
70
+
71
+ # @return [Array] Array of dashboard uids within the current grafana object
72
+ def dashboard_ids
73
+ response = prepare_request({ relative_url: '/api/search' }).execute
74
+ return [] unless response.is_a?(Net::HTTPOK)
75
+
76
+ dashboards = JSON.parse(response.body)
77
+
78
+ dashboards.each do |dashboard|
79
+ @dashboards[dashboard['uid']] = nil unless @dashboards[dashboard['uid']]
80
+ end
81
+
82
+ @dashboards.keys
63
83
  end
64
84
 
65
85
  # @param dashboard_uid [String] UID of the searched {Dashboard}
@@ -67,64 +87,42 @@ module Grafana
67
87
  def dashboard(dashboard_uid)
68
88
  return @dashboards[dashboard_uid] unless @dashboards[dashboard_uid].nil?
69
89
 
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?
90
+ response = prepare_request({ relative_url: "/api/dashboards/uid/#{dashboard_uid}" }).execute
91
+ raise DashboardDoesNotExistError, dashboard_uid unless response.is_a?(Net::HTTPOK)
74
92
 
75
93
  # cache dashboard for reuse
94
+ model = JSON.parse(response.body)['dashboard']
76
95
  @dashboards[dashboard_uid] = Dashboard.new(model, self)
77
96
 
78
97
  @dashboards[dashboard_uid]
79
98
  end
80
99
 
81
- # Runs a specific HTTP request against the current grafana instance.
100
+ # Prepares a {WebRequest} object for the current {Grafana} instance, which may be enriched
101
+ # with further properties and can then run {WebRequest#execute}.
82
102
  #
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)
103
+ # @option options [Hash] :relative_url relative URL with a leading slash, which shall be queried
104
+ # @option options [Hash] :accept
105
+ # @option options [Hash] :body
106
+ # @option options [Hash] :content_type
107
+ # @return [WebRequest] webrequest prepared for execution
108
+ def prepare_request(options = {})
109
+ auth = @key ? { authorization: "Bearer #{@key}" } : {}
110
+ WebRequest.new(@base_uri, auth.merge({ logger: @logger }).merge(options))
111
111
  end
112
112
 
113
113
  private
114
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
115
+ def initialize_datasources
116
+ @datasources = {}
117
+
118
+ settings = prepare_request({ relative_url: '/api/frontend/settings' }).execute
119
+ return unless settings.is_a?(Net::HTTPOK)
120
+
121
+ json = JSON.parse(settings.body)
122
+ json['datasources'].select { |_k, v| v['id'].to_i.positive? }.each do |ds_name, ds_value|
123
+ @datasources[ds_name] = AbstractDatasource.build_instance(ds_value)
126
124
  end
127
- @datasources
125
+ @datasources['default'] = @datasources[json['defaultDatasource']]
128
126
  end
129
127
  end
130
128
  end
@@ -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,30 @@
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
+
25
+ # @see AbstractDatasource#default_variable_format
26
+ def default_variable_format
27
+ 'glob'
28
+ end
29
+ end
30
+ 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