Support dashboard params for metrics dashboard
https://gitlab.com/gitlab-org/gitlab-ce/issues/62971 Adds support to EnvironmentsController#metrics_dashboard for the following params: group, title, y_label These params are used to uniquely identify a panel on the metrics dashboard. Metrics are stored in several places, so this adds utilities to find a specific panel from the database or filesystem depending on the metric specified. Also moves some shared utilities into separate classes, notably default values and errors.
This commit is contained in:
parent
d8966abd20
commit
bf918b68f6
23 changed files with 917 additions and 55 deletions
|
@ -165,7 +165,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
project,
|
||||
current_user,
|
||||
environment,
|
||||
embedded: params[:embedded]
|
||||
dashboard_path: params[:dashboard],
|
||||
**dashboard_params.to_h.symbolize_keys
|
||||
)
|
||||
elsif Feature.enabled?(:environment_metrics_show_multiple_dashboards, project)
|
||||
result = dashboard_finder.find(
|
||||
|
@ -233,6 +234,10 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
params.require([:start, :end])
|
||||
end
|
||||
|
||||
def dashboard_params
|
||||
params.permit(:embedded, :group, :title, :y_label)
|
||||
end
|
||||
|
||||
def dashboard_finder
|
||||
Gitlab::Metrics::Dashboard::Finder
|
||||
end
|
||||
|
|
|
@ -32,6 +32,10 @@ class PrometheusMetric < ApplicationRecord
|
|||
Gitlab::Prometheus::Metric.new(id: id, title: title, required_metrics: required_metrics, weight: 0, y_label: y_label, queries: queries)
|
||||
end
|
||||
|
||||
def to_metric_hash
|
||||
queries.first.merge(metric_id: id)
|
||||
end
|
||||
|
||||
def queries
|
||||
[
|
||||
{
|
||||
|
|
|
@ -9,13 +9,17 @@ module PrometheusMetricEnums
|
|||
aws_elb: -3,
|
||||
nginx: -4,
|
||||
kubernetes: -5,
|
||||
nginx_ingress: -6,
|
||||
nginx_ingress: -6
|
||||
}.merge(custom_groups).freeze
|
||||
end
|
||||
|
||||
# custom/user groups
|
||||
# custom/user groups
|
||||
def self.custom_groups
|
||||
{
|
||||
business: 0,
|
||||
response: 1,
|
||||
system: 2
|
||||
}
|
||||
}.freeze
|
||||
end
|
||||
|
||||
def self.group_details
|
||||
|
@ -50,16 +54,20 @@ module PrometheusMetricEnums
|
|||
group_title: _('System metrics (Kubernetes)'),
|
||||
required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
|
||||
priority: 5
|
||||
}.freeze,
|
||||
}.freeze
|
||||
}.merge(custom_group_details).freeze
|
||||
end
|
||||
|
||||
# custom/user groups
|
||||
# custom/user groups
|
||||
def self.custom_group_details
|
||||
{
|
||||
business: {
|
||||
group_title: _('Business metrics (Custom)'),
|
||||
priority: 0
|
||||
}.freeze,
|
||||
response: {
|
||||
group_title: _('Response metrics (Custom)'),
|
||||
priority: -5
|
||||
priority: -5
|
||||
}.freeze,
|
||||
system: {
|
||||
group_title: _('System metrics (Custom)'),
|
||||
|
|
36
app/services/metrics/dashboard/base_embed_service.rb
Normal file
36
app/services/metrics/dashboard/base_embed_service.rb
Normal file
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Base class for embed services. Contains a few basic helper
|
||||
# methods that the embed services share.
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class BaseEmbedService < ::Metrics::Dashboard::BaseService
|
||||
def cache_key
|
||||
"dynamic_metrics_dashboard_#{identifiers}"
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def dashboard_path
|
||||
params[:dashboard_path].presence ||
|
||||
::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH
|
||||
end
|
||||
|
||||
def group
|
||||
params[:group]
|
||||
end
|
||||
|
||||
def title
|
||||
params[:title]
|
||||
end
|
||||
|
||||
def y_label
|
||||
params[:y_label]
|
||||
end
|
||||
|
||||
def identifiers
|
||||
[dashboard_path, group, title, y_label].join('|')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,17 +5,14 @@
|
|||
module Metrics
|
||||
module Dashboard
|
||||
class BaseService < ::BaseService
|
||||
PROCESSING_ERROR = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardProcessingError
|
||||
NOT_FOUND_ERROR = Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
|
||||
include Gitlab::Metrics::Dashboard::Errors
|
||||
|
||||
def get_dashboard
|
||||
return error('Insufficient permissions.', :unauthorized) unless allowed?
|
||||
|
||||
success(dashboard: process_dashboard)
|
||||
rescue NOT_FOUND_ERROR
|
||||
error("#{dashboard_path} could not be found.", :not_found)
|
||||
rescue PROCESSING_ERROR => e
|
||||
error(e.message, :unprocessable_entity)
|
||||
rescue StandardError => e
|
||||
handle_errors(e)
|
||||
end
|
||||
|
||||
# Summary of all known dashboards for the service.
|
||||
|
|
123
app/services/metrics/dashboard/custom_metric_embed_service.rb
Normal file
123
app/services/metrics/dashboard/custom_metric_embed_service.rb
Normal file
|
@ -0,0 +1,123 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Responsible for returning a dashboard containing specified
|
||||
# custom metrics. Creates panels based on the matching metrics
|
||||
# stored in the database.
|
||||
#
|
||||
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class CustomMetricEmbedService < ::Metrics::Dashboard::BaseEmbedService
|
||||
extend ::Gitlab::Utils::Override
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
include Gitlab::Metrics::Dashboard::Defaults
|
||||
|
||||
class << self
|
||||
# Determines whether the provided params are sufficient
|
||||
# to uniquely identify a panel composed of user-defined
|
||||
# custom metrics from the DB.
|
||||
def valid_params?(params)
|
||||
[
|
||||
params[:embedded],
|
||||
valid_dashboard?(params[:dashboard_path]),
|
||||
valid_group_title?(params[:group]),
|
||||
params[:title].present?,
|
||||
params.has_key?(:y_label)
|
||||
].all?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# A group title is valid if it is one of the limited
|
||||
# options the user can select in the UI.
|
||||
def valid_group_title?(group)
|
||||
PrometheusMetricEnums
|
||||
.custom_group_details
|
||||
.map { |_, details| details[:group_title] }
|
||||
.include?(group)
|
||||
end
|
||||
|
||||
# All custom metrics are displayed on the system dashboard.
|
||||
# Nil is acceptable as we'll default to the system dashboard.
|
||||
def valid_dashboard?(dashboard)
|
||||
dashboard.nil? || SystemDashboardService.system_dashboard?(dashboard)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a new dashboard with only the matching
|
||||
# metrics from the system dashboard, stripped of
|
||||
# group info.
|
||||
#
|
||||
# Note: This overrides the method #raw_dashboard,
|
||||
# which means the result will not be cached. This
|
||||
# is because we are inserting DB info into the
|
||||
# dashboard before post-processing. This ensures
|
||||
# we aren't acting on deleted or out-of-date metrics.
|
||||
#
|
||||
# @return [Hash]
|
||||
override :raw_dashboard
|
||||
def raw_dashboard
|
||||
panels_not_found!(identifiers) if panels.empty?
|
||||
|
||||
{ 'panel_groups' => [{ 'panels' => panels }] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Generated dashboard panels for each metric which
|
||||
# matches the provided input.
|
||||
# @return [Array<Hash>]
|
||||
def panels
|
||||
strong_memoize(:panels) do
|
||||
metrics.map { |metric| panel_for_metric(metric) }
|
||||
end
|
||||
end
|
||||
|
||||
# Metrics which match the provided inputs.
|
||||
# There may be multiple metrics, but they should be
|
||||
# displayed in a single panel/chart.
|
||||
# @return [ActiveRecord::AssociationRelation<PromtheusMetric>]
|
||||
# rubocop: disable CodeReuse/ActiveRecord
|
||||
def metrics
|
||||
project.prometheus_metrics.where(
|
||||
group: group_key,
|
||||
title: title,
|
||||
y_label: y_label
|
||||
)
|
||||
end
|
||||
# rubocop: enable CodeReuse/ActiveRecord
|
||||
|
||||
# Returns a symbol representing the group that
|
||||
# the dashboard's group title belongs to.
|
||||
# It will be one of the keys found under
|
||||
# PrometheusMetricEnums.custom_groups.
|
||||
#
|
||||
# @return [String]
|
||||
def group_key
|
||||
strong_memoize(:group_key) do
|
||||
PrometheusMetricEnums
|
||||
.group_details
|
||||
.find { |_, details| details[:group_title] == group }
|
||||
.first
|
||||
.to_s
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a representation of a PromtheusMetric
|
||||
# as a dashboard panel. As the panel is generated
|
||||
# on the fly, we're using default values for info
|
||||
# not represented in the DB.
|
||||
#
|
||||
# @return [Hash]
|
||||
def panel_for_metric(metric)
|
||||
{
|
||||
type: DEFAULT_PANEL_TYPE,
|
||||
weight: DEFAULT_PANEL_WEIGHT,
|
||||
title: metric.title,
|
||||
y_label: metric.y_label,
|
||||
metrics: [metric.to_metric_hash]
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,9 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Responsible for returning a filtered system dashboard
|
||||
# containing only the default embedded metrics. In future,
|
||||
# this class may be updated to support filtering to
|
||||
# alternate metrics/panels.
|
||||
# containing only the default embedded metrics. This class
|
||||
# operates by selecting metrics directly from the system
|
||||
# dashboard.
|
||||
#
|
||||
# Why isn't this filtering in a processing stage? By filtering
|
||||
# here, we ensure the dynamically-determined dashboard is cached.
|
||||
|
@ -11,7 +11,7 @@
|
|||
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class DefaultEmbedService < ::Metrics::Dashboard::BaseService
|
||||
class DefaultEmbedService < ::Metrics::Dashboard::BaseEmbedService
|
||||
# For the default filtering for embedded metrics,
|
||||
# uses the 'id' key in dashboard-yml definition for
|
||||
# identification.
|
||||
|
@ -33,10 +33,6 @@ module Metrics
|
|||
{ 'panel_groups' => [{ 'panels' => panels }] }
|
||||
end
|
||||
|
||||
def cache_key
|
||||
"dynamic_metrics_dashboard_#{metric_identifiers.join('_')}"
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Returns an array of the panels groups on the
|
||||
|
@ -58,6 +54,10 @@ module Metrics
|
|||
def metric_identifiers
|
||||
DEFAULT_EMBEDDED_METRICS_IDENTIFIERS
|
||||
end
|
||||
|
||||
def identifiers
|
||||
metric_identifiers.join('|')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
78
app/services/metrics/dashboard/dynamic_embed_service.rb
Normal file
78
app/services/metrics/dashboard/dynamic_embed_service.rb
Normal file
|
@ -0,0 +1,78 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Responsible for returning a filtered project dashboard
|
||||
# containing only the request-provided metrics. The result
|
||||
# is then cached for future requests. Metrics are identified
|
||||
# based on a combination of identifiers for now, but the ideal
|
||||
# would be similar to the approach in DefaultEmbedService, but
|
||||
# a single unique identifier is not currently available across
|
||||
# all metric types (custom, project-defined, cluster, or system).
|
||||
#
|
||||
# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class DynamicEmbedService < ::Metrics::Dashboard::BaseEmbedService
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
class << self
|
||||
# Determines whether the provided params are sufficient
|
||||
# to uniquely identify a panel from a yml-defined dashboard.
|
||||
#
|
||||
# See https://docs.gitlab.com/ee/user/project/integrations/prometheus.html#defining-custom-dashboards-per-project
|
||||
# for additional info on defining custom dashboards.
|
||||
def valid_params?(params)
|
||||
[
|
||||
params[:embedded],
|
||||
params[:group].present?,
|
||||
params[:title].present?,
|
||||
params[:y_label]
|
||||
].all?
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a new dashboard with only the matching
|
||||
# metrics from the system dashboard, stripped of groups.
|
||||
# @return [Hash]
|
||||
def get_raw_dashboard
|
||||
not_found! if panels.empty?
|
||||
|
||||
{ 'panel_groups' => [{ 'panels' => panels }] }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def panels
|
||||
strong_memoize(:panels) do
|
||||
not_found! unless base_dashboard
|
||||
not_found! unless groups = base_dashboard['panel_groups']
|
||||
not_found! unless matching_group = find_group(groups)
|
||||
not_found! unless all_panels = matching_group['panels']
|
||||
|
||||
find_panels(all_panels)
|
||||
end
|
||||
end
|
||||
|
||||
def base_dashboard
|
||||
strong_memoize(:base_dashboard) do
|
||||
Gitlab::Metrics::Dashboard::Finder.find_raw(project, dashboard_path: dashboard_path)
|
||||
end
|
||||
end
|
||||
|
||||
def find_group(groups)
|
||||
groups.find do |candidate_group|
|
||||
candidate_group['group'] == group
|
||||
end
|
||||
end
|
||||
|
||||
def find_panels(all_panels)
|
||||
all_panels.select do |panel|
|
||||
panel['title'] == title && panel['y_label'] == y_label
|
||||
end
|
||||
end
|
||||
|
||||
def not_found!
|
||||
panels_not_found!(identifiers)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
14
lib/gitlab/metrics/dashboard/defaults.rb
Normal file
14
lib/gitlab/metrics/dashboard/defaults.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Central point for managing default attributes from within
|
||||
# the metrics dashboard module.
|
||||
module Gitlab
|
||||
module Metrics
|
||||
module Dashboard
|
||||
module Defaults
|
||||
DEFAULT_PANEL_TYPE = 'area-chart'
|
||||
DEFAULT_PANEL_WEIGHT = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
34
lib/gitlab/metrics/dashboard/errors.rb
Normal file
34
lib/gitlab/metrics/dashboard/errors.rb
Normal file
|
@ -0,0 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Central point for managing errors from within the metrics
|
||||
# dashboard module. Handles errors from dashboard retrieval
|
||||
# and processing steps, as well as defines shared error classes.
|
||||
module Gitlab
|
||||
module Metrics
|
||||
module Dashboard
|
||||
module Errors
|
||||
PanelNotFoundError = Class.new(StandardError)
|
||||
|
||||
PROCESSING_ERROR = Gitlab::Metrics::Dashboard::Stages::BaseStage::DashboardProcessingError
|
||||
NOT_FOUND_ERROR = Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
|
||||
|
||||
def handle_errors(error)
|
||||
case error
|
||||
when PROCESSING_ERROR
|
||||
error(error.message, :unprocessable_entity)
|
||||
when NOT_FOUND_ERROR
|
||||
error("#{dashboard_path} could not be found.", :not_found)
|
||||
when PanelNotFoundError
|
||||
error(error.message, :not_found)
|
||||
else
|
||||
raise error
|
||||
end
|
||||
end
|
||||
|
||||
def panels_not_found!(opts)
|
||||
raise PanelNotFoundError.new("No panels matching properties #{opts}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -12,21 +12,37 @@ module Gitlab
|
|||
# @param project [Project]
|
||||
# @param user [User]
|
||||
# @param environment [Environment]
|
||||
# @param opts - dashboard_path [String] Path at which the
|
||||
# dashboard can be found. Nil values will
|
||||
# default to the system dashboard.
|
||||
# @param opts - embedded [Boolean] Determines whether the
|
||||
# @param options - embedded [Boolean] Determines whether the
|
||||
# dashboard is to be rendered as part of an
|
||||
# issue or location other than the primary
|
||||
# metrics dashboard UI. Returns only the
|
||||
# Memory/CPU charts of the system dash.
|
||||
# @param options - dashboard_path [String] Path at which the
|
||||
# dashboard can be found. Nil values will
|
||||
# default to the system dashboard.
|
||||
# @param options - group [String] Title of the group
|
||||
# to which a panel might belong. Used by
|
||||
# embedded dashboards.
|
||||
# @param options - title [String] Title of the panel.
|
||||
# Used by embedded dashboards.
|
||||
# @param options - y_label [String] Y-Axis label of
|
||||
# a panel. Used by embedded dashboards.
|
||||
# @return [Hash]
|
||||
def find(project, user, environment, dashboard_path: nil, embedded: false)
|
||||
service_for_path(dashboard_path, embedded: embedded)
|
||||
.new(project, user, environment: environment, dashboard_path: dashboard_path)
|
||||
def find(project, user, environment, options = {})
|
||||
service_for(options)
|
||||
.new(project, user, options.merge(environment: environment))
|
||||
.get_dashboard
|
||||
end
|
||||
|
||||
# Returns a dashboard without any supplemental info.
|
||||
# Returns only full, yml-defined dashboards.
|
||||
# @return [Hash]
|
||||
def find_raw(project, dashboard_path: nil)
|
||||
service_for(dashboard_path: dashboard_path)
|
||||
.new(project, nil, dashboard_path: dashboard_path)
|
||||
.raw_dashboard
|
||||
end
|
||||
|
||||
# Summary of all known dashboards.
|
||||
# @return [Array<Hash>] ex) [{ path: String,
|
||||
# display_name: String,
|
||||
|
@ -46,13 +62,6 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
def service_for_path(dashboard_path, embedded:)
|
||||
return embed_service if embedded
|
||||
return system_service if system_dashboard?(dashboard_path)
|
||||
|
||||
project_service
|
||||
end
|
||||
|
||||
def system_service
|
||||
::Metrics::Dashboard::SystemDashboardService
|
||||
end
|
||||
|
@ -61,12 +70,8 @@ module Gitlab
|
|||
::Metrics::Dashboard::ProjectDashboardService
|
||||
end
|
||||
|
||||
def embed_service
|
||||
::Metrics::Dashboard::DefaultEmbedService
|
||||
end
|
||||
|
||||
def system_dashboard?(filepath)
|
||||
!filepath || system_service.system_dashboard?(filepath)
|
||||
def service_for(options)
|
||||
Gitlab::Metrics::Dashboard::ServiceSelector.call(options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
50
lib/gitlab/metrics/dashboard/service_selector.rb
Normal file
50
lib/gitlab/metrics/dashboard/service_selector.rb
Normal file
|
@ -0,0 +1,50 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Responsible for determining which dashboard service should
|
||||
# be used to fetch or generate a dashboard hash.
|
||||
# The services can be considered in two categories - embeds
|
||||
# and dashboards. Embeds are all portions of dashboards.
|
||||
module Gitlab
|
||||
module Metrics
|
||||
module Dashboard
|
||||
class ServiceSelector
|
||||
SERVICES = ::Metrics::Dashboard
|
||||
|
||||
class << self
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
# Returns a class which inherits from the BaseService
|
||||
# class that can be used to obtain a dashboard.
|
||||
# @return [Gitlab::Metrics::Dashboard::Services::BaseService]
|
||||
def call(params)
|
||||
return SERVICES::CustomMetricEmbedService if custom_metric_embed?(params)
|
||||
return SERVICES::DynamicEmbedService if dynamic_embed?(params)
|
||||
return SERVICES::DefaultEmbedService if params[:embedded]
|
||||
return SERVICES::SystemDashboardService if system_dashboard?(params[:dashboard_path])
|
||||
return SERVICES::ProjectDashboardService if params[:dashboard_path]
|
||||
|
||||
default_service
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def default_service
|
||||
SERVICES::SystemDashboardService
|
||||
end
|
||||
|
||||
def system_dashboard?(filepath)
|
||||
SERVICES::SystemDashboardService.system_dashboard?(filepath)
|
||||
end
|
||||
|
||||
def custom_metric_embed?(params)
|
||||
SERVICES::CustomMetricEmbedService.valid_params?(params)
|
||||
end
|
||||
|
||||
def dynamic_embed?(params)
|
||||
SERVICES::DynamicEmbedService.valid_params?(params)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,11 +5,11 @@ module Gitlab
|
|||
module Dashboard
|
||||
module Stages
|
||||
class BaseStage
|
||||
include Gitlab::Metrics::Dashboard::Defaults
|
||||
|
||||
DashboardProcessingError = Class.new(StandardError)
|
||||
LayoutError = Class.new(DashboardProcessingError)
|
||||
|
||||
DEFAULT_PANEL_TYPE = 'area-chart'
|
||||
|
||||
attr_reader :project, :environment, :dashboard
|
||||
|
||||
def initialize(project, environment, dashboard)
|
||||
|
|
|
@ -97,7 +97,7 @@ module Gitlab
|
|||
end
|
||||
|
||||
def new_metric(metric)
|
||||
metric.queries.first.merge(metric_id: metric.id)
|
||||
metric.to_metric_hash
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -518,10 +518,10 @@ describe Projects::EnvironmentsController do
|
|||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'the default dynamic dashboard' do
|
||||
shared_examples_for 'specified dashboard embed' do |expected_titles|
|
||||
it_behaves_like '200 response'
|
||||
|
||||
it 'contains only the Memory and CPU charts' do
|
||||
it 'contains only the specified charts' do
|
||||
get :metrics_dashboard, params: environment_params(dashboard_params)
|
||||
|
||||
dashboard = json_response['dashboard']
|
||||
|
@ -531,10 +531,14 @@ describe Projects::EnvironmentsController do
|
|||
expect(dashboard['dashboard']).to be_nil
|
||||
expect(dashboard['panel_groups'].length).to eq 1
|
||||
expect(panel_group['group']).to be_nil
|
||||
expect(titles).to eq ['Memory Usage (Total)', 'Core Usage (Total)']
|
||||
expect(titles).to eq expected_titles
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples_for 'the default dynamic dashboard' do
|
||||
it_behaves_like 'specified dashboard embed', ['Memory Usage (Total)', 'Core Usage (Total)']
|
||||
end
|
||||
|
||||
shared_examples_for 'dashboard can be specified' do
|
||||
context 'when dashboard is specified' do
|
||||
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
|
||||
|
@ -551,7 +555,7 @@ describe Projects::EnvironmentsController do
|
|||
end
|
||||
|
||||
context 'when the specified dashboard is the default dashboard' do
|
||||
let(:dashboard_path) { ::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH }
|
||||
let(:dashboard_path) { system_dashboard_path }
|
||||
|
||||
it_behaves_like 'the default dashboard'
|
||||
end
|
||||
|
@ -564,12 +568,40 @@ describe Projects::EnvironmentsController do
|
|||
|
||||
it_behaves_like 'the default dynamic dashboard'
|
||||
|
||||
context 'when the dashboard is specified' do
|
||||
let(:dashboard_params) { { format: :json, embedded: true, dashboard: '.gitlab/dashboards/fake.yml' } }
|
||||
context 'when incomplete dashboard params are provided' do
|
||||
let(:dashboard_params) { { format: :json, embedded: true, title: 'Title' } }
|
||||
|
||||
# The dashboard param should be ignored.
|
||||
# The title param should be ignored.
|
||||
it_behaves_like 'the default dynamic dashboard'
|
||||
end
|
||||
|
||||
context 'when invalid params are provided' do
|
||||
let(:dashboard_params) { { format: :json, embedded: true, metric_id: 16 } }
|
||||
|
||||
# The superfluous param should be ignored.
|
||||
it_behaves_like 'the default dynamic dashboard'
|
||||
end
|
||||
|
||||
context 'when the dashboard is correctly specified' do
|
||||
let(:dashboard_params) do
|
||||
{
|
||||
format: :json,
|
||||
embedded: true,
|
||||
dashboard: system_dashboard_path,
|
||||
group: business_metric_title,
|
||||
title: 'title',
|
||||
y_label: 'y_label'
|
||||
}
|
||||
end
|
||||
|
||||
it_behaves_like 'error response', :not_found
|
||||
|
||||
context 'and exists' do
|
||||
let!(:metric) { create(:prometheus_metric, project: project) }
|
||||
|
||||
it_behaves_like 'specified dashboard embed', ['title']
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"unit": { "type": "string" },
|
||||
"label": { "type": "string" },
|
||||
"track": { "type": "string" },
|
||||
"prometheus_endpoint_path": { "type": "string" }
|
||||
"prometheus_endpoint_path": { "type": "string" },
|
||||
"metric_id": { "type": "number" }
|
||||
},
|
||||
"additionalProperties": false
|
||||
}
|
||||
|
|
8
spec/lib/gitlab/metrics/dashboard/defaults_spec.rb
Normal file
8
spec/lib/gitlab/metrics/dashboard/defaults_spec.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Metrics::Dashboard::Defaults do
|
||||
it { is_expected.to be_const_defined(:DEFAULT_PANEL_TYPE) }
|
||||
it { is_expected.to be_const_defined(:DEFAULT_PANEL_WEIGHT) }
|
||||
end
|
|
@ -5,10 +5,9 @@ require 'spec_helper'
|
|||
describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_caching do
|
||||
include MetricsDashboardHelpers
|
||||
|
||||
set(:project) { build(:project) }
|
||||
set(:project) { create(:project) }
|
||||
set(:user) { create(:user) }
|
||||
set(:environment) { create(:environment, project: project) }
|
||||
let(:system_dashboard_path) { ::Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH}
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
|
@ -52,9 +51,80 @@ describe Gitlab::Metrics::Dashboard::Finder, :use_clean_rails_memory_store_cachi
|
|||
end
|
||||
|
||||
context 'when the dashboard is expected to be embedded' do
|
||||
let(:service_call) { described_class.find(project, user, environment, dashboard_path: nil, embedded: true) }
|
||||
let(:service_call) { described_class.find(project, user, environment, **params) }
|
||||
let(:params) { { embedded: true } }
|
||||
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
|
||||
context 'when params are incomplete' do
|
||||
let(:params) { { embedded: true, dashboard_path: system_dashboard_path } }
|
||||
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
end
|
||||
|
||||
context 'when the panel is specified' do
|
||||
context 'as a custom metric' do
|
||||
let(:params) do
|
||||
{ embedded: true,
|
||||
dashboard_path: system_dashboard_path,
|
||||
group: business_metric_title,
|
||||
title: 'title',
|
||||
y_label: 'y_label' }
|
||||
end
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
|
||||
context 'when the metric exists' do
|
||||
before do
|
||||
create(:prometheus_metric, project: project)
|
||||
end
|
||||
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
end
|
||||
end
|
||||
|
||||
context 'as a project-defined panel' do
|
||||
let(:dashboard_path) { '.gitlab/dashboard/test.yml' }
|
||||
let(:params) do
|
||||
{ embedded: true,
|
||||
dashboard_path: dashboard_path,
|
||||
group: 'Group A',
|
||||
title: 'Super Chart A1',
|
||||
y_label: 'y_label' }
|
||||
end
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
|
||||
context 'when the metric exists' do
|
||||
let(:project) { project_with_dashboard(dashboard_path) }
|
||||
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.find_raw' do
|
||||
let(:dashboard) { YAML.load_file(Rails.root.join('config', 'prometheus', 'common_metrics.yml')) }
|
||||
let(:params) { {} }
|
||||
|
||||
subject { described_class.find_raw(project, **params) }
|
||||
|
||||
it { is_expected.to eq dashboard }
|
||||
|
||||
context 'when the system dashboard is specified' do
|
||||
let(:params) { { dashboard_path: system_dashboard_path } }
|
||||
|
||||
it { is_expected.to eq dashboard }
|
||||
end
|
||||
|
||||
context 'when an existing project dashboard is specified' do
|
||||
let(:dashboard) { YAML.safe_load(fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml')) }
|
||||
let(:params) { { dashboard_path: '.gitlab/dashboards/test.yml' } }
|
||||
let(:project) { project_with_dashboard(params[:dashboard_path]) }
|
||||
|
||||
it { is_expected.to eq dashboard }
|
||||
end
|
||||
end
|
||||
|
||||
|
|
80
spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb
Normal file
80
spec/lib/gitlab/metrics/dashboard/service_selector_spec.rb
Normal file
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::Metrics::Dashboard::ServiceSelector do
|
||||
include MetricsDashboardHelpers
|
||||
|
||||
describe '#call' do
|
||||
let(:arguments) { {} }
|
||||
|
||||
subject { described_class.call(arguments) }
|
||||
|
||||
it { is_expected.to be Metrics::Dashboard::SystemDashboardService }
|
||||
|
||||
context 'when just the dashboard path is provided' do
|
||||
let(:arguments) { { dashboard_path: '.gitlab/dashboards/test.yml' } }
|
||||
|
||||
it { is_expected.to be Metrics::Dashboard::ProjectDashboardService }
|
||||
|
||||
context 'when the path is for the system dashboard' do
|
||||
let(:arguments) { { dashboard_path: system_dashboard_path } }
|
||||
|
||||
it { is_expected.to be Metrics::Dashboard::SystemDashboardService }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the embedded flag is provided' do
|
||||
let(:arguments) { { embedded: true } }
|
||||
|
||||
it { is_expected.to be Metrics::Dashboard::DefaultEmbedService }
|
||||
|
||||
context 'when an incomplete set of dashboard identifiers are provided' do
|
||||
let(:arguments) { { embedded: true, dashboard_path: '.gitlab/dashboards/test.yml' } }
|
||||
|
||||
it { is_expected.to be Metrics::Dashboard::DefaultEmbedService }
|
||||
end
|
||||
|
||||
context 'when all the chart identifiers are provided' do
|
||||
let(:arguments) do
|
||||
{
|
||||
embedded: true,
|
||||
dashboard_path: '.gitlab/dashboards/test.yml',
|
||||
group: 'Important Metrics',
|
||||
title: 'Total Requests',
|
||||
y_label: 'req/sec'
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to be Metrics::Dashboard::DynamicEmbedService }
|
||||
end
|
||||
|
||||
context 'when all chart params expect dashboard_path are provided' do
|
||||
let(:arguments) do
|
||||
{
|
||||
embedded: true,
|
||||
group: 'Important Metrics',
|
||||
title: 'Total Requests',
|
||||
y_label: 'req/sec'
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to be Metrics::Dashboard::DynamicEmbedService }
|
||||
end
|
||||
|
||||
context 'with a system dashboard and "custom" group' do
|
||||
let(:arguments) do
|
||||
{
|
||||
embedded: true,
|
||||
dashboard_path: system_dashboard_path,
|
||||
group: business_metric_title,
|
||||
title: 'Total Requests',
|
||||
y_label: 'req/sec'
|
||||
}
|
||||
end
|
||||
|
||||
it { is_expected.to be Metrics::Dashboard::CustomMetricEmbedService }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -150,4 +150,17 @@ describe PrometheusMetric do
|
|||
expect(subject.to_query_metric.queries).to eq(queries)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#to_metric_hash' do
|
||||
it 'returns a hash suitable for inclusion on a metrics dashboard' do
|
||||
expected_output = {
|
||||
query_range: subject.query,
|
||||
unit: subject.unit,
|
||||
label: subject.legend,
|
||||
metric_id: subject.id
|
||||
}
|
||||
|
||||
expect(subject.to_metric_hash).to eq(expected_output)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,145 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Metrics::Dashboard::CustomMetricEmbedService do
|
||||
include MetricsDashboardHelpers
|
||||
|
||||
set(:project) { build(:project) }
|
||||
set(:user) { create(:user) }
|
||||
set(:environment) { create(:environment, project: project) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
let(:dashboard_path) { system_dashboard_path }
|
||||
let(:group) { business_metric_title }
|
||||
let(:title) { 'title' }
|
||||
let(:y_label) { 'y_label' }
|
||||
|
||||
describe '.valid_params?' do
|
||||
let(:valid_params) do
|
||||
{
|
||||
embedded: true,
|
||||
dashboard_path: dashboard_path,
|
||||
group: group,
|
||||
title: title,
|
||||
y_label: y_label
|
||||
}
|
||||
end
|
||||
|
||||
subject { described_class.valid_params?(params) }
|
||||
|
||||
let(:params) { valid_params }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'not embedded' do
|
||||
let(:params) { valid_params.except(:embedded) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'non-system dashboard' do
|
||||
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'undefined dashboard' do
|
||||
let(:params) { valid_params.except(:dashboard_path) }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'non-custom metric group' do
|
||||
let(:group) { 'Different Group' }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'missing group' do
|
||||
let(:group) { nil }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'missing title' do
|
||||
let(:title) { nil }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'undefined y-axis label' do
|
||||
let(:params) { valid_params.except(:y_label) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_dashboard' do
|
||||
let(:service_params) do
|
||||
[
|
||||
project,
|
||||
user,
|
||||
{
|
||||
embedded: true,
|
||||
environment: environment,
|
||||
dashboard_path: dashboard_path,
|
||||
group: group,
|
||||
title: title,
|
||||
y_label: y_label
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
let(:service_call) { described_class.new(*service_params).get_dashboard }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
it_behaves_like 'raises error for users with insufficient permissions'
|
||||
|
||||
context 'the custom metric exists' do
|
||||
let!(:metric) { create(:prometheus_metric, project: project) }
|
||||
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
|
||||
it 'does not cache the unprocessed dashboard' do
|
||||
expect(Gitlab::Metrics::Dashboard::Cache).not_to receive(:fetch)
|
||||
|
||||
described_class.new(*service_params).get_dashboard
|
||||
end
|
||||
|
||||
context 'multiple metrics meet criteria' do
|
||||
let!(:metric_2) { create(:prometheus_metric, project: project, query: 'avg(metric_2)') }
|
||||
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
|
||||
it 'includes both metrics' do
|
||||
result = service_call
|
||||
included_queries = all_queries(result[:dashboard])
|
||||
|
||||
expect(included_queries).to include('avg(metric_2)', 'avg(metric)')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the metric exists in another project' do
|
||||
let!(:metric) { create(:prometheus_metric, project: create(:project)) }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def all_queries(dashboard)
|
||||
dashboard[:panel_groups].flat_map do |group|
|
||||
group[:panels].flat_map do |panel|
|
||||
panel[:metrics].map do |metric|
|
||||
metric[:query_range]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
151
spec/services/metrics/dashboard/dynamic_embed_service_spec.rb
Normal file
151
spec/services/metrics/dashboard/dynamic_embed_service_spec.rb
Normal file
|
@ -0,0 +1,151 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
describe Metrics::Dashboard::DynamicEmbedService, :use_clean_rails_memory_store_caching do
|
||||
include MetricsDashboardHelpers
|
||||
|
||||
set(:project) { build(:project) }
|
||||
set(:user) { create(:user) }
|
||||
set(:environment) { create(:environment, project: project) }
|
||||
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
let(:dashboard_path) { '.gitlab/dashboards/test.yml' }
|
||||
let(:group) { 'Group A' }
|
||||
let(:title) { 'Super Chart A1' }
|
||||
let(:y_label) { 'y_label' }
|
||||
|
||||
describe '.valid_params?' do
|
||||
let(:valid_params) do
|
||||
{
|
||||
embedded: true,
|
||||
dashboard_path: dashboard_path,
|
||||
group: group,
|
||||
title: title,
|
||||
y_label: y_label
|
||||
}
|
||||
end
|
||||
|
||||
subject { described_class.valid_params?(params) }
|
||||
|
||||
let(:params) { valid_params }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
|
||||
context 'not embedded' do
|
||||
let(:params) { valid_params.except(:embedded) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'undefined dashboard' do
|
||||
let(:params) { valid_params.except(:dashboard_path) }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'missing dashboard' do
|
||||
let(:dashboard) { '' }
|
||||
|
||||
it { is_expected.to be_truthy }
|
||||
end
|
||||
|
||||
context 'missing group' do
|
||||
let(:group) { '' }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'missing title' do
|
||||
let(:title) { '' }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
|
||||
context 'undefined y-axis label' do
|
||||
let(:params) { valid_params.except(:y_label) }
|
||||
|
||||
it { is_expected.to be_falsey }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#get_dashboard' do
|
||||
let(:service_params) do
|
||||
[
|
||||
project,
|
||||
user,
|
||||
{
|
||||
environment: environment,
|
||||
dashboard_path: dashboard_path,
|
||||
group: group,
|
||||
title: title,
|
||||
y_label: y_label
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
let(:service_call) { described_class.new(*service_params).get_dashboard }
|
||||
|
||||
context 'when the dashboard does not exist' do
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
end
|
||||
|
||||
context 'when the dashboard is exists' do
|
||||
let(:project) { project_with_dashboard(dashboard_path) }
|
||||
|
||||
it_behaves_like 'valid embedded dashboard service response'
|
||||
it_behaves_like 'raises error for users with insufficient permissions'
|
||||
|
||||
it 'caches the unprocessed dashboard for subsequent calls' do
|
||||
expect(YAML).to receive(:safe_load).once.and_call_original
|
||||
|
||||
described_class.new(*service_params).get_dashboard
|
||||
described_class.new(*service_params).get_dashboard
|
||||
end
|
||||
|
||||
context 'when the specified group is not present on the dashboard' do
|
||||
let(:group) { 'Group Not Found' }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
end
|
||||
|
||||
context 'when the specified title is not present on the dashboard' do
|
||||
let(:title) { 'Title Not Found' }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
end
|
||||
|
||||
context 'when the specified y-axis label is not present on the dashboard' do
|
||||
let(:y_label) { 'Y-Axis Not Found' }
|
||||
|
||||
it_behaves_like 'misconfigured dashboard service response', :not_found
|
||||
end
|
||||
end
|
||||
|
||||
shared_examples 'uses system dashboard' do
|
||||
it 'uses the default dashboard' do
|
||||
expect(Gitlab::Metrics::Dashboard::Finder)
|
||||
.to receive(:find_raw)
|
||||
.with(project, dashboard_path: system_dashboard_path)
|
||||
.once
|
||||
|
||||
service_call
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the dashboard is nil' do
|
||||
let(:dashboard_path) { nil }
|
||||
|
||||
it_behaves_like 'uses system dashboard'
|
||||
end
|
||||
|
||||
context 'when the dashboard is not present' do
|
||||
let(:dashboard_path) { '' }
|
||||
|
||||
it_behaves_like 'uses system dashboard'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -18,6 +18,14 @@ module MetricsDashboardHelpers
|
|||
project.repository.refresh_method_caches([:metrics_dashboard])
|
||||
end
|
||||
|
||||
def system_dashboard_path
|
||||
Metrics::Dashboard::SystemDashboardService::SYSTEM_DASHBOARD_PATH
|
||||
end
|
||||
|
||||
def business_metric_title
|
||||
PrometheusMetricEnums.group_details[:business][:group_title]
|
||||
end
|
||||
|
||||
shared_examples_for 'misconfigured dashboard service response' do |status_code|
|
||||
it 'returns an appropriate message and status code' do
|
||||
result = service_call
|
||||
|
|
Loading…
Reference in a new issue