2019-12-26 22:10:19 +05:30
|
|
|
# frozen_string_literal: true
|
|
|
|
|
|
|
|
# Responsible for returning a gitlab-compatible dashboard
|
|
|
|
# containing info based on a grafana dashboard and datasource.
|
|
|
|
#
|
|
|
|
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
|
|
|
|
module Metrics
|
|
|
|
module Dashboard
|
2020-04-08 14:13:33 +05:30
|
|
|
class GrafanaMetricEmbedService < ::Metrics::Dashboard::BaseEmbedService
|
2019-12-26 22:10:19 +05:30
|
|
|
include ReactiveCaching
|
|
|
|
|
|
|
|
SEQUENCE = [
|
2020-04-22 19:07:51 +05:30
|
|
|
::Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter,
|
|
|
|
::Gitlab::Metrics::Dashboard::Stages::PanelIdsInserter
|
2019-12-26 22:10:19 +05:30
|
|
|
].freeze
|
|
|
|
|
|
|
|
self.reactive_cache_key = ->(service) { service.cache_key }
|
|
|
|
self.reactive_cache_lease_timeout = 30.seconds
|
|
|
|
self.reactive_cache_refresh_interval = 30.minutes
|
|
|
|
self.reactive_cache_lifetime = 30.minutes
|
|
|
|
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
|
|
|
|
|
|
|
|
class << self
|
|
|
|
# Determines whether the provided params are sufficient
|
|
|
|
# to uniquely identify a grafana dashboard.
|
|
|
|
def valid_params?(params)
|
|
|
|
[
|
2020-04-08 14:13:33 +05:30
|
|
|
embedded?(params[:embedded]),
|
2019-12-26 22:10:19 +05:30
|
|
|
params[:grafana_url]
|
|
|
|
].all?
|
|
|
|
end
|
|
|
|
|
|
|
|
def from_cache(project_id, user_id, grafana_url)
|
|
|
|
project = Project.find(project_id)
|
|
|
|
user = User.find(user_id)
|
|
|
|
|
|
|
|
new(project, user, grafana_url: grafana_url)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def get_dashboard
|
|
|
|
with_reactive_cache(*cache_key) { |result| result }
|
|
|
|
end
|
|
|
|
|
|
|
|
# Inherits the primary logic from the parent class and
|
|
|
|
# maintains the service's API while including ReactiveCache
|
|
|
|
def calculate_reactive_cache(*)
|
|
|
|
# This is called with explicit parentheses to prevent
|
|
|
|
# the params passed to #calculate_reactive_cache from
|
|
|
|
# being passed to #get_dashboard (which accepts none)
|
|
|
|
::Metrics::Dashboard::BaseService
|
|
|
|
.instance_method(:get_dashboard)
|
|
|
|
.bind(self)
|
|
|
|
.call() # rubocop:disable Style/MethodCallWithoutArgsParentheses
|
|
|
|
end
|
|
|
|
|
|
|
|
def cache_key(*args)
|
|
|
|
[project.id, current_user.id, grafana_url]
|
|
|
|
end
|
|
|
|
|
|
|
|
# Required for ReactiveCaching; Usage overridden by
|
|
|
|
# self.reactive_cache_worker_finder
|
|
|
|
def id
|
|
|
|
nil
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def get_raw_dashboard
|
|
|
|
raise MissingIntegrationError unless client
|
|
|
|
|
|
|
|
grafana_dashboard = fetch_dashboard
|
|
|
|
datasource = fetch_datasource(grafana_dashboard)
|
|
|
|
|
|
|
|
params.merge!(grafana_dashboard: grafana_dashboard, datasource: datasource)
|
|
|
|
|
|
|
|
{}
|
|
|
|
end
|
|
|
|
|
|
|
|
def fetch_dashboard
|
|
|
|
uid = GrafanaUidParser.new(grafana_url, project).parse
|
|
|
|
raise DashboardProcessingError.new('Dashboard uid not found') unless uid
|
|
|
|
|
|
|
|
response = client.get_dashboard(uid: uid)
|
|
|
|
|
|
|
|
parse_json(response.body)
|
|
|
|
end
|
|
|
|
|
|
|
|
def fetch_datasource(dashboard)
|
|
|
|
name = DatasourceNameParser.new(grafana_url, dashboard).parse
|
|
|
|
raise DashboardProcessingError.new('Datasource name not found') unless name
|
|
|
|
|
|
|
|
response = client.get_datasource(name: name)
|
|
|
|
|
|
|
|
parse_json(response.body)
|
|
|
|
end
|
|
|
|
|
|
|
|
def grafana_url
|
|
|
|
params[:grafana_url]
|
|
|
|
end
|
|
|
|
|
|
|
|
def client
|
|
|
|
project.grafana_integration&.client
|
|
|
|
end
|
|
|
|
|
|
|
|
def allowed?
|
|
|
|
Ability.allowed?(current_user, :read_project, project)
|
|
|
|
end
|
|
|
|
|
|
|
|
def sequence
|
|
|
|
SEQUENCE
|
|
|
|
end
|
|
|
|
|
|
|
|
def parse_json(json)
|
|
|
|
JSON.parse(json, symbolize_names: true)
|
|
|
|
rescue JSON::ParserError
|
|
|
|
raise DashboardProcessingError.new('Grafana response contains invalid json')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Identifies the uid of the dashboard based on url format
|
|
|
|
class GrafanaUidParser
|
|
|
|
def initialize(grafana_url, project)
|
|
|
|
@grafana_url, @project = grafana_url, project
|
|
|
|
end
|
|
|
|
|
|
|
|
def parse
|
|
|
|
@grafana_url.match(uid_regex) { |m| m.named_captures['uid'] }
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
# URLs are expected to look like https://domain.com/d/:uid/other/stuff
|
|
|
|
def uid_regex
|
|
|
|
base_url = @project.grafana_integration.grafana_url.chomp('/')
|
|
|
|
|
2020-01-01 13:55:28 +05:30
|
|
|
%r{^(#{Regexp.escape(base_url)}\/d\/(?<uid>.+)\/)}x
|
2019-12-26 22:10:19 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Identifies the name of the datasource for a dashboard
|
2020-04-08 14:13:33 +05:30
|
|
|
# based on the panelId query parameter found in the url.
|
|
|
|
#
|
|
|
|
# If no panel is specified, defaults to the first valid panel.
|
2019-12-26 22:10:19 +05:30
|
|
|
class DatasourceNameParser
|
|
|
|
def initialize(grafana_url, grafana_dashboard)
|
|
|
|
@grafana_url, @grafana_dashboard = grafana_url, grafana_dashboard
|
|
|
|
end
|
|
|
|
|
|
|
|
def parse
|
|
|
|
@grafana_dashboard[:dashboard][:panels]
|
2020-04-08 14:13:33 +05:30
|
|
|
.find { |panel| panel_id ? matching_panel?(panel) : valid_panel?(panel) }
|
2019-12-26 22:10:19 +05:30
|
|
|
.try(:[], :datasource)
|
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2020-04-08 14:13:33 +05:30
|
|
|
def panel_id
|
|
|
|
query_params[:panelId]
|
|
|
|
end
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
def query_params
|
|
|
|
Gitlab::Metrics::Dashboard::Url.parse_query(@grafana_url)
|
|
|
|
end
|
2020-04-08 14:13:33 +05:30
|
|
|
|
|
|
|
def matching_panel?(panel)
|
|
|
|
panel[:id].to_s == panel_id
|
|
|
|
end
|
|
|
|
|
|
|
|
def valid_panel?(panel)
|
|
|
|
::Grafana::Validator
|
|
|
|
.new(@grafana_dashboard, nil, panel, query_params)
|
|
|
|
.valid?
|
|
|
|
end
|
2019-12-26 22:10:19 +05:30
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|