1# frozen_string_literal: true 2 3# Representation of a payload of an alert. Defines a constant 4# API so that payloads from various sources can be treated 5# identically. Subclasses should define how to parse payload 6# based on source of alert. 7module Gitlab 8 module AlertManagement 9 module Payload 10 class Base 11 include ActiveModel::Model 12 include Gitlab::Utils::StrongMemoize 13 include Gitlab::Routing 14 15 attr_accessor :project, :payload, :integration 16 17 # Any attribute expected to be specifically read from 18 # or derived from an alert payload should be defined. 19 EXPECTED_PAYLOAD_ATTRIBUTES = [ 20 :alert_markdown, 21 :alert_title, 22 :annotations, 23 :description, 24 :ends_at, 25 :environment, 26 :environment_name, 27 :full_query, 28 :generator_url, 29 :gitlab_alert, 30 :gitlab_fingerprint, 31 :gitlab_prometheus_alert_id, 32 :gitlab_y_label, 33 :has_required_attributes?, 34 :hosts, 35 :metric_id, 36 :metrics_dashboard_url, 37 :monitoring_tool, 38 :resolved?, 39 :runbook, 40 :service, 41 :severity, 42 :starts_at, 43 :status, 44 :title 45 ].freeze 46 47 private_constant :EXPECTED_PAYLOAD_ATTRIBUTES 48 49 # Define expected API for a payload 50 EXPECTED_PAYLOAD_ATTRIBUTES.each do |key| 51 define_method(key) {} 52 end 53 54 SEVERITY_MAPPING = { 55 'critical' => :critical, 56 'high' => :high, 57 'medium' => :medium, 58 'low' => :low, 59 'info' => :info 60 }.freeze 61 62 # Handle an unmapped severity value the same way we treat missing values 63 # so we can fallback to alert's default severity `critical`. 64 UNMAPPED_SEVERITY = nil 65 66 # Defines a method which allows access to a given 67 # value within an alert payload 68 # 69 # @param key [Symbol] Name expected to be used to reference value 70 # @param paths [String, Array<String>, Array<Array<String>>,] 71 # List of (nested) keys at value can be found, the 72 # first to yield a result will be used 73 # @param type [Symbol] If value should be converted to another type, 74 # that should be specified here 75 # @param fallback [Proc] Block to be executed to yield a value if 76 # a value cannot be idenitied at any provided paths 77 # Example) 78 # attribute :title 79 # paths: [['title'], 80 # ['details', 'title']] 81 # fallback: Proc.new { 'New Alert' } 82 # 83 # The above sample definition will define a method 84 # called #title which will return the value from the 85 # payload under the key `title` if available, otherwise 86 # looking under `details.title`. If neither returns a 87 # value, the return value will be `'New Alert'` 88 def self.attribute(key, paths:, type: nil, fallback: -> { nil }) 89 define_method(key) do 90 strong_memoize(key) do 91 paths = Array(paths).first.is_a?(String) ? [Array(paths)] : paths 92 value = value_for_paths(paths) 93 value = parse_value(value, type) if value 94 95 value.presence || fallback.call 96 end 97 end 98 end 99 100 # Attributes of an AlertManagement::Alert as read 101 # directly from a payload. Prefer accessing 102 # AlertManagement::Alert directly for read operations. 103 def alert_params 104 { 105 description: description&.truncate(::AlertManagement::Alert::DESCRIPTION_MAX_LENGTH), 106 ended_at: ends_at, 107 environment: environment, 108 fingerprint: gitlab_fingerprint, 109 hosts: truncate_hosts(Array(hosts).flatten), 110 monitoring_tool: monitoring_tool&.truncate(::AlertManagement::Alert::TOOL_MAX_LENGTH), 111 payload: payload, 112 project_id: project.id, 113 prometheus_alert: gitlab_alert, 114 service: service&.truncate(::AlertManagement::Alert::SERVICE_MAX_LENGTH), 115 severity: severity, 116 started_at: starts_at, 117 title: title&.truncate(::AlertManagement::Alert::TITLE_MAX_LENGTH) 118 }.transform_values(&:presence).compact 119 end 120 121 def gitlab_fingerprint 122 strong_memoize(:gitlab_fingerprint) do 123 next unless plain_gitlab_fingerprint 124 125 Gitlab::AlertManagement::Fingerprint.generate(plain_gitlab_fingerprint) 126 end 127 end 128 129 def environment 130 strong_memoize(:environment) do 131 next unless environment_name 132 133 ::Environments::EnvironmentsFinder 134 .new(project, nil, { name: environment_name }) 135 .execute 136 .first 137 end 138 end 139 140 def resolved? 141 status == 'resolved' 142 end 143 144 def has_required_attributes? 145 true 146 end 147 148 def severity 149 severity_mapping.fetch(severity_raw.to_s.downcase, UNMAPPED_SEVERITY) 150 end 151 152 private 153 154 def plain_gitlab_fingerprint 155 end 156 157 def severity_raw 158 end 159 160 def severity_mapping 161 SEVERITY_MAPPING 162 end 163 164 def truncate_hosts(hosts) 165 return hosts if hosts.join.length <= ::AlertManagement::Alert::HOSTS_MAX_LENGTH 166 167 hosts.inject([]) do |new_hosts, host| 168 remaining_length = ::AlertManagement::Alert::HOSTS_MAX_LENGTH - new_hosts.join.length 169 170 break new_hosts unless remaining_length > 0 171 172 new_hosts << host.to_s.truncate(remaining_length, omission: '') 173 end 174 end 175 176 # Overriden in EE::Gitlab::AlertManagement::Payload::Generic 177 def value_for_paths(paths) 178 target_path = paths.find { |path| payload&.dig(*path) } 179 180 payload&.dig(*target_path) if target_path 181 end 182 183 def parse_value(value, type) 184 case type 185 when :time 186 parse_time(value) 187 when :integer 188 parse_integer(value) 189 else 190 value 191 end 192 end 193 194 def parse_time(value) 195 Time.parse(value).utc 196 rescue ArgumentError, TypeError 197 end 198 199 def parse_integer(value) 200 Integer(value) 201 rescue ArgumentError, TypeError 202 end 203 end 204 end 205 end 206end 207