Commit 17a907a7 authored by Sarah Yasonik's avatar Sarah Yasonik Committed by Peter Leitzen

Write classes to represent an alert payload

Adds a class to represent an alert payload, along with
subclasses for different sources of alerts. This is meant
to ultimately replace a subset of alert presenter logic,
Gitlab::Alerting::Alert, and
Gitlab::Alerting::NotificationPayloadParser.
parent 6abf565a
......@@ -120,6 +120,10 @@ module Types
def notes
object.ordered_notes
end
def runbook
object.parsed_payload.runbook
end
end
end
end
......@@ -11,6 +11,7 @@ module AlertManagement
include Noteable
include Gitlab::SQL::Pattern
include Presentable
include Gitlab::Utils::StrongMemoize
STATUSES = {
triggered: 0,
......@@ -118,7 +119,7 @@ module AlertManagement
end
delegate :iid, to: :issue, prefix: true, allow_nil: true
delegate :metrics_dashboard_url, :runbook, :details_url, to: :present
delegate :metrics_dashboard_url, :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_status, -> (status) { where(status: status) }
......@@ -197,6 +198,14 @@ module AlertManagement
project.execute_services(hook_data, :alert_hooks)
end
# Representation of the alert's payload. Avoid accessing
# #payload attribute directly.
def parsed_payload
strong_memoize(:parsed_payload) do
Gitlab::AlertManagement::Payload.parse(project, payload, monitoring_tool: monitoring_tool)
end
end
def present
return super(presenter_class: AlertManagement::PrometheusAlertPresenter) if prometheus?
......
......@@ -38,12 +38,6 @@ module AlertManagement
MARKDOWN
end
def runbook
strong_memoize(:runbook) do
payload&.dig('runbook')
end
end
def metrics_dashboard_url; end
def details_url
......
......@@ -2,12 +2,6 @@
module AlertManagement
class PrometheusAlertPresenter < AlertManagement::AlertPresenter
def runbook
strong_memoize(:runbook) do
payload&.dig('annotations', 'runbook')
end
end
def metrics_dashboard_url
alerting_alert.metrics_dashboard_url
end
......
# frozen_string_literal: true
module Gitlab
module AlertManagement
module Payload
MONITORING_TOOLS = {
prometheus: 'Prometheus'
}.freeze
class << self
# Instantiates an instance of a subclass of
# Gitlab::AlertManagement::Payload::Base. This can
# be used to create new alerts or read content from
# the payload of an existing AlertManagement::Alert
#
# @param project [Project]
# @param payload [Hash]
# @param monitoring_tool [String]
def parse(project, payload, monitoring_tool: nil)
payload_class = payload_class_for(
monitoring_tool: monitoring_tool || payload&.dig('monitoring_tool'),
payload: payload
)
payload_class.new(project: project, payload: payload)
end
private
def payload_class_for(monitoring_tool:, payload:)
if monitoring_tool == MONITORING_TOOLS[:prometheus]
if gitlab_managed_prometheus?(payload)
::Gitlab::AlertManagement::Payload::ManagedPrometheus
else
::Gitlab::AlertManagement::Payload::Prometheus
end
else
::Gitlab::AlertManagement::Payload::Generic
end
end
def gitlab_managed_prometheus?(payload)
payload&.dig('labels', 'gitlab_alert_id').present?
end
end
end
end
end
# frozen_string_literal: true
# Representation of a payload of an alert. Defines a constant
# API so that payloads from various sources can be treated
# identically. Subclasses should define how to parse payload
# based on source of alert.
module Gitlab
module AlertManagement
module Payload
class Base
include ActiveModel::Model
include Gitlab::Utils::StrongMemoize
include Gitlab::Routing
attr_accessor :project, :payload
# Any attribute expected to be specifically read from
# or derived from an alert payload should be defined.
EXPECTED_PAYLOAD_ATTRIBUTES = [
:alert_markdown,
:alert_title,
:annotations,
:ends_at,
:environment,
:environment_name,
:full_query,
:generator_url,
:gitlab_alert,
:gitlab_fingerprint,
:gitlab_prometheus_alert_id,
:gitlab_y_label,
:description,
:hosts,
:metric_id,
:metrics_dashboard_url,
:monitoring_tool,
:runbook,
:service,
:severity,
:starts_at,
:status,
:title
].freeze
# Define expected API for a payload
EXPECTED_PAYLOAD_ATTRIBUTES.each do |key|
define_method(key) {}
end
# Defines a method which allows access to a given
# value within an alert payload
#
# @param key [Symbol] Name expected to be used to reference value
# @param paths [String, Array<String>, Array<Array<String>>,]
# List of (nested) keys at value can be found, the
# first to yield a result will be used
# @param type [Symbol] If value should be converted to another type,
# that should be specified here
# @param fallback [Proc] Block to be executed to yield a value if
# a value cannot be idenitied at any provided paths
# Example)
# attribute :title
# paths: [['title'],
# ['details', 'title']]
# fallback: Proc.new { 'New Alert' }
#
# The above sample definition will define a method
# called #title which will return the value from the
# payload under the key `title` if available, otherwise
# looking under `details.title`. If neither returns a
# value, the return value will be `'New Alert'`
def self.attribute(key, paths:, type: nil, fallback: -> { nil })
define_method(key) do
strong_memoize(key) do
paths = Array(paths).first.is_a?(String) ? [Array(paths)] : paths
value = value_for_paths(paths)
value = parse_value(value, type) if value
value.presence || fallback.call
end
end
end
# Attributes of an AlertManagement::Alert as read
# directly from a payload. Prefer accessing
# AlertManagement::Alert directly for read operations.
def alert_params
{
description: description,
ended_at: ends_at,
environment: environment,
fingerprint: gitlab_fingerprint,
hosts: Array(hosts),
monitoring_tool: monitoring_tool,
payload: payload,
project_id: project.id,
prometheus_alert: gitlab_alert,
service: service,
severity: severity,
started_at: starts_at,
title: title
}.transform_values(&:presence).compact
end
def gitlab_fingerprint
strong_memoize(:gitlab_fingerprint) do
Gitlab::AlertManagement::Fingerprint.generate(plain_gitlab_fingerprint) if plain_gitlab_fingerprint
end
end
private
def plain_gitlab_fingerprint; end
def value_for_paths(paths)
target_path = paths.find { |path| payload&.dig(*path) }
payload&.dig(*target_path) if target_path
end
def parse_value(value, type)
case type
when :time
parse_time(value)
when :integer
parse_integer(value)
else
value
end
end
def parse_time(value)
Time.parse(value).utc
rescue ArgumentError
end
def parse_integer(value)
Integer(value)
rescue ArgumentError, TypeError
end
end
end
end
end
# frozen_string_literal: true
# Attribute mapping for alerts via generic alerting integration.
module Gitlab
module AlertManagement
module Payload
class Generic < Base
DEFAULT_TITLE = 'New: Incident'
DEFAULT_SEVERITY = 'critical'
attribute :hosts, paths: 'hosts'
attribute :monitoring_tool, paths: 'monitoring_tool'
attribute :runbook, paths: 'runbook'
attribute :service, paths: 'service'
attribute :severity, paths: 'severity', fallback: -> { DEFAULT_SEVERITY }
attribute :starts_at, paths: 'start_time', type: :time, fallback: -> { Time.current.utc }
attribute :title, paths: 'title', fallback: -> { DEFAULT_TITLE }
attribute :plain_gitlab_fingerprint, paths: 'fingerprint'
private :plain_gitlab_fingerprint
end
end
end
end
# frozen_string_literal: true
# Attribute mapping for alerts via prometheus alerting integration,
# and for which payload includes gitlab-controlled attributes.
module Gitlab
module AlertManagement
module Payload
class ManagedPrometheus < ::Gitlab::AlertManagement::Payload::Prometheus
attribute :gitlab_prometheus_alert_id,
paths: %w(labels gitlab_prometheus_alert_id),
type: :integer
attribute :metric_id,
paths: %w(labels gitlab_alert_id),
type: :integer
def gitlab_alert
strong_memoize(:gitlab_alert) do
next unless metric_id || gitlab_prometheus_alert_id
alerts = Projects::Prometheus::AlertsFinder
.new(project: project, metric: metric_id, id: gitlab_prometheus_alert_id)
.execute
next if alerts.blank? || alerts.size > 1
alerts.first
end
end
def full_query
gitlab_alert&.full_query || super
end
def environment
gitlab_alert&.environment || super
end
def metrics_dashboard_url
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
)
end
private
def plain_gitlab_fingerprint
[metric_id, starts_at_raw].join('/')
end
end
end
end
end
# frozen_string_literal: true
# Attribute mapping for alerts via prometheus alerting integration.
module Gitlab
module AlertManagement
module Payload
class Prometheus < Base
attribute :alert_markdown, paths: %w(annotations gitlab_incident_markdown)
attribute :annotations, paths: 'annotations'
attribute :description, paths: %w(annotations description)
attribute :ends_at, paths: 'endsAt', type: :time
attribute :environment_name, paths: %w(labels gitlab_environment_name)
attribute :generator_url, paths: %w(generatorURL)
attribute :gitlab_y_label,
paths: [%w(annotations gitlab_y_label),
%w(annotations title),
%w(annotations summary),
%w(labels alertname)]
attribute :runbook, paths: %w(annotations runbook)
attribute :starts_at,
paths: 'startsAt',
type: :time,
fallback: -> { Time.current.utc }
attribute :status, paths: 'status'
attribute :title,
paths: [%w(annotations title),
%w(annotations summary),
%w(labels alertname)]
attribute :starts_at_raw,
paths: [%w(startsAt)]
private :starts_at_raw
METRIC_TIME_WINDOW = 30.minutes
def monitoring_tool
Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end
# Parses `g0.expr` from `generatorURL`.
#
# Example: http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1
def full_query
return unless generator_url
uri = URI(generator_url)
Rack::Utils.parse_query(uri.query).fetch('g0.expr')
rescue URI::InvalidURIError, KeyError
end
def environment
return unless environment_name
EnvironmentsFinder.new(project, nil, { name: environment_name })
.find
&.first
end
def metrics_dashboard_url
return unless environment && full_query && title
metrics_dashboard_project_environment_url(
project,
environment,
embed_json: dashboard_json,
embedded: true,
**alert_embed_window_params
)
end
private
def plain_gitlab_fingerprint
[starts_at_raw, title, full_query].join('/')
end
# Formatted for parsing by JS
def alert_embed_window_params
{
start: (starts_at - METRIC_TIME_WINDOW).utc.strftime('%FT%TZ'),
end: (starts_at + METRIC_TIME_WINDOW).utc.strftime('%FT%TZ')
}
end
def dashboard_json
{
panel_groups: [{
panels: [{
type: 'area-chart',
title: title,
y_label: gitlab_y_label,
metrics: [{
query_range: full_query
}]
}]
}]
}.to_json
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::AlertManagement::Payload::Base do
let_it_be(:project) { build_stubbed(:project) }
let(:raw_payload) { {} }
let(:payload_class) { described_class }
subject(:parsed_payload) { payload_class.new(project: project, payload: raw_payload) }
describe '.attribute' do
subject { parsed_payload.test }
context 'with a single path provided' do
let(:payload_class) do
Class.new(described_class) do
attribute :test, paths: [['test']]
end
end
it { is_expected.to be_nil }
context 'and a matching value' do
let(:raw_payload) { { 'test' => 'value' } }
it { is_expected.to eq 'value' }
end
end
context 'with multiple paths provided' do
let(:payload_class) do
Class.new(described_class) do
attribute :test, paths: [['test'], %w(alt test)]
end
end
it { is_expected.to be_nil }
context 'and a matching value' do
let(:raw_payload) { { 'alt' => { 'test' => 'value' } } }
it { is_expected.to eq 'value' }
end
end
context 'with a fallback provided' do
let(:payload_class) do
Class.new(described_class) do
attribute :test, paths: [['test']], fallback: -> { 'fallback' }
end
end
it { is_expected.to eq('fallback') }
context 'and a matching value' do
let(:raw_payload) { { 'test' => 'value' } }
it { is_expected.to eq 'value' }
end
end
context 'with a time type provided' do
let(:test_time) { Time.current.change(usec: 0) }
let(:payload_class) do
Class.new(described_class) do
attribute :test, paths: [['test']], type: :time
end
end
it { is_expected.to be_nil }
context 'with a compatible matching value' do
let(:raw_payload) { { 'test' => test_time.to_s } }
it { is_expected.to eq test_time }
end
context 'with a value in rfc3339 format' do
let(:raw_payload) { { 'test' => test_time.rfc3339 } }
it { is_expected.to eq test_time }
end
context 'with an incompatible matching value' do
let(:raw_payload) { { 'test' => 'bad time' } }
it { is_expected.to be_nil }
end
end
context 'with an integer type provided' do
let(:payload_class) do
Class.new(described_class) do
attribute :test, paths: [['test']], type: :integer
end
end
it { is_expected.to be_nil }
context 'with a compatible matching value' do
let(:raw_payload) { { 'test' => '15' } }
it { is_expected.to eq 15 }
end
context 'with an incompatible matching value' do
let(:raw_payload) { { 'test' => String } }
it { is_expected.to be_nil }
end
context 'with an incompatible matching value' do
let(:raw_payload) { { 'test' => 'apple' } }
it { is_expected.to be_nil }
end
end
end
describe '#alert_params' do
let(:payload_class) do
Class.new(described_class) do
def title
'title'
end
def description
'description'
end
end
end
subject { parsed_payload.alert_params }
it { is_expected.to eq({ description: 'description', project_id: project.id, title: 'title' }) }
end
describe '#gitlab_fingerprint' do
subject { parsed_payload.gitlab_fingerprint }
it { is_expected.to be_nil }
context 'when plain_gitlab_fingerprint is defined' do
let(:payload_class) do
Class.new(described_class) do
def plain_gitlab_fingerprint
'fingerprint'
end
end
end
it 'returns a fingerprint' do
is_expected.to eq(Digest::SHA1.hexdigest('fingerprint'))
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) }
it_behaves_like 'subclass has expected api'
describe '#title' do
subject { parsed_payload.title }
it_behaves_like 'parsable alert payload field with fallback', 'New: Incident', 'title'
end
describe '#severity' do
subject { parsed_payload.severity }
it_behaves_like 'parsable alert payload field with fallback', 'critical', 'severity'
end
describe '#monitoring_tool' do
subject { parsed_payload.monitoring_tool }
it_behaves_like 'parsable alert payload field', 'monitoring_tool'
end
describe '#service' do
subject { parsed_payload.service }
it_behaves_like 'parsable alert payload field', 'service'
end
describe '#hosts' do
subject { parsed_payload.hosts }
it_behaves_like 'parsable alert payload field', 'hosts'
end
describe '#starts_at' do
let(:current_time) { Time.current.change(usec: 0).utc }
subject { parsed_payload.starts_at }
around do |example|
Timecop.freeze(current_time) { example.run }
end
context 'without start_time' do
it { is_expected.to eq(current_time) }
end
context "with start_time" do
let(:value) { 10.minutes.ago.change(usec: 0).utc }
before do
raw_payload['start_time'] = value.to_s
end
it { is_expected.to eq(value) }
end
end
describe '#runbook' do
subject { parsed_payload.runbook }
it_behaves_like 'parsable alert payload field', 'runbook'
end
describe '#gitlab_fingerprint' do
let(:plain_fingerprint) { 'fingerprint' }
let(:raw_payload) { { 'fingerprint' => plain_fingerprint } }
subject { parsed_payload.gitlab_fingerprint }
it 'returns a fingerprint' do
is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint))
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::AlertManagement::Payload::ManagedPrometheus do
let_it_be(:project) { create(:project) }
let(:raw_payload) { {} }
let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) }
it_behaves_like 'subclass has expected api'
shared_context 'with gitlab alert' do
let_it_be(:gitlab_alert) { create(:prometheus_alert, project: project) }
let(:metric_id) { gitlab_alert.prometheus_metric_id.to_s }
let(:alert_id) { gitlab_alert.id.to_s }
end
describe '#metric_id' do
subject { parsed_payload.metric_id }
it { is_expected.to be_nil }
context 'with gitlab_alert_id' do
let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => '12' } } }
it { is_expected.to eq(12) }
end
end
describe '#gitlab_prometheus_alert_id' do
subject { parsed_payload.gitlab_prometheus_alert_id }
it { is_expected.to be_nil }
context 'with gitlab_alert_id' do
let(:raw_payload) { { 'labels' => { 'gitlab_prometheus_alert_id' => '12' } } }
it { is_expected.to eq(12) }
end
end
describe '#gitlab_alert' do
subject { parsed_payload.gitlab_alert }
context 'without alert info in payload' do
it { is_expected.to be_nil }
end
context 'with metric id in payload' do
let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id } } }
let(:metric_id) { '-1' }
context 'without matching alert' do
it { is_expected.to be_nil }
end
context 'with matching alert' do
include_context 'with gitlab alert'
it { is_expected.to eq(gitlab_alert) }
context 'when unclear which alert applies' do
# With multiple alerts for different environments,
# we can't be sure which prometheus alert the payload
# belongs to
let_it_be(:another_alert) do
create(:prometheus_alert,
prometheus_metric: gitlab_alert.prometheus_metric,
project: project)
end
it { is_expected.to be_nil }
end
end
end
context 'with alert id' do
# gitlab_prometheus_alert_id is a stronger identifier,
# but was added after gitlab_alert_id; we won't
# see it without gitlab_alert_id also present
let(:raw_payload) do
{
'labels' => {
'gitlab_alert_id' => metric_id,
'gitlab_prometheus_alert_id' => alert_id
}
}
end
context 'without matching alert' do
let(:alert_id) { '-1' }
let(:metric_id) { '-1' }
it { is_expected.to be_nil }
end
context 'with matching alerts' do
include_context 'with gitlab alert'
it { is_expected.to eq(gitlab_alert) }
end
end
end
describe '#full_query' do
subject { parsed_payload.full_query }
it { is_expected.to be_nil }
context 'with gitlab alert' do
include_context 'with gitlab alert'
let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id } } }
it { is_expected.to eq(gitlab_alert.full_query) }
end
context 'with sufficient fallback info' do
let(:raw_payload) { { 'generatorURL' => 'http://localhost:9090/graph?g0.expr=vector%281%29' } }
it { is_expected.to eq('vector(1)') }
end
end
describe '#environment' do
subject { parsed_payload.environment }
context 'with gitlab alert' do
include_context 'with gitlab alert'
let(:raw_payload) { { 'labels' => { 'gitlab_alert_id' => metric_id } } }
it { is_expected.to eq(gitlab_alert.environment) }
end
context 'with sufficient fallback info' do
let_it_be(:environment) { create(:environment, project: project, name: 'production') }
let(:raw_payload) do
{
'labels' => {
'gitlab_alert_id' => '-1',
'gitlab_environment_name' => 'production'
}
}
end
it { is_expected.to eq(environment) }
end
end
describe '#metrics_dashboard_url' do
subject { parsed_payload.metrics_dashboard_url }
context 'without alert' do
it { is_expected.to be_nil }
end
context 'with gitlab alert' do
include_context 'gitlab-managed prometheus alert attributes' do
let(:raw_payload) { payload }
end
it { is_expected.to eq(dashboard_url_for_alert) }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::AlertManagement::Payload::Prometheus do
let_it_be(:project) { create(:project) }
let(:raw_payload) { {} }
let(:parsed_payload) { described_class.new(project: project, payload: raw_payload) }
it_behaves_like 'subclass has expected api'
shared_context 'with environment' do
let_it_be(:environment) { create(:environment, project: project, name: 'production') }
end
describe '#title' do
subject { parsed_payload.title }
it_behaves_like 'parsable alert payload field',
'annotations/title',
'annotations/summary',
'labels/alertname'
end
describe '#description' do
subject { parsed_payload.description }
it_behaves_like 'parsable alert payload field', 'annotations/description'
end
describe '#annotations' do
subject { parsed_payload.annotations }
it_behaves_like 'parsable alert payload field', 'annotations'
end
describe '#status' do
subject { parsed_payload.status }
it_behaves_like 'parsable alert payload field', 'status'
end
describe '#starts_at' do
let(:current_time) { Time.current.utc }
around do |example|
Timecop.freeze { example.run }
end
subject { parsed_payload.starts_at }
context 'without payload' do
it { is_expected.to eq(current_time) }
end
context "with startsAt" do
let(:value) { 10.minutes.ago.change(usec: 0).utc }
let(:raw_payload) { { 'startsAt' => value.rfc3339 } }
it { is_expected.to eq(value) }
end
end
describe '#ends_at' do
subject { parsed_payload.ends_at }
context 'without payload' do
it { is_expected.to be_nil }
end
context "with endsAt" do
let(:value) { Time.current.change(usec: 0).utc }
let(:raw_payload) { { 'endsAt' => value.rfc3339 } }
it { is_expected.to eq(value) }
end
end
describe '#generator_url' do
subject { parsed_payload.generator_url }
it_behaves_like 'parsable alert payload field', 'generatorURL'
end
describe '#runbook' do
subject { parsed_payload.runbook }
it_behaves_like 'parsable alert payload field', 'annotations/runbook'
end
describe '#alert_markdown' do
subject { parsed_payload.alert_markdown }
it_behaves_like 'parsable alert payload field', 'annotations/gitlab_incident_markdown'
end
describe '#environment_name' do
subject { parsed_payload.environment_name }
it_behaves_like 'parsable alert payload field', 'labels/gitlab_environment_name'
end
describe '#gitlab_y_label' do
subject { parsed_payload.gitlab_y_label }
it_behaves_like 'parsable alert payload field',
'annotations/gitlab_y_label',
'annotations/title',
'annotations/summary',
'labels/alertname'
end
describe '#monitoring_tool' do
subject { parsed_payload.monitoring_tool }
it { is_expected.to eq('Prometheus') }
end
describe '#full_query' do
using RSpec::Parameterized::TableSyntax
subject { parsed_payload.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
let(:raw_payload) { { 'generatorURL' => generator_url } }
it { is_expected.to eq(expected_query) }
end
end
describe '#environment' do
subject { parsed_payload.environment }
it { is_expected.to be_nil }
context 'with environment_name' do
let(:raw_payload) { { 'labels' => { 'gitlab_environment_name' => 'production' } } }
it { is_expected.to be_nil }
context 'with matching environment' do
include_context 'with environment'
it { is_expected.to eq(environment) }
end
end
end
describe '#gitlab_fingerprint' do
subject { parsed_payload.gitlab_fingerprint }
let(:raw_payload) do
{
'startsAt' => Time.current.to_s,
'generatorURL' => 'http://localhost:9090/graph?g0.expr=vector%281%29',
'annotations' => { 'title' => 'title' }
}
end
it 'returns a fingerprint' do
plain_fingerprint = [
parsed_payload.send(:starts_at_raw),
parsed_payload.title,
parsed_payload.full_query
].join('/')
is_expected.to eq(Digest::SHA1.hexdigest(plain_fingerprint))
end
end
describe '#metrics_dashboard_url' do
include_context 'self-managed prometheus alert attributes' do
let(:raw_payload) { payload }
end
subject { parsed_payload.metrics_dashboard_url }
it { is_expected.to eq(dashboard_url_for_alert) }
context 'without environment' do
let(:raw_payload) { payload.except('labels') }
it { is_expected.to be_nil }
end
context 'without full query' do
let(:raw_payload) { payload.except('generatorURL') }
it { is_expected.to be_nil }
end
context 'without title' do
let(:raw_payload) { payload.except('annotations') }
it { is_expected.to be_nil }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::AlertManagement::Payload do
describe '#parse' do
let_it_be(:project) { build_stubbed(:project) }
let(:payload) { {} }
context 'without a monitoring_tool specified by caller' do
subject { described_class.parse(project, payload) }
context 'without a monitoring tool in the payload' do
it { is_expected.to be_a Gitlab::AlertManagement::Payload::Generic }
end
context 'with the payload specifying Prometheus' do
let(:payload) { { 'monitoring_tool' => 'Prometheus' } }
it { is_expected.to be_a Gitlab::AlertManagement::Payload::Prometheus }
context 'with gitlab-managed attributes' do
let(:payload) { { 'monitoring_tool' => 'Prometheus', 'labels' => { 'gitlab_alert_id' => '12' } } }
it { is_expected.to be_a Gitlab::AlertManagement::Payload::ManagedPrometheus }
end
end
context 'with the payload specifying an unknown tool' do
let(:payload) { { 'monitoring_tool' => 'Custom Tool' } }
it { is_expected.to be_a Gitlab::AlertManagement::Payload::Generic }
end
end
context 'with monitoring_tool specified by caller' do
subject { described_class.parse(project, payload, monitoring_tool: monitoring_tool) }
context 'as Prometheus' do
let(:monitoring_tool) { 'Prometheus' }
context 'with an externally managed prometheus payload' do
it { is_expected.to be_a Gitlab::AlertManagement::Payload::Prometheus }
end
context 'with a self-managed prometheus payload' do
let(:payload) { { 'labels' => { 'gitlab_alert_id' => '14' } } }
it { is_expected.to be_a Gitlab::AlertManagement::Payload::ManagedPrometheus }
end
end
context 'as an unknown tool' do
let(:monitoring_tool) { 'Custom Tool' }
it { is_expected.to be_a Gitlab::AlertManagement::Payload::Generic }
end
end
end
end
......@@ -9,8 +9,7 @@ RSpec.describe AlertManagement::AlertPresenter do
{
'title' => 'Alert title',
'start_time' => '2020-04-27T10:10:22.265949279Z',
'custom' => { 'param' => 73 },
'runbook' => 'https://runbook.com'
'custom' => { 'param' => 73 }
}
end
......@@ -40,8 +39,7 @@ RSpec.describe AlertManagement::AlertPresenter do
#### Alert Details
**custom.param:** 73#{markdown_line_break}
**runbook:** https://runbook.com
**custom.param:** 73
MARKDOWN
)
end
......@@ -53,12 +51,6 @@ RSpec.describe AlertManagement::AlertPresenter do
end
end
describe '#runbook' do
it 'shows the runbook from the payload' do
expect(presenter.runbook).to eq('https://runbook.com')
end
end
describe '#details_url' do
it 'returns the details URL' do
expect(presenter.details_url).to match(%r{#{project.web_url}/-/alert_management/#{alert.iid}/details})
......
......@@ -69,17 +69,4 @@ RSpec.describe AlertManagement::PrometheusAlertPresenter do
it { is_expected.to eq(dashboard_url_for_alert) }
end
end
describe '#runbook' do
subject { presenter.runbook }
it { is_expected.to be_nil }
context 'with runbook in payload' do
let(:expected_runbook) { 'https://awesome-runbook.com' }
let(:payload) { { 'annotations' => { 'runbook' => expected_runbook } } }
it { is_expected.to eq(expected_runbook) }
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'parsable alert payload field with fallback' do |fallback, *paths|
context 'without payload' do
it { is_expected.to eq(fallback) }
end
paths.each do |path|
context "with #{path}" do
let(:value) { 'some value' }
before do
section, name = path.split('/')
raw_payload[section] = name ? { name => value } : value
end
it { is_expected.to eq(value) }
end
end
end
RSpec.shared_examples 'parsable alert payload field' do |*paths|
it_behaves_like 'parsable alert payload field with fallback', nil, *paths
end
RSpec.shared_examples 'subclass has expected api' do
it 'defines all public methods in the base class' do
default_methods = Gitlab::AlertManagement::Payload::Base.public_instance_methods
subclass_methods = described_class.public_instance_methods
missing_methods = subclass_methods - default_methods
expect(missing_methods).to be_empty
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment