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 ...@@ -150,8 +150,19 @@ module AlertManagement
'' ''
end 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 private
def hook_data
Gitlab::DataBuilder::Alert.build(self)
end
def hosts_length def hosts_length
return unless hosts 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 # frozen_string_literal: true
class SlackService < ChatNotificationService class SlackService < ChatNotificationService
prop_accessor EVENT_CHANNEL['alert']
def title def title
'Slack notifications' 'Slack notifications'
end end
...@@ -21,13 +23,25 @@ class SlackService < ChatNotificationService ...@@ -21,13 +23,25 @@ class SlackService < ChatNotificationService
'https://hooks.slack.com/services/…' 'https://hooks.slack.com/services/…'
end 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 module Notifier
private private
def notify(message, opts) def notify(message, opts)
# See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client # See https://gitlab.com/gitlab-org/slack-notifier/#custom-http-client
notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient)) notifier = Slack::Messenger.new(webhook, opts.merge(http_client: HTTPClient))
notifier.ping( notifier.ping(
message.pretext, message.pretext,
attachments: message.attachments, attachments: message.attachments,
......
...@@ -22,6 +22,7 @@ class Service < ApplicationRecord ...@@ -22,6 +22,7 @@ class Service < ApplicationRecord
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
default_value_for :active, false default_value_for :active, false
default_value_for :alert_events, true
default_value_for :push_events, true default_value_for :push_events, true
default_value_for :issues_events, true default_value_for :issues_events, true
default_value_for :confidential_issues_events, true default_value_for :confidential_issues_events, true
...@@ -72,6 +73,7 @@ class Service < ApplicationRecord ...@@ -72,6 +73,7 @@ class Service < ApplicationRecord
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) } scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_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 :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 :external_issue_trackers, -> { issue_trackers.active.without_defaults }
scope :deployment, -> { where(category: 'deployment') } scope :deployment, -> { where(category: 'deployment') }
...@@ -172,7 +174,7 @@ class Service < ApplicationRecord ...@@ -172,7 +174,7 @@ class Service < ApplicationRecord
end end
def configurable_events def configurable_events
events = self.class.supported_events events = supported_events
# No need to disable individual triggers when there is only one # No need to disable individual triggers when there is only one
if events.count == 1 if events.count == 1
...@@ -403,6 +405,8 @@ class Service < ApplicationRecord ...@@ -403,6 +405,8 @@ class Service < ApplicationRecord
"Event will be triggered when a commit is created/updated" "Event will be triggered when a commit is created/updated"
when "deployment" when "deployment"
"Event will be triggered when a deployment finishes" "Event will be triggered when a deployment finishes"
when "alert"
"Event will be triggered when a new, unique alert is recorded"
end end
end end
......
...@@ -48,7 +48,10 @@ module AlertManagement ...@@ -48,7 +48,10 @@ module AlertManagement
def create_alert_management_alert def create_alert_management_alert
am_alert = AlertManagement::Alert.new(am_alert_params.merge(ended_at: nil)) 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( logger.warn(
message: 'Unable to create AlertManagement::Alert', message: 'Unable to create AlertManagement::Alert',
......
...@@ -32,15 +32,24 @@ module Projects ...@@ -32,15 +32,24 @@ module Projects
end end
def process_alert def process_alert
if alert = find_alert_by_fingerprint(am_alert_params[:fingerprint]) existing_alert = find_alert_by_fingerprint(am_alert_params[:fingerprint])
alert.register_new_event!
if existing_alert
process_existing_alert(existing_alert)
else else
create_alert create_alert
end end
end end
def process_existing_alert(alert)
alert.register_new_event!
end
def create_alert def create_alert
AlertManagement::Alert.create(am_alert_params) alert = AlertManagement::Alert.create(am_alert_params)
alert.execute_services if alert.persisted?
alert
end end
def find_alert_by_fingerprint(fingerprint) 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 ( ...@@ -6147,7 +6147,8 @@ CREATE TABLE public.services (
template boolean DEFAULT false, template boolean DEFAULT false,
instance boolean DEFAULT false NOT NULL, instance boolean DEFAULT false NOT NULL,
comment_detail smallint, comment_detail smallint,
inherit_from_id bigint inherit_from_id bigint,
alert_events boolean
); );
CREATE SEQUENCE public.services_id_seq CREATE SEQUENCE public.services_id_seq
...@@ -13778,6 +13779,7 @@ COPY "schema_migrations" (version) FROM STDIN; ...@@ -13778,6 +13779,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200525114553 20200525114553
20200525121014 20200525121014
20200526000407 20200526000407
20200526013844
20200526120714 20200526120714
20200526153844 20200526153844
20200526164946 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: ...@@ -472,6 +472,7 @@ Service:
- properties - properties
- template - template
- instance - instance
- alert_events
- push_events - push_events
- issues_events - issues_events
- commit_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 ...@@ -114,6 +114,20 @@ describe Service do
expect(described_class.confidential_note_hooks.count).to eq 0 expect(described_class.confidential_note_hooks.count).to eq 0
end end
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 end
describe "Test Button" do describe "Test Button" do
......
...@@ -5,6 +5,10 @@ require 'spec_helper' ...@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe AlertManagement::ProcessPrometheusAlertService do RSpec.describe AlertManagement::ProcessPrometheusAlertService do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
before do
allow(ProjectServiceWorker).to receive(:perform_async)
end
describe '#execute' do describe '#execute' do
subject(:execute) { described_class.new(project, nil, payload).execute } subject(:execute) { described_class.new(project, nil, payload).execute }
...@@ -47,6 +51,12 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do ...@@ -47,6 +51,12 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
end end
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 context 'when status change did not succeed' do
before do before do
allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert]) allow(AlertManagement::Alert).to receive(:for_fingerprint).and_return([alert])
...@@ -72,6 +82,26 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do ...@@ -72,6 +82,26 @@ RSpec.describe AlertManagement::ProcessPrometheusAlertService do
it 'creates a new alert' do it 'creates a new alert' do
expect { execute }.to change { AlertManagement::Alert.where(project: project).count }.by(1) expect { execute }.to change { AlertManagement::Alert.where(project: project).count }.by(1)
end 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 end
context 'when alert cannot be created' do context 'when alert cannot be created' do
......
...@@ -8,12 +8,17 @@ describe Projects::Alerting::NotifyService do ...@@ -8,12 +8,17 @@ describe Projects::Alerting::NotifyService do
before do before do
# We use `let_it_be(:project)` so we make sure to clear caches # We use `let_it_be(:project)` so we make sure to clear caches
project.clear_memoization(:licensed_feature_available) project.clear_memoization(:licensed_feature_available)
allow(ProjectServiceWorker).to receive(:perform_async)
end end
shared_examples 'processes incident issues' do |amount| shared_examples 'processes incident issues' do |amount|
let(:create_incident_service) { spy } let(:create_incident_service) { spy }
let(:new_alert) { instance_double(AlertManagement::Alert, id: 503, persisted?: true) } 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 it 'processes issues' do
expect(AlertManagement::Alert) expect(AlertManagement::Alert)
.to receive(:create) .to receive(:create)
...@@ -138,6 +143,25 @@ describe Projects::Alerting::NotifyService do ...@@ -138,6 +143,25 @@ describe Projects::Alerting::NotifyService do
) )
end 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 context 'existing alert with same fingerprint' do
let!(:existing_alert) { create(:alert_management_alert, project: project, fingerprint: Digest::SHA1.hexdigest(fingerprint)) } let!(:existing_alert) { create(:alert_management_alert, project: project, fingerprint: Digest::SHA1.hexdigest(fingerprint)) }
...@@ -148,6 +172,12 @@ describe Projects::Alerting::NotifyService do ...@@ -148,6 +172,12 @@ describe Projects::Alerting::NotifyService do
it 'increments the existing alert count' do it 'increments the existing alert count' do
expect { subject }.to change { existing_alert.reload.events }.from(1).to(2) expect { subject }.to change { existing_alert.reload.events }.from(1).to(2)
end end
it 'does not executes the alert service hooks' do
subject
expect(ProjectServiceWorker).not_to have_received(:perform_async)
end
end end
context 'with a minimal payload' do 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