Commit f3ea9ae3 authored by Peter Leitzen's avatar Peter Leitzen Committed by Nick Thomas

Implement finder for Prometheus alerts

Don't query for Prometheus alerts directly from project or environment.
parent 353a1ce9
...@@ -16,8 +16,6 @@ module Projects ...@@ -16,8 +16,6 @@ module Projects
before_action :alert, only: [:update, :show, :destroy] before_action :alert, only: [:update, :show, :destroy]
def index def index
alerts = prometheus_alerts.order_by('id_asc')
render json: serialize_as_json(alerts) render json: serialize_as_json(alerts)
end end
...@@ -27,10 +25,8 @@ module Projects ...@@ -27,10 +25,8 @@ module Projects
def notify def notify
token = extract_alert_manager_token(request) token = extract_alert_manager_token(request)
notify = Projects::Prometheus::Alerts::NotifyService
.new(project, current_user, params.permit!)
if notify.execute(token) if notify_service.execute(token)
head :ok head :ok
else else
head :unprocessable_entity head :unprocessable_entity
...@@ -38,7 +34,7 @@ module Projects ...@@ -38,7 +34,7 @@ module Projects
end end
def create def create
@alert = prometheus_alerts.create(alerts_params) @alert = create_service.execute
if @alert.persisted? if @alert.persisted?
schedule_prometheus_update! schedule_prometheus_update!
...@@ -50,7 +46,7 @@ module Projects ...@@ -50,7 +46,7 @@ module Projects
end end
def update def update
if alert.update(alerts_params) if update_service.execute(alert)
schedule_prometheus_update! schedule_prometheus_update!
render json: serialize_as_json(alert) render json: serialize_as_json(alert)
...@@ -60,7 +56,7 @@ module Projects ...@@ -60,7 +56,7 @@ module Projects
end end
def destroy def destroy
if alert.destroy if destroy_service.execute(alert)
schedule_prometheus_update! schedule_prometheus_update!
head :ok head :ok
...@@ -72,13 +68,27 @@ module Projects ...@@ -72,13 +68,27 @@ module Projects
private private
def alerts_params def alerts_params
alerts_params = params.permit(:operator, :threshold, :environment_id, :prometheus_metric_id) params.permit(:operator, :threshold, :environment_id, :prometheus_metric_id)
end
if alerts_params[:operator].present? def notify_service
alerts_params[:operator] = PrometheusAlert.operator_to_enum(alerts_params[:operator]) Projects::Prometheus::Alerts::NotifyService
end .new(project, current_user, params.permit!)
end
def create_service
Projects::Prometheus::Alerts::CreateService
.new(project, current_user, alerts_params)
end
def update_service
Projects::Prometheus::Alerts::UpdateService
.new(project, current_user, alerts_params)
end
alerts_params def destroy_service
Projects::Prometheus::Alerts::DestroyService
.new(project, current_user, nil)
end end
def schedule_prometheus_update! def schedule_prometheus_update!
...@@ -90,11 +100,23 @@ module Projects ...@@ -90,11 +100,23 @@ module Projects
end end
def serializer def serializer
PrometheusAlertSerializer.new(project: project, current_user: current_user) PrometheusAlertSerializer
.new(project: project, current_user: current_user)
end
def alerts
alerts_finder.execute
end end
def alert def alert
@alert ||= prometheus_alerts.for_metric(params[:id]).first || render_404 @alert ||= alerts_finder(metric: params[:id]).execute.first || render_404
end
def alerts_finder(opts = {})
Projects::Prometheus::AlertsFinder.new({
project: project,
environment: params[:environment_id]
}.reverse_merge(opts))
end end
def application def application
......
# frozen_string_literal: true
module Projects
module Prometheus
# Find Prometheus alerts by +project+, by +environment+, or both.
#
# Optionally filter by +metric+.
#
# Arguments:
# params:
# project: Project | integer
# environment: Environment | integer
# metric: PrometheusMetric | integer
class AlertsFinder
def initialize(params = {})
unless params[:project] || params[:environment]
raise ArgumentError,
'Please provide either :project or :environment, or both'
end
@params = params
end
# Find all matching alerts
#
# @return [ActiveRecord::Relation<PrometheusAlert>]
def execute
relation = by_project(PrometheusAlert)
relation = by_environment(relation)
relation = by_metric(relation)
relation = ordered(relation)
relation
end
private
attr_reader :params
def by_project(relation)
return relation unless params[:project]
relation.for_project(params[:project])
end
def by_environment(relation)
return relation unless params[:environment]
relation.for_environment(params[:environment])
end
def by_metric(relation)
return relation unless params[:metric]
relation.for_metric(params[:metric])
end
def ordered(relation)
relation.order_by('id_asc')
end
end
end
end
...@@ -26,6 +26,7 @@ class PrometheusAlert < ActiveRecord::Base ...@@ -26,6 +26,7 @@ class PrometheusAlert < ActiveRecord::Base
delegate :title, :query, to: :prometheus_metric delegate :title, :query, to: :prometheus_metric
scope :for_metric, -> (metric) { where(prometheus_metric: metric) } scope :for_metric, -> (metric) { where(prometheus_metric: metric) }
scope :for_project, -> (project) { where(project_id: project) }
scope :for_environment, -> (environment) { where(environment_id: environment) } scope :for_environment, -> (environment) { where(environment_id: environment) }
def self.distinct_projects def self.distinct_projects
......
...@@ -129,8 +129,11 @@ module Clusters ...@@ -129,8 +129,11 @@ module Clusters
def alerts(environment) def alerts(environment)
variables = Gitlab::Prometheus::QueryVariables.call(environment) variables = Gitlab::Prometheus::QueryVariables.call(environment)
alerts = Projects::Prometheus::AlertsFinder
.new(environment: environment)
.execute
environment.prometheus_alerts.map do |alert| alerts.map do |alert|
substitute_query_variables(alert.to_param, variables) substitute_query_variables(alert.to_param, variables)
end end
end end
......
# frozen_string_literal: true
module Projects
module Prometheus
module Alerts
module AlertParams
def alert_params
return params if params[:operator].blank?
params.merge(
operator: PrometheusAlert.operator_to_enum(params[:operator])
)
end
end
end
end
end
...@@ -30,7 +30,7 @@ module Projects ...@@ -30,7 +30,7 @@ module Projects
gitlab_alert_id = payload.dig('labels', 'gitlab_alert_id') gitlab_alert_id = payload.dig('labels', 'gitlab_alert_id')
return unless gitlab_alert_id return unless gitlab_alert_id
alert = project.prometheus_alerts.for_metric(gitlab_alert_id).first alert = find_alert(gitlab_alert_id)
return unless alert return unless alert
payload_key = PrometheusAlertEvent.payload_key_for(gitlab_alert_id, started_at) payload_key = PrometheusAlertEvent.payload_key_for(gitlab_alert_id, started_at)
...@@ -50,6 +50,13 @@ module Projects ...@@ -50,6 +50,13 @@ module Projects
params['alerts'] params['alerts']
end end
def find_alert(metric)
Projects::Prometheus::AlertsFinder
.new(project: project, metric: metric)
.execute
.first
end
def validate_date(date) def validate_date(date)
return unless date return unless date
......
# frozen_string_literal: true
module Projects
module Prometheus
module Alerts
class CreateService < BaseService
include AlertParams
def execute
project.prometheus_alerts.create(alert_params)
end
end
end
end
end
# frozen_string_literal: true
module Projects
module Prometheus
module Alerts
class DestroyService < BaseService
def execute(alert)
alert.destroy
end
end
end
end
end
...@@ -62,7 +62,7 @@ module Projects ...@@ -62,7 +62,7 @@ module Projects
alert_id = gitlab_alert_id alert_id = gitlab_alert_id
return unless alert_id return unless alert_id
alert = project.prometheus_alerts.for_metric(alert_id).first alert = find_alert(project, alert_id)
return unless alert return unless alert
cluster = alert.environment.deployment_platform&.cluster cluster = alert.environment.deployment_platform&.cluster
...@@ -72,6 +72,13 @@ module Projects ...@@ -72,6 +72,13 @@ module Projects
cluster.application_prometheus cluster.application_prometheus
end end
def find_alert(project, metric)
Projects::Prometheus::AlertsFinder
.new(project: project, metric: metric)
.execute
.first
end
def gitlab_alert_id def gitlab_alert_id
alerts&.first&.dig('labels', 'gitlab_alert_id') alerts&.first&.dig('labels', 'gitlab_alert_id')
end end
......
# frozen_string_literal: true
module Projects
module Prometheus
module Alerts
class UpdateService < BaseService
include AlertParams
def execute(alert)
alert.update(alert_params)
end
end
end
end
end
...@@ -29,9 +29,14 @@ module Projects ...@@ -29,9 +29,14 @@ module Projects
end end
def alert def alert
strong_memoize(:alert) do strong_memoize(:alert) { find_alert(metric) }
metric.prometheus_alerts.find_by(project: project) # rubocop: disable CodeReuse/ActiveRecord end
end
def find_alert(metric)
Projects::Prometheus::AlertsFinder
.new(project: project, metric: metric)
.execute
.first
end end
def has_alert? def has_alert?
......
...@@ -13,7 +13,7 @@ module EE ...@@ -13,7 +13,7 @@ module EE
def query_with_alert(project, environment) def query_with_alert(project, environment)
alerts_map = alerts_map =
environment.prometheus_alerts.each_with_object({}) do |alert, hsh| alerts(project, environment).each_with_object({}) do |alert, hsh|
hsh[alert[:prometheus_metric_id]] = alert.prometheus_metric_id hsh[alert[:prometheus_metric_id]] = alert.prometheus_metric_id
end end
...@@ -38,6 +38,12 @@ module EE ...@@ -38,6 +38,12 @@ module EE
private private
def alerts(project, environment)
::Projects::Prometheus::AlertsFinder
.new(project: project, environment: environment)
.execute
end
def alert_path(alerts_map, key, project, environment) def alert_path(alerts_map, key, project, environment)
::Gitlab::Routing.url_helpers.project_prometheus_alert_path(project, alerts_map[key], environment_id: environment.id, format: :json) ::Gitlab::Routing.url_helpers.project_prometheus_alert_path(project, alerts_map[key], environment_id: environment.id, format: :json)
end end
......
...@@ -45,7 +45,10 @@ module Gitlab ...@@ -45,7 +45,10 @@ module Gitlab
metric_id = payload&.dig('labels', 'gitlab_alert_id') metric_id = payload&.dig('labels', 'gitlab_alert_id')
return unless metric_id return unless metric_id
project.prometheus_alerts.for_metric(metric_id).first Projects::Prometheus::AlertsFinder
.new(project: project, metric: metric_id)
.execute
.first
end end
def parse_title_from_payload def parse_title_from_payload
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Prometheus::AlertsFinder do
let(:finder) { described_class.new(params) }
let(:params) { {} }
describe 'with params' do
set(:project) { create(:project) }
set(:other_project) { create(:project) }
set(:other_env) { create(:environment, project: other_project) }
set(:production) { create(:environment, project: project) }
set(:staging) { create(:environment, project: project) }
set(:alert) { create_alert(project, production) }
set(:alert2) { create_alert(project, production) }
set(:stg_alert) { create_alert(project, staging) }
set(:other_alert) { create_alert(other_project, other_env) }
describe '#execute' do
subject { finder.execute }
context 'with project' do
before do
params[:project] = project
end
it { is_expected.to eq([alert, alert2, stg_alert]) }
context 'with matching metric' do
before do
params[:metric] = alert.prometheus_metric
end
it { is_expected.to eq([alert]) }
end
context 'with matching metric id' do
before do
params[:metric] = alert.prometheus_metric_id
end
it { is_expected.to eq([alert]) }
end
context 'with project non-specific metric' do
before do
params[:metric] = other_alert.prometheus_metric
end
it { is_expected.to be_empty }
end
end
context 'with environment' do
before do
params[:environment] = production
end
it { is_expected.to eq([alert, alert2]) }
context 'with matching metric' do
before do
params[:metric] = alert.prometheus_metric
end
it { is_expected.to eq([alert]) }
end
context 'with environment non-specific metric' do
before do
params[:metric] = stg_alert.prometheus_metric
end
it { is_expected.to be_empty }
end
end
context 'with matching project and environment' do
before do
params[:project] = project
params[:environment] = production
end
it { is_expected.to eq([alert, alert2]) }
context 'with matching metric' do
before do
params[:metric] = alert.prometheus_metric
end
it { is_expected.to eq([alert]) }
end
context 'with environment non-specific metric' do
before do
params[:metric] = stg_alert.prometheus_metric
end
it { is_expected.to be_empty }
end
end
context 'with non-matching project-environment pair' do
before do
params[:project] = project
params[:environment] = other_env
end
it { is_expected.to be_empty }
end
end
private
def create_alert(project, environment)
create(:prometheus_alert, project: project, environment: environment)
end
end
describe 'without params' do
subject { finder }
it 'raises an error' do
expect { subject }
.to raise_error(ArgumentError, 'Please provide either :project or :environment, or both')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Prometheus::Alerts::CreateService do
set(:project) { create(:project) }
set(:user) { create(:user) }
let(:service) { described_class.new(project, user, params) }
subject { service.execute }
describe '#execute' do
context 'with params' do
set(:environment) { create(:environment, project: project) }
set(:metric) do
create(:prometheus_metric, project: project)
end
let(:params) do
{
environment_id: environment.id,
prometheus_metric_id: metric.id,
operator: '<',
threshold: 1.0
}
end
it 'creates an alert' do
expect(subject).to be_persisted
expect(subject).to have_attributes(
project: project,
environment: environment,
prometheus_metric: metric,
operator: 'lt',
threshold: 1.0
)
end
end
context 'without params' do
let(:params) { {} }
it 'fails to create' do
expect(subject).to be_new_record
expect(subject).to be_invalid
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Prometheus::Alerts::DestroyService do
set(:project) { create(:project) }
set(:user) { create(:user) }
set(:alert) { create(:prometheus_alert, project: project) }
let(:service) { described_class.new(project, user, nil) }
describe '#execute' do
subject { service.execute(alert) }
it 'deletes the alert' do
expect(subject).to be_truthy
expect(alert).to be_destroyed
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Prometheus::Alerts::UpdateService do
set(:project) { create(:project) }
set(:user) { create(:user) }
set(:environment) { create(:environment, project: project) }
set(:alert) do
create(:prometheus_alert, project: project, environment: environment)
end
let(:service) { described_class.new(project, user, params) }
let(:params) do
{
environment_id: alert.environment_id,
prometheus_metric_id: alert.prometheus_metric_id,
operator: '=',
threshold: 2.0
}
end
describe '#execute' do
subject { service.execute(alert) }
context 'with valid params' do
it 'updates the alert' do
expect(subject).to be_truthy
expect(alert.reload).to have_attributes(
operator: 'eq',
threshold: 2.0
)
end
end
context 'with invalid params' do
let(:other_environment) { create(:environment) }
before do
params[:environment_id] = other_environment.id
end
it 'fails to update' do
expect(subject).to be_falsey
expect(alert).to be_invalid
end
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