Commit 6aac4d23 authored by James Lopez's avatar James Lopez

Merge branch '214556-user-defined-alert-identification' into 'master'

User defined alert fingerprinting

Closes #217050

See merge request gitlab-org/gitlab!32613
parents 2cfd33d4 41d69023
...@@ -135,6 +135,10 @@ module AlertManagement ...@@ -135,6 +135,10 @@ module AlertManagement
monitoring_tool == Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus] monitoring_tool == Gitlab::AlertManagement::AlertParams::MONITORING_TOOLS[:prometheus]
end end
def register_new_event!
increment!(:events, 1)
end
private private
def hosts_length def hosts_length
......
...@@ -10,7 +10,7 @@ module Projects ...@@ -10,7 +10,7 @@ module Projects
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 = create_alert 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(alert) if process_issues?
...@@ -26,13 +26,29 @@ module Projects ...@@ -26,13 +26,29 @@ module Projects
delegate :alerts_service, :alerts_service_activated?, to: :project delegate :alerts_service, :alerts_service_activated?, to: :project
def am_alert_params def am_alert_params
strong_memoize(:am_alert_params) do
Gitlab::AlertManagement::AlertParams.from_generic_alert(project: project, payload: params.to_h) Gitlab::AlertManagement::AlertParams.from_generic_alert(project: project, payload: params.to_h)
end end
end
def process_alert
if alert = find_alert_by_fingerprint(am_alert_params[:fingerprint])
alert.register_new_event!
else
create_alert
end
end
def create_alert def create_alert
AlertManagement::Alert.create(am_alert_params) AlertManagement::Alert.create(am_alert_params)
end end
def find_alert_by_fingerprint(fingerprint)
return unless fingerprint
AlertManagement::Alert.for_fingerprint(project, fingerprint).first
end
def send_email? def send_email?
incident_management_setting.send_email? incident_management_setting.send_email?
end end
......
---
title: Set fingerprints and increment events count for Alert Management alerts
merge_request: 32613
author:
type: added
...@@ -28,7 +28,7 @@ To set up the generic alerts integration: ...@@ -28,7 +28,7 @@ To set up the generic alerts integration:
## Customizing the payload ## Customizing the payload
You can customize the payload by sending the following parameters. All fields are optional: You can customize the payload by sending the following parameters. All fields other than `title` are optional:
| Property | Type | Description | | Property | Type | Description |
| -------- | ---- | ----------- | | -------- | ---- | ----------- |
...@@ -39,6 +39,7 @@ You can customize the payload by sending the following parameters. All fields ar ...@@ -39,6 +39,7 @@ You can customize the payload by sending the following parameters. All fields ar
| `monitoring_tool` | String | The name of the associated monitoring tool. | | `monitoring_tool` | String | The name of the associated monitoring tool. |
| `hosts` | String or Array | One or more hosts, as to where this incident occurred. | | `hosts` | String or Array | One or more hosts, as to where this incident occurred. |
| `severity` | String | The severity of the alert. Must be one of `critical`, `high`, `medium`, `low`, `info`, `unknown`. Default is `critical`. | | `severity` | String | The severity of the alert. Must be one of `critical`, `high`, `medium`, `low`, `info`, `unknown`. Default is `critical`. |
| `fingerprint` | String or Array | The unique identifier of the alert. This can be used to group occurrences of the same alert. |
TIP: **Payload size:** TIP: **Payload size:**
Ensure your requests are smaller than the [payload application limits](../../../administration/instance_limits.md#generic-alert-json-payloads). Ensure your requests are smaller than the [payload application limits](../../../administration/instance_limits.md#generic-alert-json-payloads).
...@@ -65,5 +66,7 @@ Example payload: ...@@ -65,5 +66,7 @@ Example payload:
"service": "service affected", "service": "service affected",
"monitoring_tool": "value", "monitoring_tool": "value",
"hosts": "value", "hosts": "value",
"severity": "high",
"fingerprint": "d19381d4e8ebca87b55cda6e8eee7385"
} }
``` ```
...@@ -20,7 +20,8 @@ module Gitlab ...@@ -20,7 +20,8 @@ module Gitlab
hosts: Array(annotations[:hosts]), hosts: Array(annotations[:hosts]),
payload: payload, payload: payload,
started_at: parsed_payload['startsAt'], started_at: parsed_payload['startsAt'],
severity: annotations[:severity] severity: annotations[:severity],
fingerprint: annotations[:fingerprint]
} }
end end
......
# frozen_string_literal: true
module Gitlab
module AlertManagement
class Fingerprint
def self.generate(data)
new.generate(data)
end
def generate(data)
return unless data.present?
if data.is_a?(Array)
data = flatten_array(data)
end
Digest::SHA1.hexdigest(data.to_s)
end
private
def flatten_array(array)
array.flatten.map!(&:to_s).join
end
end
end
end
...@@ -106,7 +106,7 @@ module Gitlab ...@@ -106,7 +106,7 @@ module Gitlab
end end
def gitlab_fingerprint def gitlab_fingerprint
Digest::SHA1.hexdigest(plain_gitlab_fingerprint) Gitlab::AlertManagement::Fingerprint.generate(plain_gitlab_fingerprint)
end end
def valid? def valid?
......
...@@ -35,6 +35,10 @@ module Gitlab ...@@ -35,6 +35,10 @@ module Gitlab
payload[:severity].presence || DEFAULT_SEVERITY payload[:severity].presence || DEFAULT_SEVERITY
end end
def fingerprint
Gitlab::AlertManagement::Fingerprint.generate(payload[:fingerprint])
end
def annotations def annotations
primary_params primary_params
.reverse_merge(flatten_secondary_params) .reverse_merge(flatten_secondary_params)
...@@ -49,7 +53,8 @@ module Gitlab ...@@ -49,7 +53,8 @@ module Gitlab
'monitoring_tool' => payload[:monitoring_tool], 'monitoring_tool' => payload[:monitoring_tool],
'service' => payload[:service], 'service' => payload[:service],
'hosts' => hosts.presence, 'hosts' => hosts.presence,
'severity' => severity 'severity' => severity,
'fingerprint' => fingerprint
} }
end end
......
...@@ -32,7 +32,8 @@ describe Gitlab::AlertManagement::AlertParams do ...@@ -32,7 +32,8 @@ describe Gitlab::AlertManagement::AlertParams do
severity: 'critical', severity: 'critical',
hosts: ['gitlab.com'], hosts: ['gitlab.com'],
payload: payload, payload: payload,
started_at: started_at started_at: started_at,
fingerprint: nil
) )
end end
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::AlertManagement::Fingerprint do
using RSpec::Parameterized::TableSyntax
let_it_be(:alert) { create(:alert_management_alert) }
describe '.generate' do
subject { described_class.generate(data) }
context 'when data is an array' do
let(:data) { [1, 'fingerprint', 'given'] }
it 'flattens the array' do
expect_next_instance_of(described_class) do |obj|
expect(obj).to receive(:flatten_array)
end
subject
end
it 'returns the hashed fingerprint' do
expected_fingerprint = Digest::SHA1.hexdigest(data.flatten.map!(&:to_s).join)
expect(subject).to eq(expected_fingerprint)
end
end
context 'when data is a non-array type' do
where(:data) do
[
111,
'fingerprint',
:fingerprint,
true,
{ test: true }
]
end
with_them do
it 'performs like a hashed fingerprint' do
expect(subject).to eq(Digest::SHA1.hexdigest(data.to_s))
end
end
end
end
end
# frozen_string_literal: true # frozen_string_literal: true
require 'fast_spec_helper' require 'spec_helper'
describe Gitlab::Alerting::NotificationPayloadParser do describe Gitlab::Alerting::NotificationPayloadParser do
describe '.call' do describe '.call' do
...@@ -89,6 +89,39 @@ describe Gitlab::Alerting::NotificationPayloadParser do ...@@ -89,6 +89,39 @@ describe Gitlab::Alerting::NotificationPayloadParser do
end end
end end
context 'with fingerprint' do
before do
payload[:fingerprint] = data
end
shared_examples 'fingerprint generation' do
it 'generates the fingerprint correctly' do
expect(result).to eq(Gitlab::AlertManagement::Fingerprint.generate(data))
end
end
context 'with blank fingerprint' do
it_behaves_like 'fingerprint generation' do
let(:data) { ' ' }
let(:result) { subject.dig('annotations', 'fingerprint') }
end
end
context 'with fingerprint given' do
it_behaves_like 'fingerprint generation' do
let(:data) { 'fingerprint' }
let(:result) { subject.dig('annotations', 'fingerprint') }
end
end
context 'with array fingerprint given' do
it_behaves_like 'fingerprint generation' do
let(:data) { [1, 'fingerprint', 'given'] }
let(:result) { subject.dig('annotations', 'fingerprint') }
end
end
end
context 'when payload attributes have blank lines' do context 'when payload attributes have blank lines' do
let(:payload) do let(:payload) do
{ {
......
...@@ -317,4 +317,14 @@ describe AlertManagement::Alert do ...@@ -317,4 +317,14 @@ describe AlertManagement::Alert do
expect { subject }.to change { alert.reload.ended_at }.to nil expect { subject }.to change { alert.reload.ended_at }.to nil
end end
end end
describe '#register_new_event!' do
subject { alert.register_new_event! }
let(:alert) { create(:alert_management_alert) }
it 'increments the events count by 1' do
expect { subject }.to change { alert.events}.by(1)
end
end
end end
...@@ -73,6 +73,7 @@ describe Projects::Alerting::NotifyService do ...@@ -73,6 +73,7 @@ describe Projects::Alerting::NotifyService do
describe '#execute' do describe '#execute' do
let(:token) { 'invalid-token' } let(:token) { 'invalid-token' }
let(:starts_at) { Time.current.change(usec: 0) } let(:starts_at) { Time.current.change(usec: 0) }
let(:fingerprint) { 'testing' }
let(:service) { described_class.new(project, nil, payload) } let(:service) { described_class.new(project, nil, payload) }
let(:payload_raw) do let(:payload_raw) do
{ {
...@@ -82,7 +83,8 @@ describe Projects::Alerting::NotifyService do ...@@ -82,7 +83,8 @@ describe Projects::Alerting::NotifyService do
monitoring_tool: 'GitLab RSpec', monitoring_tool: 'GitLab RSpec',
service: 'GitLab Test Suite', service: 'GitLab Test Suite',
description: 'Very detailed description', description: 'Very detailed description',
hosts: ['1.1.1.1', '2.2.2.2'] hosts: ['1.1.1.1', '2.2.2.2'],
fingerprint: fingerprint
}.with_indifferent_access }.with_indifferent_access
end end
let(:payload) { ActionController::Parameters.new(payload_raw).permit! } let(:payload) { ActionController::Parameters.new(payload_raw).permit! }
...@@ -131,11 +133,23 @@ describe Projects::Alerting::NotifyService do ...@@ -131,11 +133,23 @@ describe Projects::Alerting::NotifyService do
description: payload_raw.fetch(:description), description: payload_raw.fetch(:description),
monitoring_tool: payload_raw.fetch(:monitoring_tool), monitoring_tool: payload_raw.fetch(:monitoring_tool),
service: payload_raw.fetch(:service), service: payload_raw.fetch(:service),
fingerprint: nil, fingerprint: Digest::SHA1.hexdigest(fingerprint),
ended_at: nil ended_at: nil
) )
end end
context 'existing alert with same fingerprint' do
let!(:existing_alert) { create(:alert_management_alert, project: project, fingerprint: Digest::SHA1.hexdigest(fingerprint)) }
it 'does not create AlertManagement::Alert' do
expect { subject }.not_to change(AlertManagement::Alert, :count)
end
it 'increments the existing alert count' do
expect { subject }.to change { existing_alert.reload.events }.from(1).to(2)
end
end
context 'with a minimal payload' do context 'with a minimal payload' do
let(:payload_raw) do let(:payload_raw) 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