summaryrefslogtreecommitdiff
path: root/lib/gitlab/alert_management/payload/base.rb
blob: 2d769148c5f21b39db27c8ac5bda886b802697db (plain)
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
207
208
209
210
# 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: truncate(description, ::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH),
            ended_at: ends_at,
            environment: environment,
            fingerprint: gitlab_fingerprint,
            hosts: truncate_hosts(Array(hosts).flatten),
            monitoring_tool: truncate(monitoring_tool, ::AlertManagement::Alert::TOOL_MAX_LENGTH),
            payload: payload,
            project_id: project.id,
            prometheus_alert: gitlab_alert,
            service: truncate(service, ::AlertManagement::Alert::SERVICE_MAX_LENGTH),
            severity: severity,
            started_at: starts_at,
            title: truncate(title, ::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

            ::Environments::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(value, length)
          value.to_s.truncate(length)
        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, TypeError
        end

        def parse_integer(value)
          Integer(value)
        rescue ArgumentError, TypeError
        end
      end
    end
  end
end