Commit 89b3e1dc authored by Peter Leitzen's avatar Peter Leitzen

Merge branch '216326-send-alerts-to-slack-db-settings' into 'master'

Alert Management alerts Slack Notifications

See merge request gitlab-org/gitlab!33017
parents b146cfda a80a29d6
......@@ -150,8 +150,19 @@ module AlertManagement
''
end
def execute_services
return unless Feature.enabled?(:alert_slack_event, project)
return unless project.has_active_services?(:alert_hooks)
project.execute_services(hook_data, :alert_hooks)
end
private
def hook_data
Gitlab::DataBuilder::Alert.build(self)
end
def hosts_length
return unless hosts
......
# frozen_string_literal: true
module ChatMessage
class AlertMessage < BaseMessage
attr_reader :title
attr_reader :alert_url
attr_reader :severity
attr_reader :events
attr_reader :status
attr_reader :started_at
def initialize(params)
@project_name = params[:project_name] || params.dig(:project, :path_with_namespace)
@project_url = params.dig(:project, :web_url) || params[:project_url]
@title = params.dig(:object_attributes, :title)
@alert_url = params.dig(:object_attributes, :url)
@severity = params.dig(:object_attributes, :severity)
@events = params.dig(:object_attributes, :events)
@status = params.dig(:object_attributes, :status)
@started_at = params.dig(:object_attributes, :started_at)
end
def attachments
[{
title: title,
title_link: alert_url,
color: attachment_color,
fields: attachment_fields
}]
end
def message
"Alert firing in #{project_name}"
end
private
def attachment_color
"#C95823"
end
def attachment_fields
[
{
title: "Severity",
value: severity.to_s.humanize,
short: true
},
{
title: "Events",
value: events,
short: true
},
{
title: "Status",
value: status.to_s.humanize,
short: true
},
{
title: "Start time",
value: format_time(started_at),
short: true
}
]
end
# This formats time into the following format
# April 23rd, 2020 1:06AM UTC
def format_time(time)
time = Time.zone.parse(time.to_s)
time.strftime("%B #{time.day.ordinalize}, %Y%l:%M%p %Z")
end
end
end
# frozen_string_literal: true
class SlackService < ChatNotificationService
prop_accessor EVENT_CHANNEL['alert']
def title
'Slack notifications'
end
......@@ -21,13 +23,25 @@ class SlackService < ChatNotificationService
'https://hooks.slack.com/services/…'
end
def supported_events
additional = []
additional << 'alert' if Feature.enabled?(:alert_slack_event, project)
super + additional
end
def get_message(object_kind, data)
return ChatMessage::AlertMessage.new(data) if object_kind == 'alert'
super
end
module Notifier
private
def notify(message, opts)
# See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client
notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
notifier.ping(
message.pretext,
attachments: message.attachments,
......
......@@ -22,6 +22,7 @@ class Service < ApplicationRecord
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
default_value_for :active, false
default_value_for :alert_events, true
default_value_for :push_events, true
default_value_for :issues_events, true
default_value_for :confidential_issues_events, true
......@@ -72,6 +73,7 @@ class Service < ApplicationRecord
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
scope :deployment_hooks, -> { where(deployment_events: true, active: true) }
scope :alert_hooks, -> { where(alert_events: true, active: true) }
scope :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') }
......@@ -172,7 +174,7 @@ class Service < ApplicationRecord
end
def configurable_events
events = self.class.supported_events
events = supported_events
# No need to disable individual triggers when there is only one
if events.count == 1
......@@ -403,6 +405,8 @@ class Service < ApplicationRecord
"Event will be triggered when a commit is created/updated"
when "deployment"
"Event will be triggered when a deployment finishes"
when "alert"
"Event will be triggered when a new, unique alert is recorded"
end
end
......
......@@ -48,7 +48,10 @@ module AlertManagement
def create_alert_management_alert
am_alert = AlertManagement::Alert.new(am_alert_params.merge(ended_at: nil))
return if am_alert.save
if am_alert.save
am_alert.execute_services
return
end
logger.warn(
message: 'Unable to create AlertManagement::Alert',
......
......@@ -32,15 +32,24 @@ module Projects
end
def process_alert
if alert = find_alert_by_fingerprint(am_alert_params[:fingerprint])
alert.register_new_event!
existing_alert = find_alert_by_fingerprint(am_alert_params[:fingerprint])
if existing_alert
process_existing_alert(existing_alert)
else
create_alert
end
end
def process_existing_alert(alert)
alert.register_new_event!
end
def create_alert
AlertManagement::Alert.create(am_alert_params)
alert = AlertManagement::Alert.create(am_alert_params)
alert.execute_services if alert.persisted?
alert
end
def find_alert_by_fingerprint(fingerprint)
......
---
title: Add column for alert slack notifications
merge_request: 33017
author:
type: added
# frozen_string_literal: true
class AddAlertEventsToServices < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
with_lock_retries do
add_column :services, :alert_events, :boolean
end
end
def down
with_lock_retries do
remove_column :services, :alert_events
end
end
end
......@@ -6147,7 +6147,8 @@ CREATE TABLE public.services (
template boolean DEFAULT false,
instance boolean DEFAULT false NOT NULL,
comment_detail smallint,
inherit_from_id bigint
inherit_from_id bigint,
alert_events boolean
);
CREATE SEQUENCE public.services_id_seq
......@@ -13778,6 +13779,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200525114553
20200525121014
20200526000407
20200526013844
20200526120714
20200526153844
20200526164946
......
# frozen_string_literal: true
module Gitlab
module DataBuilder
module Alert
extend self
def build(alert)
{
object_kind: 'alert',
object_attributes: hook_attrs(alert)
}
end
def hook_attrs(alert)
{
title: alert.title,
url: Gitlab::Routing.url_helpers.details_project_alert_management_url(alert.project, alert.iid),
severity: alert.severity,
events: alert.events,
status: alert.status_name,
started_at: alert.started_at
}
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::DataBuilder::Alert do
let_it_be(:project) { create(:project) }
let_it_be(:alert) { create(:alert_management_alert, project: project) }
describe '.build' do
let_it_be(:data) { described_class.build(alert) }
it { expect(data).to be_a(Hash) }
it { expect(data[:object_kind]).to eq('alert') }
it 'contains the correct object attributes', :aggregate_failures do
object_attributes = data[:object_attributes]
expect(object_attributes[:title]).to eq(alert.title)
expect(object_attributes[:url]).to eq(Gitlab::Routing.url_helpers.details_project_alert_management_url(project, alert.iid))
expect(object_attributes[:severity]).to eq(alert.severity)
expect(object_attributes[:events]).to eq(alert.events)
expect(object_attributes[:status]).to eq(alert.status_name)
expect(object_attributes[:started_at]).to eq(alert.started_at)
end
end
end
......@@ -472,6 +472,7 @@ Service:
- properties
- template
- instance
- alert_events
- push_events
- issues_events
- commit_events
......
# frozen_string_literal: true
require 'spec_helper'
describe ChatMessage::AlertMessage do
subject { described_class.new(args) }
let(:alert) { create(:alert_management_alert) }
let(:args) do
{
project_name: 'project_name',
project_url: 'http://example.com'
}.merge(Gitlab::DataBuilder::Alert.build(alert))
end
describe '#message' do
it 'returns the correct message' do
expect(subject.message).to eq("Alert firing in #{args[:project_name]}")
end
end
describe '#attachments' do
it 'returns an array of one' do
expect(subject.attachments).to be_a(Array)
expect(subject.attachments.size).to eq(1)
end
it 'contains the correct attributes' do
attachments_item = subject.attachments.first
expect(attachments_item).to have_key(:title)
expect(attachments_item).to have_key(:title_link)
expect(attachments_item).to have_key(:color)
expect(attachments_item).to have_key(:fields)
end
it 'returns the correct color' do
expect(subject.attachments.first[:color]).to eq("#C95823")
end
it 'returns the correct attachment fields' do
attachments_item = subject.attachments.first
fields = attachments_item[:fields].map { |h| h[:title] }
expect(fields).to match_array(['Severity', 'Events', 'Status', 'Start time'])
end
end
end
......@@ -114,6 +114,20 @@ describe Service do
expect(described_class.confidential_note_hooks.count).to eq 0
end
end
describe '.alert_hooks' do
it 'includes services where alert_events is true' do
create(:service, active: true, alert_events: true)
expect(described_class.alert_hooks.count).to eq 1
end
it 'excludes services where alert_events is false' do
create(:service, active: true, alert_events: false)
expect(described_class.alert_hooks.count).to eq 0
end
end
end
describe "Test Button" do
......
......@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe AlertManagement::ProcessPrometheusAlertService do
let_it_be(:project) { create(:project) }
before do
allow(ProjectServiceWorker).to receive(:perform_async)
end
describe '#execute' do
subject(:execute) { described_class.new(project, nil, payload).execute }
......@@ -47,6 +51,12 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
end
end
it 'does not executes the alert service hooks' do
expect(alert).not_to receive(:execute_services)
subject
end
context 'when status change did not succeed' do
before do
allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert])
......@@ -72,6 +82,26 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
it 'creates a new alert' do
expect { execute }.to change { AlertManagement::Alert.where(project: project).count }.by(1)
end
it 'executes the alert service hooks' do
slack_service = create(:service, type: 'SlackService', project: project, alert_events: true, active: true)
subject
expect(ProjectServiceWorker).to have_received(:perform_async).with(slack_service.id, an_instance_of(Hash))
end
context 'feature flag disabled' do
before do
stub_feature_flags(alert_slack_event: false)
end
it 'does not execute the alert service hooks' do
subject
expect(ProjectServiceWorker).not_to have_received(:perform_async)
end
end
end
context 'when alert cannot be created' do
......
......@@ -8,12 +8,17 @@ describe Projects::Alerting::NotifyService do
before do
# We use `let_it_be(:project)` so we make sure to clear caches
project.clear_memoization(:licensed_feature_available)
allow(ProjectServiceWorker).to receive(:perform_async)
end
shared_examples 'processes incident issues' do |amount|
let(:create_incident_service) { spy }
let(:new_alert) { instance_double(AlertManagement::Alert, id: 503, persisted?: true) }
before do
allow(new_alert).to receive(:execute_services)
end
it 'processes issues' do
expect(AlertManagement::Alert)
.to receive(:create)
......@@ -138,6 +143,25 @@ describe Projects::Alerting::NotifyService do
)
end
it 'executes the alert service hooks' do
slack_service = create(:service, type: 'SlackService', project: project, alert_events: true, active: true)
subject
expect(ProjectServiceWorker).to have_received(:perform_async).with(slack_service.id, an_instance_of(Hash))
end
context 'feature flag disabled' do
before do
stub_feature_flags(alert_slack_event: false)
end
it 'does not executes the alert service hooks' do
subject
expect(ProjectServiceWorker).not_to have_received(:perform_async)
end
end
context 'existing alert with same fingerprint' do
let!(:existing_alert) { create(:alert_management_alert, project: project, fingerprint: Digest::SHA1.hexdigest(fingerprint)) }
......@@ -148,6 +172,12 @@ describe Projects::Alerting::NotifyService do
it 'increments the existing alert count' do
expect { subject }.to change { existing_alert.reload.events }.from(1).to(2)
end
it 'does not executes the alert service hooks' do
subject
expect(ProjectServiceWorker).not_to have_received(:perform_async)
end
end
context 'with a minimal payload' do
......
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