179 lines
5.6 KiB
Ruby
179 lines
5.6 KiB
Ruby
# 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,
|
|
: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
|
|
|
|
# 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&.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 })
|
|
.find
|
|
.first
|
|
end
|
|
end
|
|
|
|
def resolved?
|
|
status == 'resolved'
|
|
end
|
|
|
|
def has_required_attributes?
|
|
true
|
|
end
|
|
|
|
private
|
|
|
|
def plain_gitlab_fingerprint; 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
|
|
|
|
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
|