summaryrefslogtreecommitdiff
path: root/lib/gitlab/metrics
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-21 12:06:14 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-21 12:06:14 +0000
commit664c4c7b49c6056136299817eb79e9f1de83e567 (patch)
treeff9e4f53481d42284f82664722278e05f5bbbcbd /lib/gitlab/metrics
parent6791eefead979110cc773720daee6e58c56483d9 (diff)
downloadgitlab-ce-664c4c7b49c6056136299817eb79e9f1de83e567.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'lib/gitlab/metrics')
-rw-r--r--lib/gitlab/metrics/dashboard/errors.rb5
-rw-r--r--lib/gitlab/metrics/dashboard/processor.rb3
-rw-r--r--lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb224
3 files changed, 232 insertions, 0 deletions
diff --git a/lib/gitlab/metrics/dashboard/errors.rb b/lib/gitlab/metrics/dashboard/errors.rb
index d41bd2c43c7..264ea0488e7 100644
--- a/lib/gitlab/metrics/dashboard/errors.rb
+++ b/lib/gitlab/metrics/dashboard/errors.rb
@@ -9,6 +9,7 @@ module Gitlab
module Errors
DashboardProcessingError = Class.new(StandardError)
PanelNotFoundError = Class.new(StandardError)
+ MissingIntegrationError = Class.new(StandardError)
LayoutError = Class.new(DashboardProcessingError)
MissingQueryError = Class.new(DashboardProcessingError)
@@ -22,6 +23,10 @@ module Gitlab
error("#{dashboard_path} could not be found.", :not_found)
when PanelNotFoundError
error(error.message, :not_found)
+ when ::Grafana::Client::Error
+ error(error.message, :service_unavailable)
+ when MissingIntegrationError
+ error('Proxy support for this API is not available currently', :bad_request)
else
raise error
end
diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb
index bfdee76a818..9566e5afb9a 100644
--- a/lib/gitlab/metrics/dashboard/processor.rb
+++ b/lib/gitlab/metrics/dashboard/processor.rb
@@ -17,7 +17,10 @@ module Gitlab
# Returns a new dashboard hash with the results of
# running transforms on the dashboard.
+ # @return [Hash, nil]
def process
+ return unless @dashboard
+
@dashboard.deep_symbolize_keys.tap do |dashboard|
@sequence.each do |stage|
stage.new(@project, dashboard, @params).transform!
diff --git a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
new file mode 100644
index 00000000000..ce75c54d014
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class GrafanaFormatter < BaseStage
+ include Gitlab::Utils::StrongMemoize
+
+ CHART_TYPE = 'area-chart'
+ PROXY_PATH = 'api/v1/query_range'
+
+ # Reformats the specified panel in the Gitlab
+ # dashboard-yml format
+ def transform!
+ InputFormatValidator.new(
+ grafana_dashboard,
+ datasource,
+ panel,
+ query_params
+ ).validate!
+
+ new_dashboard = formatted_dashboard
+
+ dashboard.clear
+ dashboard.merge!(new_dashboard)
+ end
+
+ private
+
+ def formatted_dashboard
+ { panel_groups: [{ panels: [formatted_panel] }] }
+ end
+
+ def formatted_panel
+ {
+ title: panel[:title],
+ type: CHART_TYPE,
+ y_label: '', # Grafana panels do not include a Y-Axis label
+ metrics: panel[:targets].map.with_index do |target, idx|
+ formatted_metric(target, idx)
+ end
+ }
+ end
+
+ def formatted_metric(metric, idx)
+ {
+ id: "#{metric[:legendFormat]}_#{idx}",
+ query_range: format_query(metric),
+ label: replace_variables(metric[:legendFormat]),
+ prometheus_endpoint_path: prometheus_endpoint_for_metric(metric)
+ }.compact
+ end
+
+ # Panel specified by the url from the Grafana dashboard
+ def panel
+ strong_memoize(:panel) do
+ grafana_dashboard[:dashboard][:panels].find do |panel|
+ panel[:id].to_s == query_params[:panelId]
+ end
+ end
+ end
+
+ # Grafana url query parameters. Includes information
+ # on which panel to select and time range.
+ def query_params
+ strong_memoize(:query_params) do
+ Gitlab::Metrics::Dashboard::Url.parse_query(grafana_url)
+ end
+ end
+
+ # Endpoint which will return prometheus metric data
+ # for the metric
+ def prometheus_endpoint_for_metric(metric)
+ Gitlab::Routing.url_helpers.project_grafana_api_path(
+ project,
+ datasource_id: datasource[:id],
+ proxy_path: PROXY_PATH,
+ query: format_query(metric)
+ )
+ end
+
+ # Reformats query for compatibility with prometheus api.
+ def format_query(metric)
+ expression = remove_new_lines(metric[:expr])
+ expression = replace_variables(expression)
+ expression = replace_global_variables(expression, metric)
+
+ expression
+ end
+
+ # Accomodates instance-defined Grafana variables.
+ # These are variables defined by users, and values
+ # must be provided in the query parameters.
+ def replace_variables(expression)
+ return expression unless grafana_dashboard[:dashboard][:templating]
+
+ grafana_dashboard[:dashboard][:templating][:list]
+ .sort_by { |variable| variable[:name].length }
+ .each do |variable|
+ variable_value = query_params[:"var-#{variable[:name]}"]
+
+ expression = expression.gsub("$#{variable[:name]}", variable_value)
+ expression = expression.gsub("[[#{variable[:name]}]]", variable_value)
+ expression = expression.gsub("{{#{variable[:name]}}}", variable_value)
+ end
+
+ expression
+ end
+
+ # Replaces Grafana global built-in variables with values.
+ # Only $__interval and $__from and $__to are supported.
+ #
+ # See https://grafana.com/docs/reference/templating/#global-built-in-variables
+ def replace_global_variables(expression, metric)
+ expression = expression.gsub('$__interval', metric[:interval]) if metric[:interval]
+ expression = expression.gsub('$__from', query_params[:from])
+ expression = expression.gsub('$__to', query_params[:to])
+
+ expression
+ end
+
+ # Removes new lines from expression.
+ def remove_new_lines(expression)
+ expression.gsub(/\R+/, '')
+ end
+
+ # Grafana datasource object corresponding to the
+ # specified dashboard
+ def datasource
+ params[:datasource]
+ end
+
+ # The specified Grafana dashboard
+ def grafana_dashboard
+ params[:grafana_dashboard]
+ end
+
+ # The URL specifying which Grafana panel to embed
+ def grafana_url
+ params[:grafana_url]
+ end
+ end
+
+ class InputFormatValidator
+ include ::Gitlab::Metrics::Dashboard::Errors
+
+ attr_reader :grafana_dashboard, :datasource, :panel, :query_params
+
+ UNSUPPORTED_GRAFANA_GLOBAL_VARS = %w(
+ $__interval_ms
+ $__timeFilter
+ $__name
+ $timeFilter
+ $interval
+ ).freeze
+
+ def initialize(grafana_dashboard, datasource, panel, query_params)
+ @grafana_dashboard = grafana_dashboard
+ @datasource = datasource
+ @panel = panel
+ @query_params = query_params
+ end
+
+ def validate!
+ validate_query_params!
+ validate_datasource!
+ validate_panel_type!
+ validate_variable_definitions!
+ validate_global_variables!
+ end
+
+ private
+
+ def validate_datasource!
+ return if datasource[:access] == 'proxy' && datasource[:type] == 'prometheus'
+
+ raise_error 'Only Prometheus datasources with proxy access in Grafana are supported.'
+ end
+
+ def validate_query_params!
+ return if [:panelId, :from, :to].all? { |param| query_params.include?(param) }
+
+ raise_error 'Grafana query parameters must include panelId, from, and to.'
+ end
+
+ def validate_panel_type!
+ return if panel[:type] == 'graph' && panel[:lines]
+
+ raise_error 'Panel type must be a line graph.'
+ end
+
+ def validate_variable_definitions!
+ return unless grafana_dashboard[:dashboard][:templating]
+
+ return if grafana_dashboard[:dashboard][:templating][:list].all? do |variable|
+ query_params[:"var-#{variable[:name]}"].present?
+ end
+
+ raise_error 'All Grafana variables must be defined in the query parameters.'
+ end
+
+ def validate_global_variables!
+ return unless panel_contains_unsupported_vars?
+
+ raise_error 'Prometheus must not include'
+ end
+
+ def panel_contains_unsupported_vars?
+ panel[:targets].any? do |target|
+ UNSUPPORTED_GRAFANA_GLOBAL_VARS.any? do |variable|
+ target[:expr].include?(variable)
+ end
+ end
+ end
+
+ def raise_error(message)
+ raise DashboardProcessingError.new(message)
+ end
+ end
+ end
+ end
+ end
+end