diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-09-19 01:45:44 +0000 |
commit | 85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch) | |
tree | 9160f299afd8c80c038f08e1545be119f5e3f1e1 /lib/gitlab/alert_management | |
parent | 15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff) | |
download | gitlab-ce-85dc423f7090da0a52c73eb66faf22ddb20efff9.tar.gz |
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'lib/gitlab/alert_management')
-rw-r--r-- | lib/gitlab/alert_management/alert_params.rb | 4 | ||||
-rw-r--r-- | lib/gitlab/alert_management/payload.rb | 48 | ||||
-rw-r--r-- | lib/gitlab/alert_management/payload/base.rb | 167 | ||||
-rw-r--r-- | lib/gitlab/alert_management/payload/generic.rb | 25 | ||||
-rw-r--r-- | lib/gitlab/alert_management/payload/managed_prometheus.rb | 58 | ||||
-rw-r--r-- | lib/gitlab/alert_management/payload/prometheus.rb | 99 |
6 files changed, 400 insertions, 1 deletions
diff --git a/lib/gitlab/alert_management/alert_params.rb b/lib/gitlab/alert_management/alert_params.rb index 84a75e62ecf..3bb839c1114 100644 --- a/lib/gitlab/alert_management/alert_params.rb +++ b/lib/gitlab/alert_management/alert_params.rb @@ -20,8 +20,10 @@ module Gitlab hosts: Array(annotations[:hosts]), payload: payload, started_at: parsed_payload['startsAt'], + ended_at: parsed_payload['endsAt'], severity: annotations[:severity], - fingerprint: annotations[:fingerprint] + fingerprint: annotations[:fingerprint], + environment: annotations[:environment] } end diff --git a/lib/gitlab/alert_management/payload.rb b/lib/gitlab/alert_management/payload.rb new file mode 100644 index 00000000000..177d544d720 --- /dev/null +++ b/lib/gitlab/alert_management/payload.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +module Gitlab + module AlertManagement + module Payload + MONITORING_TOOLS = { + prometheus: 'Prometheus' + }.freeze + + class << self + # Instantiates an instance of a subclass of + # Gitlab::AlertManagement::Payload::Base. This can + # be used to create new alerts or read content from + # the payload of an existing AlertManagement::Alert + # + # @param project [Project] + # @param payload [Hash] + # @param monitoring_tool [String] + def parse(project, payload, monitoring_tool: nil) + payload_class = payload_class_for( + monitoring_tool: monitoring_tool || payload&.dig('monitoring_tool'), + payload: payload + ) + + payload_class.new(project: project, payload: payload) + end + + private + + def payload_class_for(monitoring_tool:, payload:) + if monitoring_tool == MONITORING_TOOLS[:prometheus] + if gitlab_managed_prometheus?(payload) + ::Gitlab::AlertManagement::Payload::ManagedPrometheus + else + ::Gitlab::AlertManagement::Payload::Prometheus + end + else + ::Gitlab::AlertManagement::Payload::Generic + end + end + + def gitlab_managed_prometheus?(payload) + payload&.dig('labels', 'gitlab_alert_id').present? + end + end + end + end +end diff --git a/lib/gitlab/alert_management/payload/base.rb b/lib/gitlab/alert_management/payload/base.rb new file mode 100644 index 00000000000..74e47e5226e --- /dev/null +++ b/lib/gitlab/alert_management/payload/base.rb @@ -0,0 +1,167 @@ +# 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, + ended_at: ends_at, + environment: environment, + fingerprint: gitlab_fingerprint, + hosts: Array(hosts), + monitoring_tool: monitoring_tool, + payload: payload, + project_id: project.id, + prometheus_alert: gitlab_alert, + service: service, + severity: severity, + started_at: starts_at, + title: title + }.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 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 diff --git a/lib/gitlab/alert_management/payload/generic.rb b/lib/gitlab/alert_management/payload/generic.rb new file mode 100644 index 00000000000..7efdfac75dc --- /dev/null +++ b/lib/gitlab/alert_management/payload/generic.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Attribute mapping for alerts via generic alerting integration. +module Gitlab + module AlertManagement + module Payload + class Generic < Base + DEFAULT_TITLE = 'New: Incident' + DEFAULT_SEVERITY = 'critical' + + attribute :environment_name, paths: 'gitlab_environment_name' + attribute :hosts, paths: 'hosts' + attribute :monitoring_tool, paths: 'monitoring_tool' + attribute :runbook, paths: 'runbook' + attribute :service, paths: 'service' + attribute :severity, paths: 'severity', fallback: -> { DEFAULT_SEVERITY } + attribute :starts_at, paths: 'start_time', type: :time, fallback: -> { Time.current.utc } + attribute :title, paths: 'title', fallback: -> { DEFAULT_TITLE } + + attribute :plain_gitlab_fingerprint, paths: 'fingerprint' + private :plain_gitlab_fingerprint + end + end + end +end diff --git a/lib/gitlab/alert_management/payload/managed_prometheus.rb b/lib/gitlab/alert_management/payload/managed_prometheus.rb new file mode 100644 index 00000000000..2236e60a0c6 --- /dev/null +++ b/lib/gitlab/alert_management/payload/managed_prometheus.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +# Attribute mapping for alerts via prometheus alerting integration, +# and for which payload includes gitlab-controlled attributes. +module Gitlab + module AlertManagement + module Payload + class ManagedPrometheus < ::Gitlab::AlertManagement::Payload::Prometheus + attribute :gitlab_prometheus_alert_id, + paths: %w(labels gitlab_prometheus_alert_id), + type: :integer + attribute :metric_id, + paths: %w(labels gitlab_alert_id), + type: :integer + + def gitlab_alert + strong_memoize(:gitlab_alert) do + next unless metric_id || gitlab_prometheus_alert_id + + alerts = Projects::Prometheus::AlertsFinder + .new(project: project, metric: metric_id, id: gitlab_prometheus_alert_id) + .execute + + next if alerts.blank? || alerts.size > 1 + + alerts.first + end + end + + def full_query + gitlab_alert&.full_query || super + end + + def environment + gitlab_alert&.environment || super + end + + def metrics_dashboard_url + return unless gitlab_alert + + metrics_dashboard_project_prometheus_alert_url( + project, + gitlab_alert.prometheus_metric_id, + environment_id: environment.id, + embedded: true, + **alert_embed_window_params + ) + end + + private + + def plain_gitlab_fingerprint + [metric_id, starts_at_raw].join('/') + end + end + end + end +end diff --git a/lib/gitlab/alert_management/payload/prometheus.rb b/lib/gitlab/alert_management/payload/prometheus.rb new file mode 100644 index 00000000000..336e9b319e8 --- /dev/null +++ b/lib/gitlab/alert_management/payload/prometheus.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +# Attribute mapping for alerts via prometheus alerting integration. +module Gitlab + module AlertManagement + module Payload + class Prometheus < Base + attribute :alert_markdown, paths: %w(annotations gitlab_incident_markdown) + attribute :annotations, paths: 'annotations' + attribute :description, paths: %w(annotations description) + attribute :ends_at, paths: 'endsAt', type: :time + attribute :environment_name, paths: %w(labels gitlab_environment_name) + attribute :generator_url, paths: %w(generatorURL) + attribute :gitlab_y_label, + paths: [%w(annotations gitlab_y_label), + %w(annotations title), + %w(annotations summary), + %w(labels alertname)] + attribute :runbook, paths: %w(annotations runbook) + attribute :starts_at, + paths: 'startsAt', + type: :time, + fallback: -> { Time.current.utc } + attribute :status, paths: 'status' + attribute :title, + paths: [%w(annotations title), + %w(annotations summary), + %w(labels alertname)] + + attribute :starts_at_raw, + paths: [%w(startsAt)] + private :starts_at_raw + + METRIC_TIME_WINDOW = 30.minutes + + def monitoring_tool + Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus] + end + + # Parses `g0.expr` from `generatorURL`. + # + # Example: http://localhost:9090/graph?g0.expr=vector%281%29&g0.tab=1 + def full_query + return unless generator_url + + uri = URI(generator_url) + + Rack::Utils.parse_query(uri.query).fetch('g0.expr') + rescue URI::InvalidURIError, KeyError + end + + def metrics_dashboard_url + return unless environment && full_query && title + + metrics_dashboard_project_environment_url( + project, + environment, + embed_json: dashboard_json, + embedded: true, + **alert_embed_window_params + ) + end + + def has_required_attributes? + project && title && starts_at_raw + end + + private + + def plain_gitlab_fingerprint + [starts_at_raw, title, full_query].join('/') + end + + # Formatted for parsing by JS + def alert_embed_window_params + { + start: (starts_at - METRIC_TIME_WINDOW).utc.strftime('%FT%TZ'), + end: (starts_at + METRIC_TIME_WINDOW).utc.strftime('%FT%TZ') + } + end + + def dashboard_json + { + panel_groups: [{ + panels: [{ + type: 'area-chart', + title: title, + y_label: gitlab_y_label, + metrics: [{ + query_range: full_query + }] + }] + }] + }.to_json + end + end + end + end +end |