Commit 06589d92 authored by Shinya Maeda's avatar Shinya Maeda Committed by Adam Hegyi

Expose alert information for environments

This commit extends GraphQL endpoint and Internal API
to expose alert information for environments.
parent e4695080
......@@ -107,6 +107,16 @@ module Types
description: 'Todos of the current user for the alert',
resolver: Resolvers::TodoResolver
field :details_url,
GraphQL::STRING_TYPE,
null: false,
description: 'The URL of the alert detail page'
field :prometheus_alert,
Types::PrometheusAlertType,
null: true,
description: 'The alert condition for Prometheus'
def notes
object.ordered_notes
end
......
......@@ -19,5 +19,10 @@ module Types
field :metrics_dashboard, Types::Metrics::DashboardType, null: true,
description: 'Metrics dashboard schema for the environment',
resolver: Resolvers::Metrics::DashboardResolver
field :latest_opened_most_severe_alert,
Types::AlertManagement::AlertType,
null: true,
description: 'The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.'
end
end
......@@ -169,6 +169,12 @@ module Types
description: 'Environments of the project',
resolver: Resolvers::EnvironmentsResolver
field :environment,
Types::EnvironmentType,
null: true,
description: 'A single environment of the project',
resolver: Resolvers::EnvironmentsResolver.single
field :sast_ci_configuration, ::Types::CiConfiguration::Sast::Type, null: true,
description: 'SAST CI configuration for the project',
resolver: ::Resolvers::CiConfiguration::SastResolver
......
# frozen_string_literal: true
module Types
class PrometheusAlertType < BaseObject
graphql_name 'PrometheusAlert'
description 'The alert condition for Prometheus'
authorize :read_prometheus_alerts
present_using PrometheusAlertPresenter
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the alert condition'
field :humanized_text,
GraphQL::STRING_TYPE,
null: false,
description: 'The human-readable text of the alert condition'
end
end
......@@ -118,7 +118,7 @@ module AlertManagement
end
delegate :iid, to: :issue, prefix: true, allow_nil: true
delegate :metrics_dashboard_url, :runbook, to: :present
delegate :metrics_dashboard_url, :runbook, :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_status, -> (status) { where(status: status) }
......@@ -137,6 +137,7 @@ module AlertManagement
# 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) }
scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) }
# Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
# Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
......
......@@ -29,6 +29,7 @@ class Environment < ApplicationRecord
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus'
has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline'
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :nullify_external_url
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
......@@ -291,6 +292,10 @@ class Environment < ApplicationRecord
!!ENV['USE_SAMPLE_METRICS']
end
def has_opened_alert?
latest_opened_most_severe_alert.present?
end
def metrics
prometheus_adapter.query(:environment, self) if has_metrics_and_can_query?
end
......
......@@ -3,6 +3,7 @@
class PrometheusAlert < ApplicationRecord
include Sortable
include UsageStatistics
include Presentable
OPERATORS_MAP = {
lt: "<",
......
# frozen_string_literal: true
class PrometheusAlertPolicy < ::BasePolicy
delegate { @subject.project }
end
......@@ -4,6 +4,7 @@ module AlertManagement
class AlertPresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
include IncidentManagement::Settings
include ActionView::Helpers::UrlHelper
MARKDOWN_LINE_BREAK = " \n".freeze
......@@ -45,15 +46,12 @@ module AlertManagement
def metrics_dashboard_url; end
private
def details_url
::Gitlab::Routing.url_helpers.details_project_alert_management_url(
project,
alert.iid
)
details_project_alert_management_url(project, alert.iid)
end
private
attr_reader :alert, :project
def alerting_alert
......
# frozen_string_literal: true
class PrometheusAlertPresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
presents :prometheus_alert
def humanized_text
operator_text =
case prometheus_alert.operator
when 'lt' then s_('PrometheusAlerts|is less than')
when 'eq' then s_('PrometheusAlerts|is equal to')
when 'gt' then s_('PrometheusAlerts|exceeded')
end
"#{operator_text} #{prometheus_alert.threshold}#{prometheus_alert.prometheus_metric.unit}"
end
end
......@@ -71,6 +71,8 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :destroy_environment, environment)
end
expose :has_opened_alert?, if: -> (*) { can_read_alert_management_alert? }, expose_nil: false, as: :has_opened_alert
private
alias_method :environment, :object
......@@ -91,6 +93,10 @@ class EnvironmentEntity < Grape::Entity
can?(current_user, :read_pod_logs, environment.project)
end
def can_read_alert_management_alert?
can?(current_user, :read_alert_management_alert, environment.project)
end
def cluster_platform_kubernetes?
deployment_platform && deployment_platform.is_a?(Clusters::Platforms::Kubernetes)
end
......
---
title: Expose alert information for environments
merge_request: 38881
author:
type: added
......@@ -209,6 +209,11 @@ type AlertManagementAlert implements Noteable {
"""
details: JSON
"""
The URL of the alert detail page
"""
detailsUrl: String!
"""
All discussions on this noteable
"""
......@@ -294,6 +299,11 @@ type AlertManagementAlert implements Noteable {
last: Int
): NoteConnection!
"""
The alert condition for Prometheus
"""
prometheusAlert: PrometheusAlert
"""
Runbook for the alert as defined in alert details
"""
......@@ -4418,6 +4428,11 @@ type Environment {
"""
id: ID!
"""
The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.
"""
latestOpenedMostSevereAlert: AlertManagementAlert
"""
Metrics dashboard schema for the environment
"""
......@@ -10553,6 +10568,26 @@ type Project {
"""
descriptionHtml: String
"""
A single environment of the project
"""
environment(
"""
Name of the environment
"""
name: String
"""
Search query for environment name
"""
search: String
"""
States of environments that should be included in result
"""
states: [String!]
): Environment
"""
Environments of the project
"""
......@@ -12116,6 +12151,21 @@ type ProjectStatistics {
wikiSize: Float
}
"""
The alert condition for Prometheus
"""
type PrometheusAlert {
"""
The human-readable text of the alert condition
"""
humanizedText: String!
"""
ID of the alert condition
"""
id: ID!
}
type Query {
"""
Get information about current user
......
......@@ -577,6 +577,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "detailsUrl",
"description": "The URL of the alert detail page",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "discussions",
"description": "All discussions on this noteable",
......@@ -801,6 +819,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "prometheusAlert",
"description": "The alert condition for Prometheus",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "PrometheusAlert",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runbook",
"description": "Runbook for the alert as defined in alert details",
......@@ -12338,6 +12370,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "latestOpenedMostSevereAlert",
"description": "The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "AlertManagementAlert",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "metricsDashboard",
"description": "Metrics dashboard schema for the environment",
......@@ -31457,6 +31503,57 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environment",
"description": "A single environment of the project",
"args": [
{
"name": "name",
"description": "Name of the environment",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "search",
"description": "Search query for environment name",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "states",
"description": "States of environments that should be included in result",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "Environment",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "environments",
"description": "Environments of the project",
......@@ -35655,6 +35752,55 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "PrometheusAlert",
"description": "The alert condition for Prometheus",
"fields": [
{
"name": "humanizedText",
"description": "The human-readable text of the alert condition",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the alert condition",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "Query",
......@@ -64,6 +64,7 @@ Describes an alert from the project's Alert Management
| `createdAt` | Time | Timestamp the alert was created |
| `description` | String | Description of the alert |
| `details` | JSON | Alert details |
| `detailsUrl` | String! | The URL of the alert detail page |
| `endedAt` | Time | Timestamp the alert ended |
| `eventCount` | Int | Number of events of this alert |
| `hosts` | String! => Array | List of hosts the alert came from |
......@@ -71,6 +72,7 @@ Describes an alert from the project's Alert Management
| `issueIid` | ID | Internal ID of the GitLab issue attached to the alert |
| `metricsDashboardUrl` | String | URL for metrics embed for the alert |
| `monitoringTool` | String | Monitoring tool the alert came from |
| `prometheusAlert` | PrometheusAlert | The alert condition for Prometheus |
| `runbook` | String | Runbook for the alert as defined in alert details |
| `service` | String | Service the alert came from |
| `severity` | AlertManagementSeverity | Severity of the alert |
......@@ -739,6 +741,7 @@ Describes where code is deployed for a project
| Name | Type | Description |
| --- | ---- | ---------- |
| `id` | ID! | ID of the environment |
| `latestOpenedMostSevereAlert` | AlertManagementAlert | The most severe open alert for the environment. If multiple alerts have equal severity, the most recent is returned. |
| `metricsDashboard` | MetricsDashboard | Metrics dashboard schema for the environment |
| `name` | String! | Human-readable name of the environment |
| `state` | String! | State of the environment, for example: available/stopped |
......@@ -1602,6 +1605,7 @@ Information about pagination in a connection.
| `createdAt` | Time | Timestamp of the project creation |
| `description` | String | Short description of the project |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
| `environment` | Environment | A single environment of the project |
| `forksCount` | Int! | Number of times the project has been forked |
| `fullPath` | ID! | Full path of the project |
| `grafanaIntegration` | GrafanaIntegration | Grafana integration details for the project |
......@@ -1732,6 +1736,15 @@ Represents a Project Member
| `storageSize` | Float! | Storage size of the project |
| `wikiSize` | Float | Wiki size of the project |
## PrometheusAlert
The alert condition for Prometheus
| Name | Type | Description |
| --- | ---- | ---------- |
| `humanizedText` | String! | The human-readable text of the alert condition |
| `id` | ID! | ID of the alert condition |
## Release
Represents a release
......
......@@ -75,6 +75,7 @@
"can_stop": {
"type": "boolean"
},
"has_opened_alert": { "type": "boolean" },
"cancel_auto_stop_path": { "type": "string" },
"auto_stop_at": { "type": "string", "format": "date-time" },
"can_delete": {
......
......@@ -19432,9 +19432,18 @@ msgstr ""
msgid "PrometheusAlerts|Threshold"
msgstr ""
msgid "PrometheusAlerts|exceeded"
msgstr ""
msgid "PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks"
msgstr ""
msgid "PrometheusAlerts|is equal to"
msgstr ""
msgid "PrometheusAlerts|is less than"
msgstr ""
msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr ""
......
......@@ -33,6 +33,7 @@
"updated_at": { "type": "string", "format": "date-time" },
"auto_stop_at": { "type": "string", "format": "date-time" },
"can_stop": { "type": "boolean" },
"has_opened_alert": { "type": "boolean" },
"cluster_type": { "type": "types/nullable_string.json" },
"terminal_path": { "type": "types/nullable_string.json" },
"last_deployment": {
......
......@@ -30,6 +30,8 @@ RSpec.describe GitlabSchema.types['AlertManagementAlert'] do
metrics_dashboard_url
runbook
todos
details_url
prometheus_alert
]
expect(described_class).to have_graphql_fields(*expected_fields)
......
......@@ -7,11 +7,76 @@ RSpec.describe GitlabSchema.types['Environment'] do
it 'has the expected fields' do
expected_fields = %w[
name id state metrics_dashboard
name id state metrics_dashboard latest_opened_most_severe_alert
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
specify { expect(described_class).to require_graphql_authorizations(:read_environment) }
context 'when there is an environment' do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:user) { create(:user) }
subject { GitlabSchema.execute(query, context: { current_user: user }).as_json }
let(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
name
state
}
}
}
)
end
before do
project.add_developer(user)
end
it 'returns an environment' do
expect(subject['data']['project']['environment']['name']).to eq(environment.name)
end
context 'when query alert data for the environment' do
let_it_be(:query) do
%(
query {
project(fullPath: "#{project.full_path}") {
environment(name: "#{environment.name}") {
name
state
latestOpenedMostSevereAlert {
severity
title
detailsUrl
prometheusAlert {
humanizedText
}
}
}
}
}
)
end
it 'does not return alert information' do
expect(subject['data']['project']['environment']['latestOpenedMostSevereAlert']).to be_nil
end
context 'when alert is raised on the environment' do
let!(:prometheus_alert) { create(:prometheus_alert, project: project, environment: environment) }
let!(:alert) { create(:alert_management_alert, :triggered, :prometheus, project: project, environment: environment, prometheus_alert: prometheus_alert) }
it 'returns alert information' do
expect(subject['data']['project']['environment']['latestOpenedMostSevereAlert']['severity']).to eq(alert.severity.upcase)
end
end
end
end
end
......@@ -24,7 +24,7 @@ RSpec.describe GitlabSchema.types['Project'] do
namespace group statistics repository merge_requests merge_request issues
issue milestones pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
boards jira_import_status jira_imports services releases release
environment boards jira_import_status jira_imports services releases release
alert_management_alerts alert_management_alert alert_management_alert_status_counts
container_expiration_policy sast_ci_configuration service_desk_enabled service_desk_address
issue_status_counts
......@@ -98,6 +98,13 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver) }
end
describe 'environment field' do
subject { described_class.fields['environment'] }
it { is_expected.to have_graphql_type(Types::EnvironmentType) }
it { is_expected.to have_graphql_resolver(Resolvers::EnvironmentsResolver.single) }
end
describe 'members field' do
subject { described_class.fields['projectMembers'] }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['PrometheusAlert'] do
specify { expect(described_class.graphql_name).to eq('PrometheusAlert') }
it 'has the expected fields' do
expected_fields = %w[
id humanized_text
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
specify { expect(described_class).to require_graphql_authorizations(:read_prometheus_alerts) }
end
......@@ -230,6 +230,17 @@ RSpec.describe AlertManagement::Alert do
it { is_expected.to match_array(env_alert) }
end
describe '.order_severity_with_open_prometheus_alert' do
subject { described_class.where(project: alert_project).order_severity_with_open_prometheus_alert }
let_it_be(:alert_project) { create(:project) }
let_it_be(:resolved_critical_alert) { create(:alert_management_alert, :resolved, :critical, project: alert_project) }
let_it_be(:triggered_critical_alert) { create(:alert_management_alert, :triggered, :critical, project: alert_project) }
let_it_be(:triggered_high_alert) { create(:alert_management_alert, :triggered, :high, project: alert_project) }
it { is_expected.to eq([triggered_critical_alert, triggered_high_alert]) }
end
describe '.counts_by_status' do
subject { described_class.counts_by_status }
......
......@@ -19,6 +19,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
it { is_expected.to have_many(:deployments) }
it { is_expected.to have_many(:metrics_dashboard_annotations) }
it { is_expected.to have_many(:alert_management_alerts) }
it { is_expected.to have_one(:latest_opened_most_severe_alert) }
it { is_expected.to delegate_method(:stop_action).to(:last_deployment) }
it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) }
......@@ -1347,4 +1348,27 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do
expect(project.environments.count_by_state).to eq({ stopped: 0, available: 0 })
end
end
describe '#has_opened_alert?' do
subject { environment.has_opened_alert? }
let_it_be(:project) { create(:project) }
let_it_be(:environment, reload: true) { create(:environment, project: project) }
context 'when environment has an triggered alert' do
let!(:alert) { create(:alert_management_alert, :triggered, project: project, environment: environment) }
it { is_expected.to be(true) }
end
context 'when environment has an resolved alert' do
let!(:alert) { create(:alert_management_alert, :resolved, project: project, environment: environment) }
it { is_expected.to be(false) }
end
context 'when environment does not have an alert' do
it { is_expected.to be(false) }
end
end
end
......@@ -58,4 +58,10 @@ RSpec.describe AlertManagement::AlertPresenter do
expect(presenter.runbook).to eq('https://runbook.com')
end
end
describe '#details_url' do
it 'returns the details URL' do
expect(presenter.details_url).to match(%r{#{project.web_url}/-/alert_management/#{alert.iid}/details})
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PrometheusAlertPresenter do
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let(:presenter) { described_class.new(prometheus_alert) }
describe '#humanized_text' do
subject { presenter.humanized_text }
let_it_be(:prometheus_metric) { create(:prometheus_metric, project: project) }
let(:prometheus_alert) { create(:prometheus_alert, operator: operator, project: project, environment: environment, prometheus_metric: prometheus_metric) }
let(:operator) { :gt }
it { is_expected.to eq('exceeded 1.0m/s') }
context 'when operator is eq' do
let(:operator) { :eq }
it { is_expected.to eq('is equal to 1.0m/s') }
end
context 'when operator is lt' do
let(:operator) { :lt }
it { is_expected.to eq('is less than 1.0m/s') }
end
end
end
......@@ -7,9 +7,9 @@ RSpec.describe 'getting Alert Management Alerts' do
let_it_be(:payload) { { 'custom' => { 'alert' => 'payload' }, 'runbook' => 'runbook' } }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low) }
let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload) }
let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields) }
let_it_be(:resolved_alert) { create(:alert_management_alert, :all_fields, :resolved, project: project, issue: nil, severity: :low).present }
let_it_be(:triggered_alert) { create(:alert_management_alert, :all_fields, project: project, severity: :critical, payload: payload).present }
let_it_be(:other_project_alert) { create(:alert_management_alert, :all_fields).present }
let(:params) { {} }
......@@ -75,6 +75,8 @@ RSpec.describe 'getting Alert Management Alerts' do
'createdAt' => triggered_alert.created_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'updatedAt' => triggered_alert.updated_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'metricsDashboardUrl' => nil,
'detailsUrl' => triggered_alert.details_url,
'prometheusAlert' => nil,
'runbook' => 'runbook'
)
......
......@@ -82,6 +82,26 @@ RSpec.describe EnvironmentEntity do
end
end
context 'with alert' do
let!(:environment) { create(:environment, project: project) }
let!(:prometheus_alert) { create(:prometheus_alert, project: project, environment: environment) }
let!(:alert) { create(:alert_management_alert, :triggered, :prometheus, project: project, environment: environment, prometheus_alert: prometheus_alert) }
it 'exposes active alert flag' do
project.add_maintainer(user)
expect(subject[:has_opened_alert]).to eq(true)
end
context 'when user does not have permission to read alert' do
it 'does not expose active alert flag' do
project.add_reporter(user)
expect(subject[:has_opened_alert]).to be_nil
end
end
end
context 'pod_logs' do
context 'with reporter access' do
before do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment