2020-04-22 20:09:41 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2020-05-29 17:08:35 -04:00
|
|
|
require_dependency 'alert_management'
|
|
|
|
|
2020-04-22 20:09:41 -04:00
|
|
|
module AlertManagement
|
|
|
|
class Alert < ApplicationRecord
|
2020-06-05 14:08:19 -04:00
|
|
|
include IidRoutes
|
2020-04-22 20:09:41 -04:00
|
|
|
include AtomicInternalId
|
|
|
|
include ShaAttribute
|
2020-05-07 02:09:38 -04:00
|
|
|
include Sortable
|
2020-06-12 08:08:56 -04:00
|
|
|
include Noteable
|
2020-05-18 05:08:12 -04:00
|
|
|
include Gitlab::SQL::Pattern
|
2020-06-23 14:09:28 -04:00
|
|
|
include Presentable
|
2020-08-20 14:10:16 -04:00
|
|
|
include Gitlab::Utils::StrongMemoize
|
2020-09-14 23:09:24 -04:00
|
|
|
include Referable
|
2020-04-22 20:09:41 -04:00
|
|
|
|
2020-05-07 11:09:29 -04:00
|
|
|
STATUSES = {
|
|
|
|
triggered: 0,
|
|
|
|
acknowledged: 1,
|
|
|
|
resolved: 2,
|
|
|
|
ignored: 3
|
|
|
|
}.freeze
|
|
|
|
|
|
|
|
STATUS_EVENTS = {
|
|
|
|
triggered: :trigger,
|
|
|
|
acknowledged: :acknowledge,
|
|
|
|
resolved: :resolve,
|
|
|
|
ignored: :ignore
|
|
|
|
}.freeze
|
|
|
|
|
2020-07-15 20:09:17 -04:00
|
|
|
OPEN_STATUSES = [
|
|
|
|
:triggered,
|
|
|
|
:acknowledged
|
|
|
|
].freeze
|
|
|
|
|
2020-04-22 20:09:41 -04:00
|
|
|
belongs_to :project
|
|
|
|
belongs_to :issue, optional: true
|
2020-06-26 14:09:03 -04:00
|
|
|
belongs_to :prometheus_alert, optional: true
|
|
|
|
belongs_to :environment, optional: true
|
2020-04-22 20:09:41 -04:00
|
|
|
|
2020-05-29 17:08:35 -04:00
|
|
|
has_many :alert_assignees, inverse_of: :alert
|
|
|
|
has_many :assignees, through: :alert_assignees
|
|
|
|
|
2020-06-12 08:08:56 -04:00
|
|
|
has_many :notes, as: :noteable, inverse_of: :noteable, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
|
2020-06-15 17:08:35 -04:00
|
|
|
has_many :ordered_notes, -> { fresh }, as: :noteable, class_name: 'Note'
|
2020-06-12 08:08:56 -04:00
|
|
|
has_many :user_mentions, class_name: 'AlertManagement::AlertUserMention', foreign_key: :alert_management_alert_id
|
|
|
|
|
2020-05-29 17:08:35 -04:00
|
|
|
has_internal_id :iid, scope: :project, init: ->(s) { s.project.alert_management_alerts.maximum(:iid) }
|
2020-04-22 20:09:41 -04:00
|
|
|
|
|
|
|
sha_attribute :fingerprint
|
|
|
|
|
|
|
|
HOSTS_MAX_LENGTH = 255
|
|
|
|
|
|
|
|
validates :title, length: { maximum: 200 }, presence: true
|
|
|
|
validates :description, length: { maximum: 1_000 }
|
|
|
|
validates :service, length: { maximum: 100 }
|
|
|
|
validates :monitoring_tool, length: { maximum: 100 }
|
|
|
|
validates :project, presence: true
|
|
|
|
validates :events, presence: true
|
|
|
|
validates :severity, presence: true
|
|
|
|
validates :status, presence: true
|
|
|
|
validates :started_at, presence: true
|
2020-07-13 08:09:18 -04:00
|
|
|
validates :fingerprint, allow_blank: true, uniqueness: {
|
|
|
|
scope: :project,
|
2020-07-13 17:09:24 -04:00
|
|
|
conditions: -> { not_resolved },
|
2020-07-13 08:09:18 -04:00
|
|
|
message: -> (object, data) { _('Cannot have multiple unresolved alerts') }
|
|
|
|
}, unless: :resolved?
|
|
|
|
validate :hosts_length
|
2020-04-22 20:09:41 -04:00
|
|
|
|
|
|
|
enum severity: {
|
|
|
|
critical: 0,
|
|
|
|
high: 1,
|
|
|
|
medium: 2,
|
|
|
|
low: 3,
|
|
|
|
info: 4,
|
|
|
|
unknown: 5
|
|
|
|
}
|
|
|
|
|
2020-05-07 11:09:29 -04:00
|
|
|
state_machine :status, initial: :triggered do
|
|
|
|
state :triggered, value: STATUSES[:triggered]
|
|
|
|
|
|
|
|
state :acknowledged, value: STATUSES[:acknowledged]
|
|
|
|
|
|
|
|
state :resolved, value: STATUSES[:resolved] do
|
|
|
|
validates :ended_at, presence: true
|
|
|
|
end
|
|
|
|
|
|
|
|
state :ignored, value: STATUSES[:ignored]
|
|
|
|
|
|
|
|
state :triggered, :acknowledged, :ignored do
|
|
|
|
validates :ended_at, absence: true
|
|
|
|
end
|
|
|
|
|
|
|
|
event :trigger do
|
|
|
|
transition any => :triggered
|
|
|
|
end
|
|
|
|
|
|
|
|
event :acknowledge do
|
|
|
|
transition any => :acknowledged
|
|
|
|
end
|
|
|
|
|
|
|
|
event :resolve do
|
|
|
|
transition any => :resolved
|
|
|
|
end
|
|
|
|
|
|
|
|
event :ignore do
|
|
|
|
transition any => :ignored
|
|
|
|
end
|
|
|
|
|
|
|
|
before_transition to: [:triggered, :acknowledged, :ignored] do |alert, _transition|
|
|
|
|
alert.ended_at = nil
|
|
|
|
end
|
|
|
|
|
|
|
|
before_transition to: :resolved do |alert, transition|
|
|
|
|
ended_at = transition.args.first
|
|
|
|
alert.ended_at = ended_at || Time.current
|
|
|
|
end
|
|
|
|
end
|
2020-04-22 20:09:41 -04:00
|
|
|
|
2020-05-11 14:09:55 -04:00
|
|
|
delegate :iid, to: :issue, prefix: true, allow_nil: true
|
2020-09-15 08:09:30 -04:00
|
|
|
delegate :details_url, to: :present
|
2020-05-11 14:09:55 -04:00
|
|
|
|
2020-04-28 11:09:29 -04:00
|
|
|
scope :for_iid, -> (iid) { where(iid: iid) }
|
2020-05-11 11:09:37 -04:00
|
|
|
scope :for_status, -> (status) { where(status: status) }
|
2020-05-07 11:09:29 -04:00
|
|
|
scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
|
2020-07-02 17:09:14 -04:00
|
|
|
scope :for_environment, -> (environment) { where(environment: environment) }
|
2020-05-18 05:08:12 -04:00
|
|
|
scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
|
2020-07-15 20:09:17 -04:00
|
|
|
scope :open, -> { with_status(OPEN_STATUSES) }
|
2020-07-13 17:09:24 -04:00
|
|
|
scope :not_resolved, -> { where.not(status: STATUSES[:resolved]) }
|
2020-07-02 17:09:14 -04:00
|
|
|
scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
|
2020-04-28 11:09:29 -04:00
|
|
|
|
2020-05-07 02:09:38 -04:00
|
|
|
scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
|
|
|
|
scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) }
|
2020-05-26 20:08:11 -04:00
|
|
|
scope :order_event_count, -> (sort_order) { order(events: sort_order) }
|
2020-07-03 11:09:13 -04:00
|
|
|
|
|
|
|
# Ascending sort order sorts severity from less critical to more critical.
|
|
|
|
# Descending sort order sorts severity from more critical to less critical.
|
|
|
|
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
|
|
|
|
scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
|
2020-08-13 14:10:36 -04:00
|
|
|
scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) }
|
2020-07-03 11:09:13 -04:00
|
|
|
|
|
|
|
# Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
|
|
|
|
# Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
|
|
|
|
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
|
|
|
|
scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
|
2020-05-07 02:09:38 -04:00
|
|
|
|
2020-05-19 20:08:20 -04:00
|
|
|
scope :counts_by_status, -> { group(:status).count }
|
2020-07-02 17:09:14 -04:00
|
|
|
scope :counts_by_project_id, -> { group(:project_id).count }
|
2020-05-19 20:08:20 -04:00
|
|
|
|
2020-07-07 08:09:16 -04:00
|
|
|
alias_method :state, :status_name
|
|
|
|
|
2020-05-07 02:09:38 -04:00
|
|
|
def self.sort_by_attribute(method)
|
|
|
|
case method.to_s
|
2020-05-26 20:08:11 -04:00
|
|
|
when 'started_at_asc' then order_start_time(:asc)
|
|
|
|
when 'started_at_desc' then order_start_time(:desc)
|
|
|
|
when 'ended_at_asc' then order_end_time(:asc)
|
|
|
|
when 'ended_at_desc' then order_end_time(:desc)
|
|
|
|
when 'event_count_asc' then order_event_count(:asc)
|
|
|
|
when 'event_count_desc' then order_event_count(:desc)
|
2020-05-07 02:09:38 -04:00
|
|
|
when 'severity_asc' then order_severity(:asc)
|
|
|
|
when 'severity_desc' then order_severity(:desc)
|
|
|
|
when 'status_asc' then order_status(:asc)
|
|
|
|
when 'status_desc' then order_status(:desc)
|
|
|
|
else
|
|
|
|
order_by(method)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-07-02 17:09:14 -04:00
|
|
|
def self.last_prometheus_alert_by_project_id
|
|
|
|
ids = select(arel_table[:id].maximum).group(:project_id)
|
|
|
|
with_prometheus_alert.where(id: ids)
|
|
|
|
end
|
|
|
|
|
2020-09-14 23:09:24 -04:00
|
|
|
def self.reference_prefix
|
|
|
|
'^alert#'
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.reference_pattern
|
|
|
|
@reference_pattern ||= %r{
|
|
|
|
(#{Project.reference_pattern})?
|
|
|
|
#{Regexp.escape(reference_prefix)}(?<alert>\d+)
|
|
|
|
}x
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.link_reference_pattern
|
|
|
|
@link_reference_pattern ||= super("alert_management", /(?<alert>\d+)\/details(\#)?/)
|
|
|
|
end
|
|
|
|
|
|
|
|
def self.reference_valid?(reference)
|
|
|
|
reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
|
|
|
|
end
|
|
|
|
|
2020-05-15 11:08:04 -04:00
|
|
|
def prometheus?
|
2020-09-22 08:09:39 -04:00
|
|
|
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
|
2020-05-15 11:08:04 -04:00
|
|
|
end
|
|
|
|
|
2020-05-25 05:08:30 -04:00
|
|
|
def register_new_event!
|
2020-06-04 14:08:32 -04:00
|
|
|
increment!(:events)
|
2020-05-25 05:08:30 -04:00
|
|
|
end
|
|
|
|
|
2020-09-14 23:09:24 -04:00
|
|
|
def to_reference(from = nil, full: false)
|
|
|
|
reference = "#{self.class.reference_prefix}#{iid}"
|
|
|
|
|
|
|
|
"#{project.to_reference_base(from, full: full)}#{reference}"
|
2020-06-05 14:08:19 -04:00
|
|
|
end
|
|
|
|
|
2020-06-06 11:08:10 -04:00
|
|
|
def execute_services
|
|
|
|
return unless project.has_active_services?(:alert_hooks)
|
|
|
|
|
|
|
|
project.execute_services(hook_data, :alert_hooks)
|
|
|
|
end
|
|
|
|
|
2020-08-20 14:10:16 -04:00
|
|
|
# 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
|
|
|
|
|
2020-04-22 20:09:41 -04:00
|
|
|
private
|
|
|
|
|
2020-06-06 11:08:10 -04:00
|
|
|
def hook_data
|
|
|
|
Gitlab::DataBuilder::Alert.build(self)
|
|
|
|
end
|
|
|
|
|
2020-04-22 20:09:41 -04:00
|
|
|
def hosts_length
|
|
|
|
return unless hosts
|
|
|
|
|
|
|
|
errors.add(:hosts, "hosts array is over #{HOSTS_MAX_LENGTH} chars") if hosts.join.length > HOSTS_MAX_LENGTH
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|