Commit f29726c5 authored by Vitali Tatarintev's avatar Vitali Tatarintev

Merge branch 'sy-condense-alert-email-presenter' into 'master'

Remove Alerting::Alert and AlertManagement::AlertParams

See merge request gitlab-org/gitlab!41220
parents 55e69f81 b315c266
...@@ -56,16 +56,14 @@ module Emails ...@@ -56,16 +56,14 @@ module Emails
subject: @message.subject) subject: @message.subject)
end end
def prometheus_alert_fired_email(project_id, user_id, alert_payload) def prometheus_alert_fired_email(project_id, user_id, alert_attributes)
@project = ::Project.find(project_id) @project = ::Project.find(project_id)
user = ::User.find(user_id) user = ::User.find(user_id)
@alert = ::Gitlab::Alerting::Alert @alert = AlertManagement::Alert.new(alert_attributes.with_indifferent_access).present
.new(project: @project, payload: alert_payload) return unless @alert.parsed_payload.has_required_attributes?
.present
return unless @alert.valid?
subject_text = "Alert: #{@alert.full_title}" subject_text = "Alert: #{@alert.email_title}"
mail(to: user.notification_email_for(@project.group), subject: subject(subject_text)) mail(to: user.notification_email_for(@project.group), subject: subject(subject_text))
end end
end end
......
...@@ -191,7 +191,7 @@ module AlertManagement ...@@ -191,7 +191,7 @@ module AlertManagement
end end
def prometheus? def prometheus?
monitoring_tool == Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus] monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end end
def register_new_event! def register_new_event!
......
...@@ -8,6 +8,7 @@ module AlertManagement ...@@ -8,6 +8,7 @@ module AlertManagement
MARKDOWN_LINE_BREAK = " \n" MARKDOWN_LINE_BREAK = " \n"
HORIZONTAL_LINE = "\n\n---\n\n" HORIZONTAL_LINE = "\n\n---\n\n"
INCIDENT_LABEL_NAME = ::IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title]
delegate :metrics_dashboard_url, :runbook, to: :parsed_payload delegate :metrics_dashboard_url, :runbook, to: :parsed_payload
...@@ -38,6 +39,30 @@ module AlertManagement ...@@ -38,6 +39,30 @@ module AlertManagement
Gitlab::Utils::InlineHash.merge_keys(payload) Gitlab::Utils::InlineHash.merge_keys(payload)
end end
def show_incident_issues_link?
project.incident_management_setting&.create_issue?
end
def show_performance_dashboard_link?
prometheus_alert.present?
end
def incident_issues_link
project_issues_url(project, label_name: INCIDENT_LABEL_NAME)
end
def performance_dashboard_link
if environment
metrics_project_environment_url(project, environment)
else
metrics_project_environments_url(project)
end
end
def email_title
[environment&.name, query_title].compact.join(': ')
end
private private
attr_reader :alert, :project attr_reader :alert, :project
...@@ -80,5 +105,11 @@ module AlertManagement ...@@ -80,5 +105,11 @@ module AlertManagement
def host_links def host_links
hosts.join(' ') hosts.join(' ')
end end
def query_title
return title unless prometheus_alert
"#{prometheus_alert.title} #{prometheus_alert.computed_operator} #{prometheus_alert.threshold} for 5 minutes"
end
end end
end end
# frozen_string_literal: true
module Projects
module Prometheus
class AlertPresenter < Gitlab::View::Presenter::Delegated
GENERIC_ALERT_SUMMARY_ANNOTATIONS = %w(monitoring_tool service hosts).freeze
MARKDOWN_LINE_BREAK = " \n".freeze
INCIDENT_LABEL_NAME = ::IncidentManagement::CreateIncidentLabelService::LABEL_PROPERTIES[:title].freeze
METRIC_TIME_WINDOW = 30.minutes
def full_title
[environment_name, alert_title].compact.join(': ')
end
def project_full_path
project.full_path
end
def metric_query
gitlab_alert&.full_query
end
def environment_name
environment&.name
end
def performance_dashboard_link
if environment
metrics_project_environment_url(project, environment)
else
metrics_project_environments_url(project)
end
end
def show_performance_dashboard_link?
gitlab_alert.present?
end
def show_incident_issues_link?
project.incident_management_setting&.create_issue?
end
def incident_issues_link
project_issues_url(project, label_name: INCIDENT_LABEL_NAME)
end
def start_time
starts_at&.strftime('%d %B %Y, %-l:%M%p (%Z)')
end
def issue_summary_markdown
<<~MARKDOWN.chomp
#{metadata_list}
#{metric_embed_for_alert}
MARKDOWN
end
def metric_embed_for_alert
"\n[](#{metrics_dashboard_url})" if metrics_dashboard_url
end
def metrics_dashboard_url
strong_memoize(:metrics_dashboard_url) do
embed_url_for_gitlab_alert || embed_url_for_self_managed_alert
end
end
def details_url
return unless am_alert
::Gitlab::Routing.url_helpers.details_project_alert_management_url(
project,
am_alert.iid
)
end
private
def alert_title
query_title || title
end
def query_title
return unless gitlab_alert
"#{gitlab_alert.title} #{gitlab_alert.computed_operator} #{gitlab_alert.threshold} for 5 minutes"
end
def metadata_list
metadata = []
metadata << list_item('Start time', start_time) if start_time
metadata << list_item('full_query', backtick(full_query)) if full_query
metadata << list_item(service.label.humanize, service.value) if service
metadata << list_item(monitoring_tool.label.humanize, monitoring_tool.value) if monitoring_tool
metadata << list_item(hosts.label.humanize, host_links) if hosts
metadata << list_item('GitLab alert', details_url) if details_url
metadata.join(MARKDOWN_LINE_BREAK)
end
def details
Gitlab::Utils::InlineHash.merge_keys(payload)
end
def list_item(key, value)
"**#{key}:** #{value}".strip
end
def backtick(value)
"`#{value}`"
end
GENERIC_ALERT_SUMMARY_ANNOTATIONS.each do |annotation_name|
define_method(annotation_name) do
annotations.find { |a| a.label == annotation_name }
end
end
def host_links
Array(hosts.value).join(' ')
end
def embed_url_for_gitlab_alert
return unless gitlab_alert
metrics_dashboard_project_prometheus_alert_url(
project,
gitlab_alert.prometheus_metric_id,
environment_id: environment.id,
embedded: true,
**alert_embed_window_params(embed_time)
)
end
def embed_url_for_self_managed_alert
return unless environment && full_query && title
metrics_dashboard_project_environment_url(
project,
environment,
embed_json: dashboard_for_self_managed_alert.to_json,
embedded: true,
**alert_embed_window_params(embed_time)
)
end
def embed_time
starts_at || Time.current
end
def alert_embed_window_params(time)
{
start: format_embed_timestamp(time - METRIC_TIME_WINDOW),
end: format_embed_timestamp(time + METRIC_TIME_WINDOW)
}
end
def format_embed_timestamp(time)
time.utc.strftime('%FT%TZ')
end
def dashboard_for_self_managed_alert
{
panel_groups: [{
panels: [{
type: 'area-chart',
title: title,
y_label: y_label,
metrics: [{
query_range: full_query
}]
}]
}]
}
end
end
end
end
...@@ -47,7 +47,7 @@ module AlertManagement ...@@ -47,7 +47,7 @@ module AlertManagement
def create_alert_management_alert def create_alert_management_alert
if alert.save if alert.save
alert.execute_services alert.execute_services
SystemNoteService.create_new_alert(alert, Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus]) SystemNoteService.create_new_alert(alert, Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus])
return return
end end
......
...@@ -7,43 +7,34 @@ module Projects ...@@ -7,43 +7,34 @@ module Projects
include ::IncidentManagement::Settings include ::IncidentManagement::Settings
def execute(token) def execute(token)
return bad_request unless valid_payload_size?
return forbidden unless alerts_service_activated? return forbidden unless alerts_service_activated?
return unauthorized unless valid_token?(token) return unauthorized unless valid_token?(token)
alert = process_alert process_alert
return bad_request unless alert.persisted? return bad_request unless alert.persisted?
process_incident_issues(alert) if process_issues? process_incident_issues if process_issues?
send_alert_email if send_email? send_alert_email if send_email?
ServiceResponse.success ServiceResponse.success
rescue Gitlab::Alerting::NotificationPayloadParser::BadPayloadError
bad_request
end end
private private
delegate :alerts_service, :alerts_service_activated?, to: :project delegate :alerts_service, :alerts_service_activated?, to: :project
def am_alert_params
strong_memoize(:am_alert_params) do
Gitlab::AlertManagement::AlertParams.from_generic_alert(project: project, payload: params.to_h)
end
end
def process_alert def process_alert
existing_alert = find_alert_by_fingerprint(am_alert_params[:fingerprint]) if alert.persisted?
process_existing_alert
if existing_alert
process_existing_alert(existing_alert)
else else
create_alert create_alert
end end
end end
def process_existing_alert(alert) def process_existing_alert
if am_alert_params[:ended_at].present? if incoming_payload.ends_at.present?
process_resolved_alert(alert) process_resolved_alert
else else
alert.register_new_event! alert.register_new_event!
end end
...@@ -51,10 +42,10 @@ module Projects ...@@ -51,10 +42,10 @@ module Projects
alert alert
end end
def process_resolved_alert(alert) def process_resolved_alert
return unless auto_close_incident? return unless auto_close_incident?
if alert.resolve(am_alert_params[:ended_at]) if alert.resolve(incoming_payload.ends_at)
close_issue(alert.issue) close_issue(alert.issue)
end end
...@@ -72,20 +63,13 @@ module Projects ...@@ -72,20 +63,13 @@ module Projects
end end
def create_alert def create_alert
alert = AlertManagement::Alert.create(am_alert_params.except(:ended_at)) return unless alert.save
alert.execute_services if alert.persisted?
SystemNoteService.create_new_alert(alert, 'Generic Alert Endpoint')
alert alert.execute_services
end SystemNoteService.create_new_alert(alert, 'Generic Alert Endpoint')
def find_alert_by_fingerprint(fingerprint)
return unless fingerprint
AlertManagement::Alert.not_resolved.for_fingerprint(project, fingerprint).first
end end
def process_incident_issues(alert) def process_incident_issues
return if alert.issue return if alert.issue
::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id) ::IncidentManagement::ProcessAlertWorker.perform_async(nil, nil, alert.id)
...@@ -94,11 +78,33 @@ module Projects ...@@ -94,11 +78,33 @@ module Projects
def send_alert_email def send_alert_email
notification_service notification_service
.async .async
.prometheus_alerts_fired(project, [parsed_payload]) .prometheus_alerts_fired(project, [alert.attributes])
end
def alert
strong_memoize(:alert) do
existing_alert || new_alert
end
end
def existing_alert
return unless incoming_payload.gitlab_fingerprint
AlertManagement::Alert.not_resolved.for_fingerprint(project, incoming_payload.gitlab_fingerprint).first
end
def new_alert
AlertManagement::Alert.new(**incoming_payload.alert_params, ended_at: nil)
end
def incoming_payload
strong_memoize(:incoming_payload) do
Gitlab::AlertManagement::Payload.parse(project, params.to_h)
end
end end
def parsed_payload def valid_payload_size?
Gitlab::Alerting::NotificationPayloadParser.call(params.to_h, project) Gitlab::Utils::DeepSize.new(params).valid?
end end
def valid_token?(token) def valid_token?(token)
......
...@@ -125,7 +125,7 @@ module Projects ...@@ -125,7 +125,7 @@ module Projects
notification_service notification_service
.async .async
.prometheus_alerts_fired(project, firings) .prometheus_alerts_fired(project, alerts_attributes)
end end
def process_prometheus_alerts def process_prometheus_alerts
...@@ -136,6 +136,18 @@ module Projects ...@@ -136,6 +136,18 @@ module Projects
end end
end end
def alerts_attributes
firings.map do |payload|
alert_params = Gitlab::AlertManagement::Payload.parse(
project,
payload,
monitoring_tool: Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
).alert_params
AlertManagement::Alert.new(alert_params).attributes
end
end
def bad_request def bad_request
ServiceResponse.error(message: 'Bad Request', http_status: :bad_request) ServiceResponse.error(message: 'Bad Request', http_status: :bad_request)
end end
......
%p %p
= _('An alert has been triggered in %{project_path}.') % { project_path: @alert.project_full_path } = _('An alert has been triggered in %{project_path}.') % { project_path: @alert.project.full_path }
- if description = @alert.description - if description = @alert.description
%p %p
= _('Description:') = _('Description:')
= description = description
- if env_name = @alert.environment_name - if env_name = @alert.environment&.name
%p %p
= _('Environment:') = _('Environment:')
= env_name = env_name
- if metric_query = @alert.metric_query - if metric_query = @alert.prometheus_alert&.full_query
%p %p
= _('Metric:') = _('Metric:')
...@@ -25,4 +25,3 @@ ...@@ -25,4 +25,3 @@
- if @alert.show_performance_dashboard_link? - if @alert.show_performance_dashboard_link?
%p %p
= link_to(_('View performance dashboard.'), @alert.performance_dashboard_link) = link_to(_('View performance dashboard.'), @alert.performance_dashboard_link)
<%= _('An alert has been triggered in %{project_path}.') % { project_path: @alert.project_full_path } %>. <%= _('An alert has been triggered in %{project_path}.') % { project_path: @alert.project.full_path } %>.
<% if description = @alert.description %> <% if description = @alert.description %>
<%= _('Description:') %> <%= description %> <%= _('Description:') %> <%= description %>
<% end %> <% end %>
<% if env_name = @alert.environment_name %> <% if env_name = @alert.environment&.name %>
<%= _('Environment:') %> <%= env_name %> <%= _('Environment:') %> <%= env_name %>
<% end %> <% end %>
<% if metric_query = @alert.metric_query %> <% if metric_query = @alert.prometheus_alert&.full_query %>
<%= _('Metric:') %> <%= metric_query %> <%= _('Metric:') %> <%= metric_query %>
<% end %> <% end %>
......
# frozen_string_literal: true
# Attribute mapping for alerts via generic alerting integration.
module EE
module Gitlab
module AlertManagement
module Payload
module Generic
extend ::Gitlab::Utils::Override
EXCLUDED_PAYLOAD_FINGERPRINT_PARAMS = %w(start_time end_time hosts).freeze
private
# Currently we use full payloads, when generating a fingerprint.
# This results in a quite strict fingerprint.
# Over time we can relax these rules.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/214557#note_362795447
override :plain_gitlab_fingerprint
def plain_gitlab_fingerprint
strong_memoize(:plain_gitlab_fingerprint) do
next super if super.present?
next unless generic_alert_fingerprinting_enabled?
payload_excluding_params = payload.excluding(EXCLUDED_PAYLOAD_FINGERPRINT_PARAMS)
next if payload_excluding_params.none? { |_, v| v.present? }
payload_excluding_params
end
end
def generic_alert_fingerprinting_enabled?
project.feature_available?(:generic_alert_fingerprinting)
end
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::AlertManagement::Payload::Generic do
let_it_be(:project) { build_stubbed(:project) }
let(:raw_payload) { {} }
let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) }
describe '#gitlab_fingerprint' do
subject { parsed_payload.gitlab_fingerprint }
context 'with fingerprint defined in payload' do
let(:expected_fingerprint) { Digest::SHA1.hexdigest(plain_fingerprint) }
let(:plain_fingerprint) { 'fingerprint' }
let(:raw_payload) { { 'fingerprint' => plain_fingerprint } }
it { is_expected.to eq(expected_fingerprint) }
end
context 'license feature enabled' do
let(:expected_fingerprint) { Gitlab::AlertManagement::Fingerprint.generate(plain_fingerprint) }
let(:plain_fingerprint) { raw_payload.except('hosts', 'start_time') }
let(:raw_payload) do
{
'keep-this' => 'attribute',
'hosts' => 'remove me',
'start_time' => 'remove me'
}
end
before do
stub_licensed_features(generic_alert_fingerprinting: true)
end
it { is_expected.to eq(expected_fingerprint) }
context 'payload has no values' do
let(:raw_payload) do
{
'start_time' => '2020-09-17 12:49:54 -0400',
'hosts' => ['gitlab.com'],
'end_time' => '2020-09-17 12:59:54 -0400',
'title' => ' '
}
end
it { is_expected.to be_nil }
end
end
context 'license feature not enabled' do
it { is_expected.to be_nil }
end
end
end
...@@ -10,7 +10,7 @@ RSpec.describe Projects::Alerting::NotifyService do ...@@ -10,7 +10,7 @@ RSpec.describe Projects::Alerting::NotifyService do
let(:token) { alerts_service.token } let(:token) { alerts_service.token }
let(:payload) do let(:payload) do
{ {
title: 'Test alert title' 'title' => 'Test alert title'
} }
end end
...@@ -19,14 +19,11 @@ RSpec.describe Projects::Alerting::NotifyService do ...@@ -19,14 +19,11 @@ RSpec.describe Projects::Alerting::NotifyService do
subject { service.execute(token) } subject { service.execute(token) }
context 'existing alert with same payload fingerprint' do context 'existing alert with same payload fingerprint' do
let(:existing_alert) do let(:existing_alert) { create(:alert_management_alert, :from_payload, project: project, payload: payload) }
service.execute(token)
AlertManagement::Alert.last!
end
before do before do
stub_licensed_features(generic_alert_fingerprinting: fingerprinting_enabled) stub_licensed_features(generic_alert_fingerprinting: fingerprinting_enabled)
existing_alert # create existing alert existing_alert # create existing alert after enabling flag
end end
context 'generic fingerprinting license not enabled' do context 'generic fingerprinting license not enabled' do
...@@ -53,16 +50,8 @@ RSpec.describe Projects::Alerting::NotifyService do ...@@ -53,16 +50,8 @@ RSpec.describe Projects::Alerting::NotifyService do
end end
context 'end_time provided for subsequent alert' do context 'end_time provided for subsequent alert' do
before do let(:existing_alert) { create(:alert_management_alert, :from_payload, project: project, payload: payload.except('end_time')) }
payload[:end_time] = Time.current.change(usec: 0).iso8601 let(:payload) { { 'title' => 'title', 'end_time' => Time.current.change(usec: 0).iso8601 } }
end
let(:existing_alert) do
existing_payload = payload.except(:end_time)
described_class.new(project, nil, existing_payload).execute(token)
AlertManagement::Alert.last!
end
it 'does not create AlertManagement::Alert' do it 'does not create AlertManagement::Alert' do
expect { subject }.not_to change(AlertManagement::Alert, :count) expect { subject }.not_to change(AlertManagement::Alert, :count)
...@@ -70,7 +59,7 @@ RSpec.describe Projects::Alerting::NotifyService do ...@@ -70,7 +59,7 @@ RSpec.describe Projects::Alerting::NotifyService do
it 'resolves the existing alert', :aggregate_failures do it 'resolves the existing alert', :aggregate_failures do
expect { subject }.to change { existing_alert.reload.resolved? }.from(false).to(true) expect { subject }.to change { existing_alert.reload.resolved? }.from(false).to(true)
expect(existing_alert.ended_at).to eq(payload[:end_time]) expect(existing_alert.ended_at).to eq(payload['end_time'])
end end
end end
end end
......
# frozen_string_literal: true
module Gitlab
module AlertManagement
class AlertParams
MONITORING_TOOLS = {
prometheus: 'Prometheus'
}.freeze
def self.from_generic_alert(project:, payload:)
parsed_payload = Gitlab::Alerting::NotificationPayloadParser.call(payload, project).with_indifferent_access
annotations = parsed_payload[:annotations]
{
project_id: project.id,
title: annotations[:title],
description: annotations[:description],
monitoring_tool: annotations[:monitoring_tool],
service: annotations[:service],
hosts: Array(annotations[:hosts]),
payload: payload,
started_at: parsed_payload['startsAt'],
ended_at: parsed_payload['endsAt'],
severity: annotations[:severity],
fingerprint: annotations[:fingerprint],
environment: annotations[:environment]
}
end
def self.from_prometheus_alert(project:, parsed_alert:)
{
project_id: project.id,
title: parsed_alert.title,
description: parsed_alert.description,
monitoring_tool: MONITORING_TOOLS[:prometheus],
payload: parsed_alert.payload,
started_at: parsed_alert.starts_at,
ended_at: parsed_alert.ends_at,
fingerprint: parsed_alert.gitlab_fingerprint,
environment: parsed_alert.environment,
prometheus_alert: parsed_alert.gitlab_alert
}
end
end
end
end
...@@ -8,6 +8,8 @@ module Gitlab ...@@ -8,6 +8,8 @@ module Gitlab
DEFAULT_TITLE = 'New: Incident' DEFAULT_TITLE = 'New: Incident'
DEFAULT_SEVERITY = 'critical' DEFAULT_SEVERITY = 'critical'
attribute :description, paths: 'description'
attribute :ends_at, paths: 'end_time', type: :time
attribute :environment_name, paths: 'gitlab_environment_name' attribute :environment_name, paths: 'gitlab_environment_name'
attribute :hosts, paths: 'hosts' attribute :hosts, paths: 'hosts'
attribute :monitoring_tool, paths: 'monitoring_tool' attribute :monitoring_tool, paths: 'monitoring_tool'
...@@ -23,3 +25,5 @@ module Gitlab ...@@ -23,3 +25,5 @@ module Gitlab
end end
end end
end end
Gitlab::AlertManagement::Payload::Generic.prepend_if_ee('EE::Gitlab::AlertManagement::Payload::Generic')
# frozen_string_literal: true
module Gitlab
module Alerting
class Alert
include ActiveModel::Model
include Gitlab::Utils::StrongMemoize
include Presentable
attr_accessor :project, :payload, :am_alert
def self.for_alert_management_alert(project:, alert:)
params = if alert.prometheus?
alert.payload
else
Gitlab::Alerting::NotificationPayloadParser.call(alert.payload.to_h, alert.project)
end
self.new(project: project, payload: params, am_alert: alert)
end
def gitlab_alert
strong_memoize(:gitlab_alert) do
parse_gitlab_alert_from_payload
end
end
def metric_id
strong_memoize(:metric_id) do
payload&.dig('labels', 'gitlab_alert_id')
end
end
def gitlab_prometheus_alert_id
strong_memoize(:gitlab_prometheus_alert_id) do
payload&.dig('labels', 'gitlab_prometheus_alert_id')
end
end
def title
strong_memoize(:title) do
gitlab_alert&.title || parse_title_from_payload
end
end
def description
strong_memoize(:description) do
parse_description_from_payload
end
end
def environment
strong_memoize(:environment) do
gitlab_alert&.environment || parse_environment_from_payload
end
end
def annotations
strong_memoize(:annotations) do
parse_annotations_from_payload || []
end
end
def starts_at
strong_memoize(:starts_at) do
parse_datetime_from_payload('startsAt')
end
end
def starts_at_raw
strong_memoize(:starts_at_raw) do
payload&.dig('startsAt')
end
end
def ends_at
strong_memoize(:ends_at) do
parse_datetime_from_payload('endsAt')
end
end
def full_query
strong_memoize(:full_query) do
gitlab_alert&.full_query || parse_expr_from_payload
end
end
def y_label
strong_memoize(:y_label) do
parse_y_label_from_payload || title
end
end
def alert_markdown
strong_memoize(:alert_markdown) do
parse_alert_markdown_from_payload
end
end
def status
strong_memoize(:status) do
payload&.dig('status')
end
end
def firing?
status == 'firing'
end
def resolved?
status == 'resolved'
end
def gitlab_managed?
metric_id.present?
end
def gitlab_fingerprint
Gitlab::AlertManagement::Fingerprint.generate(plain_gitlab_fingerprint)
end
def valid?
payload.respond_to?(:dig) && project && title && starts_at
end
def present
super(presenter_class: Projects::Prometheus::AlertPresenter)
end
private
def plain_gitlab_fingerprint
if gitlab_managed?
[metric_id, starts_at_raw].join('/')
else # self managed
[starts_at_raw, title, full_query].join('/')
end
end
def parse_environment_from_payload
environment_name = payload&.dig('labels', 'gitlab_environment_name')
return unless environment_name
EnvironmentsFinder.new(project, nil, { name: environment_name })
.find
&.first
end
def parse_gitlab_alert_from_payload
alerts_found = matching_gitlab_alerts
return if alerts_found.blank? || alerts_found.size > 1
alerts_found.first
end
def matching_gitlab_alerts
return unless metric_id || gitlab_prometheus_alert_id
Projects::Prometheus::AlertsFinder
.new(project: project, metric: metric_id, id: gitlab_prometheus_alert_id)
.execute
end
def parse_title_from_payload
payload&.dig('annotations', 'title') ||
payload&.dig('annotations', 'summary') ||
payload&.dig('labels', 'alertname')
end
def parse_description_from_payload
payload&.dig('annotations', 'description')
end
def parse_annotations_from_payload
payload&.dig('annotations')&.map do |label, value|
Alerting::AlertAnnotation.new(label: label, value: value)
end
end
def parse_datetime_from_payload(field)
value = payload&.dig(field)
return unless value
# value is a rfc3339 timestamp
# Timestamps from Prometheus and Alertmanager are UTC RFC3339 timestamps like: '2018-03-12T09:06:00Z' (Z represents 0 offset or UTC)
# .utc sets the datetime zone to `UTC`
Time.rfc3339(value).utc
rescue ArgumentError
end
# Parses `g0.expr` from `generatorURL`.
#
# Example: http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1
def parse_expr_from_payload
url = payload&.dig('generatorURL')
return unless url
uri = URI(url)
Rack::Utils.parse_query(uri.query).fetch('g0.expr')
rescue URI::InvalidURIError, KeyError
end
def parse_alert_markdown_from_payload
payload&.dig('annotations', 'gitlab_incident_markdown')
end
def parse_y_label_from_payload
payload&.dig('annotations', 'gitlab_y_label')
end
end
end
end
...@@ -100,7 +100,7 @@ FactoryBot.define do ...@@ -100,7 +100,7 @@ FactoryBot.define do
end end
trait :prometheus do trait :prometheus do
monitoring_tool { Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus] } monitoring_tool { Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus] }
payload do payload do
{ {
annotations: { annotations: {
...@@ -123,5 +123,17 @@ FactoryBot.define do ...@@ -123,5 +123,17 @@ FactoryBot.define do
with_description with_description
low low
end end
trait :from_payload do
after(:build) do |alert|
alert_params = ::Gitlab::AlertManagement::Payload.parse(
alert.project,
alert.payload,
monitoring_tool: alert.monitoring_tool
).alert_params
alert.assign_attributes(alert_params)
end
end
end end
end end
# frozen_string_literal: true
FactoryBot.define do
factory :alerting_alert, class: 'Gitlab::Alerting::Alert' do
project
payload { {} }
transient do
metric_id { nil }
after(:build) do |alert, evaluator|
unless alert.payload.key?('startsAt')
alert.payload['startsAt'] = Time.now.rfc3339
end
if metric_id = evaluator.metric_id
alert.payload['labels'] ||= {}
alert.payload['labels']['gitlab_alert_id'] = metric_id.to_s
end
end
end
skip_create
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::AlertManagement::AlertParams do
let_it_be(:project) { create(:project, :repository, :private) }
describe '.from_generic_alert' do
let(:started_at) { Time.current.change(usec: 0).rfc3339 }
let(:default_payload) do
{
'title' => 'Alert title',
'description' => 'Description',
'monitoring_tool' => 'Monitoring tool name',
'service' => 'Service',
'hosts' => ['gitlab.com'],
'start_time' => started_at,
'some' => { 'extra' => { 'payload' => 'here' } }
}
end
let(:payload) { default_payload }
subject { described_class.from_generic_alert(project: project, payload: payload) }
it 'returns Alert compatible parameters' do
is_expected.to eq(
project_id: project.id,
title: 'Alert title',
description: 'Description',
monitoring_tool: 'Monitoring tool name',
service: 'Service',
severity: 'critical',
hosts: ['gitlab.com'],
payload: payload,
started_at: started_at,
ended_at: nil,
fingerprint: nil,
environment: nil
)
end
context 'when severity given' do
let(:payload) { default_payload.merge(severity: 'low') }
it 'returns Alert compatible parameters' do
expect(subject[:severity]).to eq('low')
end
end
context 'when there are no hosts in the payload' do
let(:payload) { {} }
it 'hosts param is an empty array' do
expect(subject[:hosts]).to be_empty
end
end
end
describe '.from_prometheus_alert' do
let(:payload) do
{
'status' => 'firing',
'labels' => {
'alertname' => 'GitalyFileServerDown',
'channel' => 'gitaly',
'pager' => 'pagerduty',
'severity' => 's1'
},
'annotations' => {
'description' => 'Alert description',
'runbook' => 'troubleshooting/gitaly-down.md',
'title' => 'Alert title'
},
'startsAt' => '2020-04-27T10:10:22.265949279Z',
'endsAt' => '0001-01-01T00:00:00Z',
'generatorURL' => 'http://8d467bd4607a:9090/graph?g0.expr=vector%281%29&g0.tab=1',
'fingerprint' => 'b6ac4d42057c43c1'
}
end
let(:parsed_alert) { Gitlab::Alerting::Alert.new(project: project, payload: payload) }
subject { described_class.from_prometheus_alert(project: project, parsed_alert: parsed_alert) }
it 'returns Alert-compatible params' do
is_expected.to eq(
project_id: project.id,
title: 'Alert title',
description: 'Alert description',
monitoring_tool: 'Prometheus',
payload: payload,
started_at: parsed_alert.starts_at,
ended_at: parsed_alert.ends_at,
fingerprint: parsed_alert.gitlab_fingerprint,
environment: parsed_alert.environment,
prometheus_alert: parsed_alert.gitlab_alert
)
end
end
end
...@@ -86,4 +86,34 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do ...@@ -86,4 +86,34 @@ RSpec.describe Gitlab::AlertManagement::Payload::Generic do
it_behaves_like 'parsable alert payload field', 'gitlab_environment_name' it_behaves_like 'parsable alert payload field', 'gitlab_environment_name'
end end
describe '#description' do
subject { parsed_payload.description }
it_behaves_like 'parsable alert payload field', 'description'
end
describe '#ends_at' do
let(:current_time) { Time.current.change(usec: 0).utc }
subject { parsed_payload.ends_at }
around do |example|
Timecop.freeze(current_time) { example.run }
end
context 'without end_time' do
it { is_expected.to be_nil }
end
context "with end_time" do
let(:value) { 10.minutes.ago.change(usec: 0).utc }
before do
raw_payload['end_time'] = value.to_s
end
it { is_expected.to eq(value) }
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Alerting::Alert do
let_it_be(:project) { create(:project) }
let(:alert) { build(:alerting_alert, project: project, payload: payload) }
let(:payload) { {} }
shared_context 'gitlab alert' do
let!(:gitlab_alert) { create(:prometheus_alert, project: project) }
let(:gitlab_alert_id) { gitlab_alert.id }
before do
payload['labels'] = {
'gitlab_alert_id' => gitlab_alert.prometheus_metric_id.to_s,
'gitlab_prometheus_alert_id' => gitlab_alert_id
}
end
end
shared_context 'full query' do
before do
payload['generatorURL'] = 'http://localhost:9090/graph?g0.expr=vector%281%29'
end
end
shared_examples 'invalid alert' do
it 'is invalid' do
expect(alert).not_to be_valid
end
end
shared_examples 'parse payload' do |*pairs|
context 'without payload' do
it { is_expected.to be_nil }
end
pairs.each do |pair|
context "with #{pair}" do
let(:value) { 'some value' }
before do
section, name = pair.split('/')
payload[section] = { name => value }
end
it { is_expected.to eq(value) }
end
end
end
describe '#gitlab_alert' do
subject { alert.gitlab_alert }
context 'without payload' do
it { is_expected.to be_nil }
end
context 'with gitlab alert' do
include_context 'gitlab alert'
it { is_expected.to eq(gitlab_alert) }
end
context 'with unknown gitlab alert' do
include_context 'gitlab alert' do
let(:gitlab_alert_id) { 'unknown' }
end
it { is_expected.to be_nil }
end
context 'when two alerts with the same metric exist' do
include_context 'gitlab alert'
let!(:second_gitlab_alert) do
create(:prometheus_alert,
project: project,
prometheus_metric_id: gitlab_alert.prometheus_metric_id
)
end
context 'alert id given in params' do
before do
payload['labels'] = {
'gitlab_alert_id' => gitlab_alert.prometheus_metric_id.to_s,
'gitlab_prometheus_alert_id' => second_gitlab_alert.id
}
end
it { is_expected.to eq(second_gitlab_alert) }
end
context 'metric id given in params' do
# This tests the case when two alerts are found, as metric id
# is not unique.
# Note the metric id was incorrectly named as 'gitlab_alert_id'
# in PrometheusAlert#to_param.
before do
payload['labels'] = { 'gitlab_alert_id' => gitlab_alert.prometheus_metric_id }
end
it { is_expected.to be_nil }
end
end
end
describe '#title' do
subject { alert.title }
it_behaves_like 'parse payload',
'annotations/title',
'annotations/summary',
'labels/alertname'
context 'with gitlab alert' do
include_context 'gitlab alert'
context 'with annotations/title' do
let(:value) { 'annotation title' }
before do
payload['annotations'] = { 'title' => value }
end
it { is_expected.to eq(gitlab_alert.title) }
end
end
end
describe '#description' do
subject { alert.description }
it_behaves_like 'parse payload', 'annotations/description'
end
describe '#annotations' do
subject { alert.annotations }
context 'without payload' do
it { is_expected.to eq([]) }
end
context 'with payload' do
before do
payload['annotations'] = { 'foo' => 'value1', 'bar' => 'value2' }
end
it 'parses annotations' do
expect(subject.size).to eq(2)
expect(subject.map(&:label)).to eq(%w[foo bar])
expect(subject.map(&:value)).to eq(%w[value1 value2])
end
end
end
describe '#environment' do
subject { alert.environment }
context 'without gitlab_alert' do
it { is_expected.to be_nil }
end
context 'with gitlab alert' do
include_context 'gitlab alert'
it { is_expected.to eq(gitlab_alert.environment) }
end
end
describe '#starts_at' do
subject { alert.starts_at }
context 'with empty startsAt' do
before do
payload['startsAt'] = nil
end
it { is_expected.to be_nil }
end
context 'with invalid startsAt' do
before do
payload['startsAt'] = 'invalid'
end
it { is_expected.to be_nil }
end
context 'with payload' do
let(:time) { Time.current.change(usec: 0) }
before do
payload['startsAt'] = time.rfc3339
end
it { is_expected.to eq(time) }
end
end
describe '#full_query' do
using RSpec::Parameterized::TableSyntax
subject { alert.full_query }
where(:generator_url, :expected_query) do
nil | nil
'http://localhost' | nil
'invalid url' | nil
'http://localhost:9090/graph?g1.expr=vector%281%29' | nil
'http://localhost:9090/graph?g0.expr=vector%281%29' | 'vector(1)'
end
with_them do
before do
payload['generatorURL'] = generator_url
end
it { is_expected.to eq(expected_query) }
end
context 'with gitlab alert' do
include_context 'gitlab alert'
include_context 'full query'
it { is_expected.to eq(gitlab_alert.full_query) }
end
end
describe '#y_label' do
subject { alert.y_label }
it_behaves_like 'parse payload', 'annotations/gitlab_y_label'
context 'when y_label is not included in the payload' do
it_behaves_like 'parse payload', 'annotations/title'
end
end
describe '#alert_markdown' do
subject { alert.alert_markdown }
it_behaves_like 'parse payload', 'annotations/gitlab_incident_markdown'
end
describe '#gitlab_fingerprint' do
subject { alert.gitlab_fingerprint }
context 'when the alert is a GitLab managed alert' do
include_context 'gitlab alert'
it 'returns a fingerprint' do
plain_fingerprint = [alert.metric_id, alert.starts_at_raw].join('/')
is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint))
end
end
context 'when the alert is from self managed Prometheus' do
include_context 'full query'
it 'returns a fingerprint' do
plain_fingerprint = [alert.starts_at_raw, alert.title, alert.full_query].join('/')
is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint))
end
end
end
describe '#valid?' do
before do
payload.update(
'annotations' => { 'title' => 'some title' },
'startsAt' => Time.current.rfc3339
)
end
subject { alert }
it { is_expected.to be_valid }
context 'without project' do
let(:project) { nil }
it { is_expected.not_to be_valid }
end
context 'without starts_at' do
before do
payload['startsAt'] = nil
end
it { is_expected.not_to be_valid }
end
end
end
...@@ -30,107 +30,118 @@ RSpec.describe Emails::Projects do ...@@ -30,107 +30,118 @@ RSpec.describe Emails::Projects do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
describe '#prometheus_alert_fired_email' do describe '#prometheus_alert_fired_email' do
subject do let(:default_title) { Gitlab::AlertManagement::Payload::Generic::DEFAULT_TITLE }
Notify.prometheus_alert_fired_email(project.id, user.id, alert_params) let(:payload) { { 'startsAt' => Time.now.rfc3339 } }
end let(:alert_attributes) { build(:alert_management_alert, :from_payload, payload: payload, project: project).attributes }
let(:alert_params) do
{ 'startsAt' => Time.now.rfc3339 }
end
context 'with a gitlab alert' do subject do
before do Notify.prometheus_alert_fired_email(project.id, user.id, alert_attributes)
alert_params['labels'] = { 'gitlab_alert_id' => alert.prometheus_metric_id.to_s }
end end
let(:title) do context 'missing required attributes' do
"#{alert.title} #{alert.computed_operator} #{alert.threshold}" let(:alert_attributes) { build(:alert_management_alert, :prometheus, :from_payload, payload: payload, project: project).attributes }
end
let(:metrics_url) do it_behaves_like 'no email'
metrics_project_environment_url(project, environment)
end end
let(:environment) { alert.environment } context 'with minimum required attributes' do
let(:payload) { {} }
let!(:alert) { create(:prometheus_alert, project: project) }
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has expected subject' do it 'has expected subject' do
is_expected.to have_subject("#{project.name} | Alert: #{environment.name}: #{title} for 5 minutes") is_expected.to have_subject("#{project.name} | Alert: #{default_title}")
end end
it 'has expected content' do it 'has expected content' do
is_expected.to have_body_text('An alert has been triggered') is_expected.to have_body_text('An alert has been triggered')
is_expected.to have_body_text(project.full_path) is_expected.to have_body_text(project.full_path)
is_expected.to have_body_text('Environment:') is_expected.not_to have_body_text('Description:')
is_expected.to have_body_text(environment.name) is_expected.not_to have_body_text('Environment:')
is_expected.to have_body_text('Metric:') is_expected.not_to have_body_text('Metric:')
is_expected.to have_body_text(alert.full_query)
is_expected.to have_body_text(metrics_url)
end end
it_behaves_like 'shows the incident issues url'
end end
context 'with no payload' do context 'with description' do
let(:alert_params) { {} } let(:payload) { { 'description' => 'alert description' } }
it_behaves_like 'no email' it_behaves_like 'an email sent from GitLab'
end it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link'
context 'with an unknown alert' do it 'has expected subject' do
before do is_expected.to have_subject("#{project.name} | Alert: #{default_title}")
alert_params['labels'] = { 'gitlab_alert_id' => 'unknown' }
end end
it_behaves_like 'no email' it 'has expected content' do
is_expected.to have_body_text('An alert has been triggered')
is_expected.to have_body_text(project.full_path)
is_expected.to have_body_text('Description:')
is_expected.to have_body_text('alert description')
is_expected.not_to have_body_text('Environment:')
is_expected.not_to have_body_text('Metric:')
end end
context 'with an external alert' do
let(:title) { 'alert title' }
let(:metrics_url) do
metrics_project_environments_url(project)
end end
before do context 'with environment' do
alert_params['annotations'] = { 'title' => title } let_it_be(:environment) { create(:environment, project: project) }
alert_params['generatorURL'] = 'http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1' let(:payload) { { 'gitlab_environment_name' => environment.name } }
end let(:metrics_url) { metrics_project_environment_url(project, environment) }
it_behaves_like 'an email sent from GitLab' it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links' it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like 'a user cannot unsubscribe through footer link' it_behaves_like 'a user cannot unsubscribe through footer link'
it 'has expected subject' do it 'has expected subject' do
is_expected.to have_subject("#{project.name} | Alert: #{title}") is_expected.to have_subject("#{project.name} | Alert: #{environment.name}: #{default_title}")
end end
it 'has expected content' do it 'has expected content' do
is_expected.to have_body_text('An alert has been triggered') is_expected.to have_body_text('An alert has been triggered')
is_expected.to have_body_text(project.full_path) is_expected.to have_body_text(project.full_path)
is_expected.to have_body_text('Environment:')
is_expected.to have_body_text(environment.name)
is_expected.not_to have_body_text('Description:') is_expected.not_to have_body_text('Description:')
is_expected.not_to have_body_text('Environment:') is_expected.not_to have_body_text('Metric:')
end end
end
context 'with gitlab alerting rule' do
let_it_be(:prometheus_alert) { create(:prometheus_alert, project: project) }
let_it_be(:environment) { prometheus_alert.environment }
context 'with annotated description' do let(:alert_attributes) { build(:alert_management_alert, :prometheus, :from_payload, payload: payload, project: project).attributes }
let(:description) { 'description' } let(:title) { "#{prometheus_alert.title} #{prometheus_alert.computed_operator} #{prometheus_alert.threshold}" }
let(:metrics_url) { metrics_project_environment_url(project, environment) }
before do before do
alert_params['annotations']['description'] = description payload['labels'] = {
'gitlab_alert_id' => prometheus_alert.prometheus_metric_id,
'alertname' => prometheus_alert.title
}
end end
it 'shows the description' do it_behaves_like 'an email sent from GitLab'
is_expected.to have_body_text('Description:') it_behaves_like 'it should not have Gmail Actions links'
is_expected.to have_body_text(description) it_behaves_like 'a user cannot unsubscribe through footer link'
end it_behaves_like 'shows the incident issues url'
it 'has expected subject' do
is_expected.to have_subject("#{project.name} | Alert: #{environment.name}: #{title} for 5 minutes")
end end
it_behaves_like 'shows the incident issues url' it 'has expected content' do
is_expected.to have_body_text('An alert has been triggered')
is_expected.to have_body_text(project.full_path)
is_expected.to have_body_text('Environment:')
is_expected.to have_body_text(environment.name)
is_expected.to have_body_text('Metric:')
is_expected.to have_body_text(prometheus_alert.full_query)
is_expected.to have_body_text(metrics_url)
is_expected.not_to have_body_text('Description:')
end
end end
end end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Prometheus::AlertPresenter do
include Gitlab::Routing.url_helpers
let_it_be(:project, reload: true) { create(:project) }
let(:presenter) { described_class.new(alert) }
let(:payload) { {} }
let(:alert) { create(:alerting_alert, project: project, payload: payload) }
shared_context 'gitlab alert' do
let(:gitlab_alert) { create(:prometheus_alert, project: project) }
let(:metric_id) { gitlab_alert.prometheus_metric_id }
let(:alert) do
create(:alerting_alert, project: project, metric_id: metric_id, payload: payload)
end
end
describe '#project_full_path' do
subject { presenter.project_full_path }
it { is_expected.to eq(project.full_path) }
end
describe '#start_time' do
subject { presenter.start_time }
let(:starts_at) { '2020-10-31T14:02:04Z' }
before do
payload['startsAt'] = starts_at
end
context 'with valid utc datetime' do
it { is_expected.to eq('31 October 2020, 2:02PM (UTC)') }
context 'with admin time zone not UTC' do
before do
allow(Time).to receive(:zone).and_return(ActiveSupport::TimeZone.new('Perth'))
end
it { is_expected.to eq('31 October 2020, 2:02PM (UTC)') }
end
end
context 'with invalid datetime' do
let(:starts_at) { 'invalid' }
it { is_expected.to be_nil }
end
end
describe '#issue_summary_markdown' do
let(:markdown_line_break) { ' ' }
subject { presenter.issue_summary_markdown }
context 'without default payload' do
it do
is_expected.to eq(
<<~MARKDOWN.chomp
**Start time:** #{presenter.start_time}
MARKDOWN
)
end
end
context 'with optional attributes' do
before do
payload['annotations'] = {
'title' => 'Alert Title',
'foo' => 'value1',
'bar' => 'value2',
'description' => 'Alert Description',
'monitoring_tool' => 'monitoring_tool_name',
'service' => 'service_name',
'hosts' => ['http://localhost:3000', 'http://localhost:3001']
}
payload['generatorURL'] = 'http://host?g0.expr=query'
end
it do
is_expected.to eq(
<<~MARKDOWN.chomp
**Start time:** #{presenter.start_time}#{markdown_line_break}
**full_query:** `query`#{markdown_line_break}
**Service:** service_name#{markdown_line_break}
**Monitoring tool:** monitoring_tool_name#{markdown_line_break}
**Hosts:** http://localhost:3000 http://localhost:3001
MARKDOWN
)
end
end
context 'when hosts is a string' do
before do
payload['annotations'] = { 'hosts' => 'http://localhost:3000' }
end
it do
is_expected.to eq(
<<~MARKDOWN.chomp
**Start time:** #{presenter.start_time}#{markdown_line_break}
**Hosts:** http://localhost:3000
MARKDOWN
)
end
end
context 'with embedded metrics' do
let(:starts_at) { '2018-03-12T09:06:00Z' }
shared_examples_for 'markdown with metrics embed' do
let(:embed_regex) { /\n\[\]\(#{Regexp.quote(presenter.metrics_dashboard_url)}\)\z/ }
context 'without a starting time available' do
around do |example|
Timecop.freeze(starts_at) { example.run }
end
before do
payload.delete('startsAt')
end
it { is_expected.to match(embed_regex) }
end
context 'with a starting time available' do
it { is_expected.to match(embed_regex) }
end
end
context 'for gitlab-managed prometheus alerts' do
include_context 'gitlab-managed prometheus alert attributes'
let(:alert) do
create(:alerting_alert, project: project, metric_id: prometheus_metric_id, payload: payload)
end
it_behaves_like 'markdown with metrics embed'
end
context 'for alerts from a self-managed prometheus' do
include_context 'self-managed prometheus alert attributes'
it_behaves_like 'markdown with metrics embed'
context 'without y_label' do
let(:y_label) { title }
before do
payload['annotations'].delete('gitlab_y_label')
end
it_behaves_like 'markdown with metrics embed'
end
context 'when not enough information is present for an embed' do
shared_examples_for 'does not include an embed' do
it { is_expected.not_to match(/\[\]\(.+\)/) }
end
context 'without title' do
before do
payload['annotations'].delete('title')
end
it_behaves_like 'does not include an embed'
end
context 'without environment' do
before do
payload['labels'].delete('gitlab_environment_name')
end
it_behaves_like 'does not include an embed'
end
context 'without full_query' do
before do
payload.delete('generatorURL')
end
it_behaves_like 'does not include an embed'
end
end
end
end
end
describe '#show_performance_dashboard_link?' do
subject { presenter.show_performance_dashboard_link? }
it { is_expected.to be_falsey }
context 'with gitlab alert' do
include_context 'gitlab alert'
it { is_expected.to eq(true) }
end
end
describe '#show_incident_issues_link?' do
subject { presenter.show_incident_issues_link? }
it { is_expected.to be_falsey }
context 'create issue setting enabled' do
before do
create(:project_incident_management_setting, project: project, create_issue: true)
end
it { is_expected.to eq(true) }
end
end
describe '#details_url' do
subject { presenter.details_url }
it { is_expected.to eq(nil) }
context 'alert management alert present' do
let_it_be(:am_alert) { create(:alert_management_alert, project: project) }
let(:alert) { create(:alerting_alert, project: project, payload: payload, am_alert: am_alert) }
it { is_expected.to eq("http://localhost/#{project.full_path}/-/alert_management/#{am_alert.iid}/details") }
end
end
context 'with gitlab alert' do
include_context 'gitlab alert'
describe '#full_title' do
let(:query_title) do
"#{gitlab_alert.title} #{gitlab_alert.computed_operator} #{gitlab_alert.threshold} for 5 minutes"
end
let(:expected_subject) do
"#{alert.environment.name}: #{query_title}"
end
subject { presenter.full_title }
it { is_expected.to eq(expected_subject) }
end
describe '#metric_query' do
subject { presenter.metric_query }
it { is_expected.to eq(gitlab_alert.full_query) }
end
describe '#environment_name' do
subject { presenter.environment_name }
it { is_expected.to eq(alert.environment.name) }
end
describe '#performance_dashboard_link' do
let(:expected_link) { metrics_project_environment_url(project, alert.environment) }
subject { presenter.performance_dashboard_link }
it { is_expected.to eq(expected_link) }
end
describe '#incident_issues_link' do
let(:expected_link) { project_issues_url(project, label_name: described_class::INCIDENT_LABEL_NAME) }
subject { presenter.incident_issues_link }
it { is_expected.to eq(expected_link) }
end
end
context 'without gitlab alert' do
describe '#full_title' do
subject { presenter.full_title }
context 'with title' do
let(:title) { 'some title' }
before do
expect(alert).to receive(:title).and_return(title)
end
it { is_expected.to eq(title) }
end
context 'without title' do
it { is_expected.to eq('') }
end
end
describe '#metric_query' do
subject { presenter.metric_query }
it { is_expected.to be_nil }
end
describe '#environment_name' do
subject { presenter.environment_name }
it { is_expected.to be_nil }
end
describe '#performance_dashboard_link' do
let(:expected_link) { metrics_project_environments_url(project) }
subject { presenter.performance_dashboard_link }
it { is_expected.to eq(expected_link) }
end
end
describe '#metrics_dashboard_url' do
subject { presenter.metrics_dashboard_url }
context 'for a non-prometheus alert' do
it { is_expected.to be_nil }
end
context 'for a self-managed prometheus alert' do
include_context 'self-managed prometheus alert attributes'
let(:prometheus_payload) { payload }
it { is_expected.to eq(dashboard_url_for_alert) }
end
context 'for a gitlab-managed prometheus alert' do
include_context 'gitlab-managed prometheus alert attributes'
let(:prometheus_payload) { payload }
it { is_expected.to eq(dashboard_url_for_alert) }
end
end
end
...@@ -3082,32 +3082,25 @@ RSpec.describe NotificationService, :mailer do ...@@ -3082,32 +3082,25 @@ RSpec.describe NotificationService, :mailer do
describe '#prometheus_alerts_fired' do describe '#prometheus_alerts_fired' do
let!(:project) { create(:project) } let!(:project) { create(:project) }
let!(:prometheus_alert) { create(:prometheus_alert, project: project) }
let!(:master) { create(:user) } let!(:master) { create(:user) }
let!(:developer) { create(:user) } let!(:developer) { create(:user) }
let(:alert_attributes) { build(:alert_management_alert, project: project).attributes }
before do before do
project.add_maintainer(master) project.add_maintainer(master)
end end
it 'sends the email to owners and masters' do it 'sends the email to owners and masters' do
expect(Notify).to receive(:prometheus_alert_fired_email).with(project.id, master.id, prometheus_alert).and_call_original expect(Notify).to receive(:prometheus_alert_fired_email).with(project.id, master.id, alert_attributes).and_call_original
expect(Notify).to receive(:prometheus_alert_fired_email).with(project.id, project.owner.id, prometheus_alert).and_call_original expect(Notify).to receive(:prometheus_alert_fired_email).with(project.id, project.owner.id, alert_attributes).and_call_original
expect(Notify).not_to receive(:prometheus_alert_fired_email).with(project.id, developer.id, prometheus_alert) expect(Notify).not_to receive(:prometheus_alert_fired_email).with(project.id, developer.id, alert_attributes)
subject.prometheus_alerts_fired(prometheus_alert.project, [prometheus_alert]) subject.prometheus_alerts_fired(project, [alert_attributes])
end end
it_behaves_like 'project emails are disabled' do it_behaves_like 'project emails are disabled' do
before do let(:notification_target) { project }
allow_next_instance_of(::Gitlab::Alerting::Alert) do |instance| let(:notification_trigger) { subject.prometheus_alerts_fired(project, [alert_attributes]) }
allow(instance).to receive(:valid?).and_return(true)
end
end
let(:alert_params) { { 'labels' => { 'gitlab_alert_id' => 'unknown' } } }
let(:notification_target) { prometheus_alert.project }
let(:notification_trigger) { subject.prometheus_alerts_fired(prometheus_alert.project, [alert_params]) }
around do |example| around do |example|
perform_enqueued_jobs { example.run } perform_enqueued_jobs { example.run }
......
...@@ -197,11 +197,10 @@ RSpec.describe Projects::Alerting::NotifyService do ...@@ -197,11 +197,10 @@ RSpec.describe Projects::Alerting::NotifyService do
end end
context 'with overlong payload' do context 'with overlong payload' do
let(:payload_raw) do let(:deep_size_object) { instance_double(Gitlab::Utils::DeepSize, valid?: false) }
{
title: 'a' * Gitlab::Utils::DeepSize::DEFAULT_MAX_SIZE, before do
start_time: starts_at.rfc3339 allow(Gitlab::Utils::DeepSize).to receive(:new).and_return(deep_size_object)
}
end end
it_behaves_like 'does not process incident issues due to error', http_status: :bad_request it_behaves_like 'does not process incident issues due to error', http_status: :bad_request
...@@ -215,17 +214,6 @@ RSpec.describe Projects::Alerting::NotifyService do ...@@ -215,17 +214,6 @@ RSpec.describe Projects::Alerting::NotifyService do
it_behaves_like 'processes incident issues' it_behaves_like 'processes incident issues'
context 'with an invalid payload' do
before do
allow(Gitlab::Alerting::NotificationPayloadParser)
.to receive(:call)
.and_raise(Gitlab::Alerting::NotificationPayloadParser::BadPayloadError)
end
it_behaves_like 'does not process incident issues due to error', http_status: :bad_request
it_behaves_like 'does not an create alert management alert'
end
context 'when alert already exists' do context 'when alert already exists' do
let(:fingerprint_sha) { Digest::SHA1.hexdigest(fingerprint) } let(:fingerprint_sha) { Digest::SHA1.hexdigest(fingerprint) }
let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) } let!(:alert) { create(:alert_management_alert, project: project, fingerprint: fingerprint_sha) }
......
...@@ -6,7 +6,7 @@ RSpec.describe IncidentManagement::ProcessPrometheusAlertWorker do ...@@ -6,7 +6,7 @@ RSpec.describe IncidentManagement::ProcessPrometheusAlertWorker do
describe '#perform' do describe '#perform' do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:prometheus_alert) { create(:prometheus_alert, project: project) } let_it_be(:prometheus_alert) { create(:prometheus_alert, project: project) }
let(:payload_key) { Gitlab::Alerting::Alert.new(project: project, payload: alert_params).gitlab_fingerprint } let(:payload_key) { Gitlab::AlertManagement::Payload::Prometheus.new(project: project, payload: alert_params).gitlab_fingerprint }
let!(:prometheus_alert_event) { create(:prometheus_alert_event, prometheus_alert: prometheus_alert, payload_key: payload_key) } let!(:prometheus_alert_event) { create(:prometheus_alert_event, prometheus_alert: prometheus_alert, payload_key: payload_key) }
let!(:settings) { create(:project_incident_management_setting, project: project, create_issue: true) } let!(:settings) { create(:project_incident_management_setting, project: project, create_issue: true) }
......
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