1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
|
# 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, :integration
# 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,
:description,
:ends_at,
:environment,
:environment_name,
:full_query,
:generator_url,
:gitlab_alert,
:gitlab_fingerprint,
:gitlab_prometheus_alert_id,
:gitlab_y_label,
:has_required_attributes?,
:hosts,
:metric_id,
:metrics_dashboard_url,
:monitoring_tool,
:resolved?,
:runbook,
:service,
:severity,
:starts_at,
:status,
:title
].freeze
private_constant :EXPECTED_PAYLOAD_ATTRIBUTES
# Define expected API for a payload
EXPECTED_PAYLOAD_ATTRIBUTES.each do |key|
define_method(key) {}
end
SEVERITY_MAPPING = {
'critical' => :critical,
'high' => :high,
'medium' => :medium,
'low' => :low,
'info' => :info
}.freeze
# Handle an unmapped severity value the same way we treat missing values
# so we can fallback to alert's default severity `critical`.
UNMAPPED_SEVERITY = nil
# 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&.truncate(::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH),
ended_at: ends_at,
environment: environment,
fingerprint: gitlab_fingerprint,
hosts: truncate_hosts(Array(hosts).flatten),
monitoring_tool: monitoring_tool&.truncate(::AlertManagement::Alert::TOOL_MAX_LENGTH),
payload: payload,
project_id: project.id,
prometheus_alert: gitlab_alert,
service: service&.truncate(::AlertManagement::Alert::SERVICE_MAX_LENGTH),
severity: severity,
started_at: starts_at,
title: title&.truncate(::AlertManagement::Alert::TITLE_MAX_LENGTH)
}.transform_values(&:presence).compact
end
def gitlab_fingerprint
strong_memoize(:gitlab_fingerprint) do
next unless plain_gitlab_fingerprint
Gitlab::AlertManagement::Fingerprint.generate(plain_gitlab_fingerprint)
end
end
def environment
strong_memoize(:environment) do
next unless environment_name
EnvironmentsFinder
.new(project, nil, { name: environment_name })
.execute
.first
end
end
def resolved?
status == 'resolved'
end
def has_required_attributes?
true
end
def severity
severity_mapping.fetch(severity_raw.to_s.downcase, UNMAPPED_SEVERITY)
end
private
def plain_gitlab_fingerprint
end
def severity_raw
end
def severity_mapping
SEVERITY_MAPPING
end
def truncate_hosts(hosts)
return hosts if hosts.join.length <= ::AlertManagement::Alert::HOSTS_MAX_LENGTH
hosts.inject([]) do |new_hosts, host|
remaining_length = ::AlertManagement::Alert::HOSTS_MAX_LENGTH - new_hosts.join.length
break new_hosts unless remaining_length > 0
new_hosts << host.to_s.truncate(remaining_length, omission: '')
end
end
# Overriden in EE::Gitlab::AlertManagement::Payload::Generic
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
|