Commit d3097abc authored by syasonik's avatar syasonik

Support alerting and distinct stages

parent 730cf5e3
...@@ -158,11 +158,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -158,11 +158,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end end
def metrics_dashboard def metrics_dashboard
render_403 && return unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, project) render_403 && return unless Feature.enabled?(:environment_metrics_use_prometheus_endpoint, @project)
respond_to do |format| respond_to do |format|
format.json do format.json do
dashboard = MetricsDashboardService.new(@project).get_dashboard dashboard = Gitlab::MetricsDashboard::Service.new(@project, environment).get_dashboard
render json: dashboard, status: :ok render json: dashboard, status: :ok
end end
......
# frozen_string_literal: true
class MetricsDashboardProcessingService
DEFAULT_PANEL_TYPE = 'area-chart'
def initialize(dashboard, project)
@dashboard = dashboard.deep_transform_keys(&:to_sym)
@project = project
end
def process
insert_metric_ids!
sort_groups!
sort_panels!
insert_project_metrics!
@dashboard.to_json
end
private
# ------- Processing Steps -----------
# For each metric in the dashboard config, attempts to find a corresponding
# database record. If found, includes the record's id in the dashboard config.
def insert_metric_ids!
for_metrics do |metric|
metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
metric[:metric_id] = metric_record.id if metric_record
end
end
# Inserts project-specific metrics into the dashboard config.
# If there are no project-specific metrics, this will have no effect.
def insert_project_metrics!
project_metrics.each do |project_metric|
group = find_or_create_group(@dashboard[:panel_groups], project_metric)
panel = find_or_create_panel(group[:panels], project_metric)
find_or_create_metric(panel[:metrics], project_metric)
end
end
# Sorts the groups in the dashboard by the :priority key
def sort_groups!
@dashboard[:panel_groups] = @dashboard[:panel_groups].sort_by { |group| group[:priority] }
end
# Sorts the panels in the dashboard by the :weight key
def sort_panels!
@dashboard[:panel_groups].each do |group|
group[:panels] = group[:panels].sort_by { |panel| panel[:weight] }
end
end
# ------- Processing Helpers -----------
def project_metrics
@project.prometheus_metrics
end
def common_metrics
@common_metrics ||= ::PrometheusMetric.common
end
def for_metrics
@dashboard[:panel_groups].each do |panel_group|
panel_group[:panels].each do |panel|
panel[:metrics].each do |metric|
yield metric
end
end
end
end
def find_or_create_group(panel_groups, metric)
target_group = panel_groups.find { |group| group[:group] == metric.group_title }
unless target_group
target_group = {
group: metric.group_title,
priority: metric.priority,
panels: []
}
panel_groups << target_group
end
target_group
end
def find_or_create_panel(panels, metric)
panel_identifiers = [DEFAULT_PANEL_TYPE, metric.title, metric.y_label]
target_panel = panels.find { |panel| panel.values_at(:type, :title, :y_label) == panel_identifiers }
unless target_panel
target_panel = {
type: DEFAULT_PANEL_TYPE,
title: metric.title,
y_label: metric.y_label,
metrics: []
}
panels << target_panel
end
target_panel
end
def find_or_create_metric(metrics, metric)
target_metric = metrics.find { |m| m[:id] == metric.identifier }
unless target_metric
target_metric = metric.queries.first.merge(metric_id: metric.id)
metrics << target_metric
end
target_metric
end
end
# frozen_string_literal: true
# Fetches the metrics dashboard layout and supplemented the output with DB info.
class MetricsDashboardService
SYSTEM_DASHBOARD_NAME = 'common_metrics'
SYSTEM_DASHBOARD_PATH = Rails.root.join('config', 'prometheus', "#{SYSTEM_DASHBOARD_NAME}.yml")
def initialize(project)
@project = project
end
# Returns a DB-supplemented json representation of a dashboard config file.
def get_dashboard
dashboard = Rails.cache.fetch(cache_key) { system_dashboard }
process_dashboard(dashboard)
end
private
# Returns the base metrics shipped with every GitLab service.
def system_dashboard
YAML.load_file(SYSTEM_DASHBOARD_PATH)
end
def cache_key
"metrics_dashboard_#{SYSTEM_DASHBOARD_NAME}"
end
def process_dashboard(dashboard)
MetricsDashboardProcessingService.new(dashboard, @project).process
end
end
# frozen_string_literal: true
module EE
module Gitlab
module MetricsDashboard
module Processor
def stages
@stages ||= super + [Stages::AlertsInserter]
end
end
end
end
end
# frozen_string_literal: true
require 'set'
module EE
module Gitlab
module MetricsDashboard
module Stages
class AlertsInserter < ::Gitlab::MetricsDashboard::Stages::BaseStage
def transform!
alerts = metrics_with_alerts
for_metrics do |metric|
next unless metric_id = metric[:metric_id]
next unless alerts.include?(metric_id)
metric[:alert_path] = alert_path(metric_id, project, environment)
end
end
private
def metrics_with_alerts
alerts = ::Projects::Prometheus::AlertsFinder
.new(project: project, environment: environment)
.execute
Set.new(alerts.map(&:id))
end
def alert_path(metric_id, project, environment)
::Gitlab::Routing.url_helpers.project_prometheus_alert_path(project, metric_id, environment_id: environment.id, format: :json)
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::MetricsDashboard::Processor do
let(:project) { build(:project) }
let(:environment) { alert.environment }
let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics_dashboard/sample_dashboard.yml') }
describe 'process' do
let(:process_params) { [dashboard_yml, project, environment] }
let(:dashboard_json) { described_class.new(*process_params).process }
let(:dashboard) { JSON.parse(dashboard_json, symbolize_names: true) }
context 'when the dashboard references persisted metrics with alerts' do
let!(:alert) { create( :prometheus_alert, project: project, prometheus_metric: persisted_metric ) }
shared_examples_for 'has saved alerts' do
it 'includes an alert path' do
target_metric = all_metrics.find { |metric| metric[:metric_id] == persisted_metric.id }
expect(target_metric).to be_a Hash
expect(target_metric).to include(:alert_path)
expect(target_metric[:alert_path]).to include(
project.path,
persisted_metric.id.to_s,
environment.id.to_s
)
end
end
context 'that are shared across projects' do
let!(:persisted_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
it_behaves_like 'has saved alerts'
end
context 'when the project has associated metrics' do
let!(:persisted_metric) { create(:prometheus_metric, project: project, group: :business) }
it_behaves_like 'has saved alerts'
end
end
end
private
def all_metrics
dashboard[:panel_groups].map do |group|
group[:panels].map { |panel| panel[:metrics] }
end.flatten
end
end
...@@ -3,18 +3,27 @@ ...@@ -3,18 +3,27 @@
module Gitlab module Gitlab
module MetricsDashboard module MetricsDashboard
class Processor class Processor
STAGES = [CommonMetricsInserter, ProjectMetricsInserter, Sorter].freeze def initialize(dashboard, project, environment)
def initialize(dashboard, project)
@dashboard = dashboard.deep_transform_keys(&:to_sym) @dashboard = dashboard.deep_transform_keys(&:to_sym)
@project = project @project = project
@environment = environment
end
def stages
@stages ||= [
Stages::CommonMetricsInserter,
Stages::ProjectMetricsInserter,
Stages::Sorter
].freeze
end end
def process def process
STAGES.each { |stage| stage.transform!(@dashboard, @project) } stages.each { |stage| stage.new(@dashboard, @project, @environment).transform! }
@dashboard.to_json @dashboard.to_json
end end
end end
end end
end end
Gitlab::MetricsDashboard::Processor.prepend EE::Gitlab::MetricsDashboard::Processor
...@@ -7,8 +7,9 @@ module Gitlab ...@@ -7,8 +7,9 @@ module Gitlab
SYSTEM_DASHBOARD_NAME = 'common_metrics' SYSTEM_DASHBOARD_NAME = 'common_metrics'
SYSTEM_DASHBOARD_PATH = Rails.root.join('config', 'prometheus', "#{SYSTEM_DASHBOARD_NAME}.yml") SYSTEM_DASHBOARD_PATH = Rails.root.join('config', 'prometheus', "#{SYSTEM_DASHBOARD_NAME}.yml")
def initialize(project) def initialize(project, environment)
@project = project @project = project
@environment = environment
end end
# Returns a DB-supplemented json representation of a dashboard config file. # Returns a DB-supplemented json representation of a dashboard config file.
...@@ -30,7 +31,7 @@ module Gitlab ...@@ -30,7 +31,7 @@ module Gitlab
end end
def process_dashboard(dashboard) def process_dashboard(dashboard)
Processor.new(dashboard, @project).process Processor.new(dashboard, @project, @environment).process
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module MetricsDashboard
module Stages
class BaseStage
DEFAULT_PANEL_TYPE = 'area-chart'
attr_reader :dashboard, :project, :environment
def initialize(dashboard, project, environment)
@dashboard = dashboard
@project = project
@environment = environment
end
# Entry-point to the stage
# @param dashboard [Hash]
# @param project [Project]
# @param environment [Environment]
def transform!
raise NotImplementedError
end
protected
def for_metrics
dashboard[:panel_groups].each do |panel_group|
panel_group[:panels].each do |panel|
panel[:metrics].each do |metric|
yield metric
end
end
end
end
end
end
end
end
...@@ -2,30 +2,18 @@ ...@@ -2,30 +2,18 @@
module Gitlab module Gitlab
module MetricsDashboard module MetricsDashboard
class CommonMetricsInserter module Stages
class << self class CommonMetricsInserter < BaseStage
# For each metric in the dashboard config, attempts to find a corresponding # For each metric in the dashboard config, attempts to find a corresponding
# database record. If found, includes the record's id in the dashboard config. # database record. If found, includes the record's id in the dashboard config.
def transform!(dashboard, _project) def transform!
common_metrics = ::PrometheusMetric.common common_metrics = ::PrometheusMetric.common
for_metrics(dashboard) do |metric| for_metrics do |metric|
metric_record = common_metrics.find { |m| m.identifier == metric[:id] } metric_record = common_metrics.find { |m| m.identifier == metric[:id] }
metric[:metric_id] = metric_record.id if metric_record metric[:metric_id] = metric_record.id if metric_record
end end
end end
private
def for_metrics(dashboard)
dashboard[:panel_groups].each do |panel_group|
panel_group[:panels].each do |panel|
panel[:metrics].each do |metric|
yield metric
end
end
end
end
end end
end end
end end
......
...@@ -2,13 +2,11 @@ ...@@ -2,13 +2,11 @@
module Gitlab module Gitlab
module MetricsDashboard module MetricsDashboard
class ProjectMetricsInserter module Stages
DEFAULT_PANEL_TYPE = 'area-chart' class ProjectMetricsInserter < BaseStage
class << self
# Inserts project-specific metrics into the dashboard config. # Inserts project-specific metrics into the dashboard config.
# If there are no project-specific metrics, this will have no effect. # If there are no project-specific metrics, this will have no effect.
def transform!(dashboard, project) def transform!
project.prometheus_metrics.each do |project_metric| project.prometheus_metrics.each do |project_metric|
group = find_or_create_panel_group(dashboard[:panel_groups], project_metric) group = find_or_create_panel_group(dashboard[:panel_groups], project_metric)
panel = find_or_create_panel(group[:panels], project_metric) panel = find_or_create_panel(group[:panels], project_metric)
......
...@@ -2,22 +2,22 @@ ...@@ -2,22 +2,22 @@
module Gitlab module Gitlab
module MetricsDashboard module MetricsDashboard
class Sorter module Stages
class << self class Sorter < BaseStage
def transform!(dashboard, _project) def transform!
sort_groups!(dashboard) sort_groups!
sort_panels!(dashboard) sort_panels!
end end
private private
# Sorts the groups in the dashboard by the :priority key # Sorts the groups in the dashboard by the :priority key
def sort_groups!(dashboard) def sort_groups!
dashboard[:panel_groups] = dashboard[:panel_groups].sort_by { |group| -group[:priority].to_i } dashboard[:panel_groups] = dashboard[:panel_groups].sort_by { |group| -group[:priority].to_i }
end end
# Sorts the panels in the dashboard by the :weight key # Sorts the panels in the dashboard by the :weight key
def sort_panels!(dashboard) def sort_panels!
dashboard[:panel_groups].each do |group| dashboard[:panel_groups].each do |group|
group[:panels] = group[:panels].sort_by { |panel| -panel[:weight].to_i } group[:panels] = group[:panels].sort_by { |panel| -panel[:weight].to_i }
end end
......
dashboard: 'Test Dashboard'
order: 1
panel_groups:
- group: Group A
priority: 10
panels:
- title: "Super Chart A1"
type: "area-chart"
y_label: "y_label"
weight: 2
metrics:
- id: metric_a1
query_range: 'query'
unit: unit
label: Legend Label
- title: "Super Chart A2"
type: "area-chart"
y_label: "y_label"
weight: 1
metrics:
- id: metric_a2
query_range: 'query'
label: Legend Label
unit: unit
- group: Group B
priority: 1
panels:
- title: "Super Chart B"
type: "area-chart"
y_label: "y_label"
weight: 1
metrics:
- id: metric_b
query_range: 'query'
unit: unit
label: Legend Label
...@@ -4,10 +4,13 @@ require 'spec_helper' ...@@ -4,10 +4,13 @@ require 'spec_helper'
describe Gitlab::MetricsDashboard::Processor do describe Gitlab::MetricsDashboard::Processor do
let(:project) { build(:project) } let(:project) { build(:project) }
let(:environment) { build(:environment) }
let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics_dashboard/sample_dashboard.yml') } let(:dashboard_yml) { YAML.load_file('spec/fixtures/lib/gitlab/metrics_dashboard/sample_dashboard.yml') }
describe 'process' do describe 'process' do
let(:dashboard) { JSON.parse(described_class.new(dashboard_yml, project).process, symbolize_names: true) } let(:process_params) { [dashboard_yml, project, environment] }
let(:dashboard_json) { described_class.new(*process_params).process }
let(:dashboard) { JSON.parse(dashboard_json, symbolize_names: true) }
context 'when dashboard config corresponds to common metrics' do context 'when dashboard config corresponds to common metrics' do
let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') } let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
...@@ -16,6 +19,7 @@ describe Gitlab::MetricsDashboard::Processor do ...@@ -16,6 +19,7 @@ describe Gitlab::MetricsDashboard::Processor do
target_metric = all_metrics.find { |metric| metric[:id] == 'metric_a1' } target_metric = all_metrics.find { |metric| metric[:id] == 'metric_a1' }
expect(target_metric).to include(:metric_id) expect(target_metric).to include(:metric_id)
expect(target_metric[:metric_id]).to eq(common_metric.id)
end end
end end
......
...@@ -4,10 +4,11 @@ require 'spec_helper' ...@@ -4,10 +4,11 @@ require 'spec_helper'
describe Gitlab::MetricsDashboard::Service, :use_clean_rails_memory_store_caching do describe Gitlab::MetricsDashboard::Service, :use_clean_rails_memory_store_caching do
let(:project) { build(:project) } let(:project) { build(:project) }
let(:environment) { build(:environment) }
describe 'get_dashboard' do describe 'get_dashboard' do
it 'returns a json representation of the environment dashboard' do it 'returns a json representation of the environment dashboard' do
dashboard = described_class.new(project).get_dashboard dashboard = described_class.new(project, environment).get_dashboard
json = JSON.parse(dashboard, symbolize_names: true) json = JSON.parse(dashboard, symbolize_names: true)
expect(json).to include(:dashboard, :order, :panel_groups) expect(json).to include(:dashboard, :order, :panel_groups)
...@@ -17,8 +18,8 @@ describe Gitlab::MetricsDashboard::Service, :use_clean_rails_memory_store_cachin ...@@ -17,8 +18,8 @@ describe Gitlab::MetricsDashboard::Service, :use_clean_rails_memory_store_cachin
it 'caches the dashboard for subsequent calls' do it 'caches the dashboard for subsequent calls' do
expect(YAML).to receive(:load_file).once.and_call_original expect(YAML).to receive(:load_file).once.and_call_original
described_class.new(project).get_dashboard described_class.new(project, environment).get_dashboard
described_class.new(project).get_dashboard described_class.new(project, environment).get_dashboard
end end
end end
end end
require 'spec_helper'
describe MetricsDashboardProcessingService do
let(:project) { build(:project) }
let(:dashboard_yml) { YAML.load_file('spec/fixtures/services/metrics_dashboard_processing_service.yml') }
describe 'process' do
let(:dashboard) { JSON.parse(described_class.new(dashboard_yml, project).process, symbolize_names: true) }
context 'when dashboard config corresponds to common metrics' do
let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
it 'inserts metric ids into the config' do
target_metric = all_metrics.find { |metric| metric[:id] == 'metric_a1' }
expect(target_metric).to include(:metric_id)
end
end
context 'when the project has associated metrics' do
let!(:project_metric) { create(:prometheus_metric, project: project) }
it 'includes project-specific metrics' do
project_metric_details = {
query_range: project_metric.query,
unit: project_metric.unit,
label: project_metric.legend,
metric_id: project_metric.id
}
expect(all_metrics).to include project_metric_details
end
it 'includes project metrics at the end of the config' do
expected_metrics_order = ['metric_b', 'metric_a2', 'metric_a1', nil]
actual_metrics_order = all_metrics.map { |m| m[:id] }
expect(actual_metrics_order).to eq expected_metrics_order
end
end
it 'orders groups by priority and panels by weight' do
expected_metrics_order = %w('metric_b metric_a2 metric_a1')
actual_metrics_order = all_metrics.map { |m| m[:id] }
expect(actual_metrics_order).to eq expected_metrics_order
end
end
def all_metrics
dashboard[:panel_groups].map do |group|
group[:panels].map { |panel| panel[:metrics] }
end.flatten
end
end
require 'spec_helper'
describe MetricsDashboardService, :use_clean_rails_memory_store_caching do
let(:project) { build(:project) }
describe 'get_dashboard' do
it 'returns a json representation of the environment dashboard' do
dashboard = described_class.new(project).get_dashboard
json = JSON.parse(dashboard, symbolize_names: true)
expect(json).to include(:dashboard, :order, :panel_groups)
expect(json[:panel_groups]).to all( include(:group, :priority, :panels) )
end
it 'caches the dashboard for subsequent calls' do
expect(YAML).to receive(:load_file).once.and_call_original
described_class.new(project).get_dashboard
described_class.new(project).get_dashboard
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment