Commit 204f4191 authored by Ryan Cobb's avatar Ryan Cobb

Hook branch update to import metrics

Hook branch update to import yml based metrics.
parent 5464cf12
...@@ -4,6 +4,7 @@ class PrometheusAlert < ApplicationRecord ...@@ -4,6 +4,7 @@ class PrometheusAlert < ApplicationRecord
include Sortable include Sortable
include UsageStatistics include UsageStatistics
include Presentable include Presentable
include EachBatch
OPERATORS_MAP = { OPERATORS_MAP = {
lt: "<", lt: "<",
......
# frozen_string_literal: true # frozen_string_literal: true
class PrometheusMetric < ApplicationRecord class PrometheusMetric < ApplicationRecord
include EachBatch
belongs_to :project, validate: true, inverse_of: :prometheus_metrics belongs_to :project, validate: true, inverse_of: :prometheus_metrics
has_many :prometheus_alerts, inverse_of: :prometheus_metric has_many :prometheus_alerts, inverse_of: :prometheus_metric
......
...@@ -76,12 +76,17 @@ module Git ...@@ -76,12 +76,17 @@ module Git
def branch_change_hooks def branch_change_hooks
enqueue_process_commit_messages enqueue_process_commit_messages
enqueue_jira_connect_sync_messages enqueue_jira_connect_sync_messages
enqueue_metrics_dashboard_sync if Feature.enabled?(:sync_metrics_dashboards, project)
end end
def branch_remove_hooks def branch_remove_hooks
project.repository.after_remove_branch(expire_cache: false) project.repository.after_remove_branch(expire_cache: false)
end end
def enqueue_metrics_dashboard_sync
::Metrics::Dashboard::SyncDashboardsWorker.perform_async(project.id)
end
# Schedules processing of commit messages # Schedules processing of commit messages
def enqueue_process_commit_messages def enqueue_process_commit_messages
referencing_commits = limited_commits.select(&:matches_cross_reference_regex?) referencing_commits = limited_commits.select(&:matches_cross_reference_regex?)
......
...@@ -42,6 +42,12 @@ module Metrics ...@@ -42,6 +42,12 @@ module Metrics
def cache_key def cache_key
"project_#{project.id}_metrics_dashboard_#{dashboard_path}" "project_#{project.id}_metrics_dashboard_#{dashboard_path}"
end end
def sequence
[
::Gitlab::Metrics::Dashboard::Stages::CustomDashboardMetricsInserter
] + super
end
end end
end end
end end
...@@ -1540,6 +1540,14 @@ ...@@ -1540,6 +1540,14 @@
:weight: 1 :weight: 1
:idempotent: true :idempotent: true
:tags: [] :tags: []
- :name: metrics_dashboard_sync_dashboards
:feature_category: :metrics
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: migrate_external_diffs - :name: migrate_external_diffs
:feature_category: :source_code_management :feature_category: :source_code_management
:has_external_dependencies: :has_external_dependencies:
......
# frozen_string_literal: true
module Metrics
module Dashboard
class SyncDashboardsWorker
include ApplicationWorker
feature_category :metrics
idempotent!
def perform(project_id)
project = Project.find(project_id)
dashboard_paths = ::Gitlab::Metrics::Dashboard::RepoDashboardFinder.list_dashboards(project)
return unless dashboard_paths.present?
dashboard_paths.each do |dashboard_path|
::Gitlab::Metrics::Dashboard::Importer.new(dashboard_path, project).execute!
end
end
end
end
end
---
name: sync_metrics_dashboards
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39658
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/241793
group: group::apm
type: development
default_enabled: false
...@@ -164,6 +164,8 @@ ...@@ -164,6 +164,8 @@
- 1 - 1
- - metrics_dashboard_prune_old_annotations - - metrics_dashboard_prune_old_annotations
- 1 - 1
- - metrics_dashboard_sync_dashboards
- 1
- - migrate_external_diffs - - migrate_external_diffs
- 1 - 1
- - namespaceless_project_destroy - - namespaceless_project_destroy
......
...@@ -13,11 +13,12 @@ module Gitlab ...@@ -13,11 +13,12 @@ module Gitlab
@dashboard_hash = dashboard_hash @dashboard_hash = dashboard_hash
@project = project @project = project
@dashboard_path = dashboard_path @dashboard_path = dashboard_path
@affected_environment_ids = []
end end
def execute def execute
import import
rescue ActiveRecord::RecordInvalid, ::Gitlab::Metrics::Dashboard::Transformers::TransformerError rescue ActiveRecord::RecordInvalid, Dashboard::Transformers::Errors::BaseError
false false
end end
...@@ -32,28 +33,52 @@ module Gitlab ...@@ -32,28 +33,52 @@ module Gitlab
def import def import
delete_stale_metrics delete_stale_metrics
create_or_update_metrics create_or_update_metrics
update_prometheus_environments
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def create_or_update_metrics def create_or_update_metrics
# TODO: use upsert and worker for callbacks? # TODO: use upsert and worker for callbacks?
affected_metric_ids = []
prometheus_metrics_attributes.each do |attributes| prometheus_metrics_attributes.each do |attributes|
prometheus_metric = PrometheusMetric.find_or_initialize_by(attributes.slice(:identifier, :project)) prometheus_metric = PrometheusMetric.find_or_initialize_by(attributes.slice(:dashboard_path, :identifier, :project))
prometheus_metric.update!(attributes.slice(*ALLOWED_ATTRIBUTES)) prometheus_metric.update!(attributes.slice(*ALLOWED_ATTRIBUTES))
affected_metric_ids << prometheus_metric.id
end end
@affected_environment_ids += find_alerts(affected_metric_ids).pluck(:environment_id)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def delete_stale_metrics def delete_stale_metrics
identifiers = prometheus_metrics_attributes.map { |metric_attributes| metric_attributes[:identifier] } identifiers_from_yml = prometheus_metrics_attributes.map { |metric_attributes| metric_attributes[:identifier] }
stale_metrics = PrometheusMetric.for_project(project) stale_metrics = PrometheusMetric.for_project(project)
.for_dashboard_path(dashboard_path) .for_dashboard_path(dashboard_path)
.for_group(Enums::PrometheusMetric.groups[:custom]) .for_group(Enums::PrometheusMetric.groups[:custom])
.not_identifier(identifiers) .not_identifier(identifiers_from_yml)
return unless stale_metrics.present?
delete_stale_alerts(stale_metrics)
stale_metrics.each_batch { |batch| batch.delete_all }
end
# rubocop: disable CodeReuse/ActiveRecord
def delete_stale_alerts(stale_metrics)
stale_alerts = find_alerts(stale_metrics)
return unless stale_alerts.present?
@affected_environment_ids += stale_alerts.pluck(:environment_id)
stale_alerts.each_batch { |batch| batch.delete_all }
end
# rubocop: enable CodeReuse/ActiveRecord
# TODO: use destroy_all and worker for callbacks? def find_alerts(metrics)
stale_metrics.each(&:destroy) Projects::Prometheus::AlertsFinder.new(project: project, metric: metrics).execute
end end
def prometheus_metrics_attributes def prometheus_metrics_attributes
...@@ -65,6 +90,21 @@ module Gitlab ...@@ -65,6 +90,21 @@ module Gitlab
).execute ).execute
end end
end end
# rubocop: disable CodeReuse/ActiveRecord
def update_prometheus_environments
affected_environments = ::Environment.where(id: @affected_environment_ids.flatten.uniq, project: project)
return unless affected_environments.present?
affected_environments.each do |affected_environment|
::Clusters::Applications::ScheduleUpdateService.new(
affected_environment.cluster_prometheus_adapter,
project
).execute
end
end
# rubocop: enable CodeReuse/ActiveRecord
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module Metrics
module Dashboard
module Stages
# Acts on metrics which have been ingested from source controlled dashboards
class CustomDashboardMetricsInserter < BaseStage
# 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 transform!
database_metrics = ::PrometheusMetricsFinder.new(common: false, group: :custom, project: project).execute
for_metrics do |metric|
metric_record = database_metrics.find { |m| m.identifier == metric[:id] }
metric[:metric_id] = metric_record.id if metric_record
end
end
end
end
end
end
end
...@@ -4,10 +4,10 @@ module Gitlab ...@@ -4,10 +4,10 @@ module Gitlab
module Metrics module Metrics
module Dashboard module Dashboard
module Transformers module Transformers
TransformerError = Class.new(StandardError)
module Errors module Errors
class MissingAttribute < TransformerError BaseError = Class.new(StandardError)
class MissingAttribute < BaseError
def initialize(attribute_name) def initialize(attribute_name)
super("Missing attribute: '#{attribute_name}'") super("Missing attribute: '#{attribute_name}'")
end end
......
...@@ -9,6 +9,7 @@ FactoryBot.define do ...@@ -9,6 +9,7 @@ FactoryBot.define do
group { :business } group { :business }
project project
legend { 'legend' } legend { 'legend' }
dashboard_path { '.gitlab/dashboards/dashboard_path.yml'}
trait :common do trait :common do
common { true } common { true }
......
...@@ -8,9 +8,16 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do ...@@ -8,9 +8,16 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
describe '#execute' do describe '#execute' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:dashboard_path) { 'path/to/dashboard.yml' } let(:dashboard_path) { 'path/to/dashboard.yml' }
let(:prometheus_adapter) { double('adapter', clear_prometheus_reactive_cache!: nil) }
subject { described_class.new(dashboard_hash, project: project, dashboard_path: dashboard_path) } subject { described_class.new(dashboard_hash, project: project, dashboard_path: dashboard_path) }
before do
allow_next_instance_of(::Clusters::Applications::ScheduleUpdateService) do |update_service|
allow(update_service).to receive(:execute)
end
end
context 'valid dashboard' do context 'valid dashboard' do
let(:dashboard_hash) { load_sample_dashboard } let(:dashboard_hash) { load_sample_dashboard }
...@@ -21,20 +28,32 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do ...@@ -21,20 +28,32 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
end end
context 'with existing metrics' do context 'with existing metrics' do
let!(:existing_metric) do let(:existing_metric_attributes) do
create(:prometheus_metric, { {
project: project, project: project,
identifier: 'metric_b', identifier: 'metric_b',
title: 'overwrite', title: 'overwrite',
y_label: 'overwrite', y_label: 'overwrite',
query: 'overwrite', query: 'overwrite',
unit: 'overwrite', unit: 'overwrite',
legend: 'overwrite' legend: 'overwrite',
}) dashboard_path: dashboard_path
}
end
let!(:existing_metric) do
create(:prometheus_metric, existing_metric_attributes)
end
let!(:existing_alert) do
alert = create(:prometheus_alert, project: project, prometheus_metric: existing_metric)
existing_metric.prometheus_alerts << alert
alert
end end
it 'updates existing PrometheusMetrics' do it 'updates existing PrometheusMetrics' do
described_class.new(dashboard_hash, project: project, dashboard_path: dashboard_path).execute subject.execute
expect(existing_metric.reload.attributes.with_indifferent_access).to include({ expect(existing_metric.reload.attributes.with_indifferent_access).to include({
title: 'Super Chart B', title: 'Super Chart B',
...@@ -49,6 +68,15 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do ...@@ -49,6 +68,15 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
expect { subject.execute }.to change { PrometheusMetric.count }.by(2) expect { subject.execute }.to change { PrometheusMetric.count }.by(2)
end end
it 'updates affected environments' do
expect(::Clusters::Applications::ScheduleUpdateService).to receive(:new).with(
existing_alert.environment.cluster_prometheus_adapter,
project
).and_return(double('ScheduleUpdateService', execute: true))
subject.execute
end
context 'with stale metrics' do context 'with stale metrics' do
let!(:stale_metric) do let!(:stale_metric) do
create(:prometheus_metric, create(:prometheus_metric,
...@@ -59,11 +87,45 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do ...@@ -59,11 +87,45 @@ RSpec.describe Gitlab::Metrics::Dashboard::Importers::PrometheusMetrics do
) )
end end
let!(:stale_alert) do
alert = create(:prometheus_alert, project: project, prometheus_metric: stale_metric)
stale_metric.prometheus_alerts << alert
alert
end
it 'updates existing PrometheusMetrics' do
subject.execute
expect(existing_metric.reload.attributes.with_indifferent_access).to include({
title: 'Super Chart B',
y_label: 'y_label',
query: 'query',
unit: 'unit',
legend: 'Legend Label'
})
end
it 'deletes stale metrics' do it 'deletes stale metrics' do
subject.execute subject.execute
expect { stale_metric.reload }.to raise_error(ActiveRecord::RecordNotFound) expect { stale_metric.reload }.to raise_error(ActiveRecord::RecordNotFound)
end end
it 'deletes stale alert' do
subject.execute
expect { stale_alert.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'updates affected environments' do
expect(::Clusters::Applications::ScheduleUpdateService).to receive(:new).with(
existing_alert.environment.cluster_prometheus_adapter,
project
).and_return(double('ScheduleUpdateService', execute: true))
subject.execute
end
end end
end end
end end
......
...@@ -67,6 +67,23 @@ RSpec.describe Metrics::Dashboard::CustomDashboardService, :use_clean_rails_memo ...@@ -67,6 +67,23 @@ RSpec.describe Metrics::Dashboard::CustomDashboardService, :use_clean_rails_memo
.at_least(:once) .at_least(:once)
end end
context 'with metric in database' do
let!(:prometheus_metric) do
create(:prometheus_metric, project: project, identifier: 'metric_a1', group: 'custom')
end
it 'includes metric_id' do
dashboard = described_class.new(*service_params).get_dashboard
metric_id = dashboard[:dashboard][:panel_groups].find { |panel_group| panel_group[:group] == 'Group A' }
.fetch(:panels).find { |panel| panel[:title] == 'Super Chart A1' }
.fetch(:metrics).find { |metric| metric[:id] == 'metric_a1' }
.fetch(:metric_id)
expect(metric_id).to eq(prometheus_metric.id)
end
end
context 'and the dashboard is then deleted' do context 'and the dashboard is then deleted' do
it 'does not return the previously cached dashboard' do it 'does not return the previously cached dashboard' do
described_class.new(*service_params).get_dashboard described_class.new(*service_params).get_dashboard
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Metrics::Dashboard::SyncDashboardsWorker do
subject(:worker) { described_class.new }
let(:project) { create(:project) }
let(:dashboard_paths) { [".gitlab/dashboards/dashboard1.yml", ".gitlab/dashboards/dashboard2.yml"] }
describe ".perform" do
before do
expect(::Gitlab::Metrics::Dashboard::RepoDashboardFinder).to receive(:list_dashboards).with(project)
.and_return(dashboard_paths)
end
it 'calls importer for each dashboard path' do
dashboard_paths.each do |dashboard_path|
expect(::Gitlab::Metrics::Dashboard::Importer).to receive(:new)
.with(dashboard_path, project).and_return(double('importer', execute!: true))
end
worker.perform(project.id)
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