Commit fd7d4fd2 authored by Sean Arnold's avatar Sean Arnold Committed by Dmytro Zaporozhets

Add Alert Management minimal GraphQL solution

types, enums, resolver, policy
parent bd311c3e
# frozen_string_literal: true
module AlertManagement
class AlertsFinder
def initialize(current_user, project, params)
@current_user = current_user
@project = project
@params = params
end
def execute
return AlertManagement::Alert.none unless authorized?
collection = project.alert_management_alerts
by_iid(collection)
end
private
attr_reader :current_user, :project, :params
def by_iid(collection)
return collection unless params[:iid]
collection.for_iid(params[:iid])
end
def authorized?
Ability.allowed?(current_user, :read_alert_management_alerts, project)
end
end
end
# frozen_string_literal: true
module Resolvers
class AlertManagementAlertResolver < BaseResolver
argument :iid, GraphQL::STRING_TYPE,
required: false,
description: 'IID of the alert. For example, "1"'
type Types::AlertManagement::AlertType, null: true
def resolve(**args)
parent = object.respond_to?(:sync) ? object.sync : object
return AlertManagement::Alert.none if parent.nil?
AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute
end
end
end
# frozen_string_literal: true
module Types
module AlertManagement
class AlertType < BaseObject
graphql_name 'AlertManagementAlert'
description "Describes an alert from the project's Alert Management"
authorize :read_alert_management_alerts
field :iid,
GraphQL::ID_TYPE,
null: false,
description: 'Internal ID of the alert'
field :title,
GraphQL::STRING_TYPE,
null: true,
description: 'Title of the alert'
field :severity,
AlertManagement::SeverityEnum,
null: true,
description: 'Severity of the alert'
field :status,
AlertManagement::StatusEnum,
null: true,
description: 'Status of the alert'
field :service,
GraphQL::STRING_TYPE,
null: true,
description: 'Service the alert came from'
field :monitoring_tool,
GraphQL::STRING_TYPE,
null: true,
description: 'Monitoring tool the alert came from'
field :started_at,
Types::TimeType,
null: true,
description: 'Timestamp the alert was raised'
field :ended_at,
Types::TimeType,
null: true,
description: 'Timestamp the alert ended'
field :event_count,
GraphQL::INT_TYPE,
null: true,
description: 'Number of events of this alert',
method: :events
end
end
end
# frozen_string_literal: true
module Types
module AlertManagement
class SeverityEnum < BaseEnum
graphql_name 'AlertManagementSeverity'
description 'Alert severity values'
::AlertManagement::Alert.severities.keys.each do |severity|
value severity.upcase, value: severity, description: "#{severity.titleize} severity"
end
end
end
end
# frozen_string_literal: true
module Types
module AlertManagement
class StatusEnum < BaseEnum
graphql_name 'AlertManagementStatus'
description 'Alert status values'
::AlertManagement::Alert.statuses.keys.each do |status|
value status.upcase, value: status, description: "#{status.titleize} status"
end
end
end
end
......@@ -205,6 +205,18 @@ module Types
null: true,
description: 'Project services',
resolver: Resolvers::Projects::ServicesResolver
field :alert_management_alerts,
Types::AlertManagement::AlertType.connection_type,
null: true,
description: 'Alert Management alerts of the project',
resolver: Resolvers::AlertManagementAlertResolver
field :alert_management_alert,
Types::AlertManagement::AlertType,
null: true,
description: 'A single Alert Management alert of the project',
resolver: Resolvers::AlertManagementAlertResolver.single
end
end
......
......@@ -43,6 +43,8 @@ module AlertManagement
ignored: 3
}
scope :for_iid, -> (iid) { where(iid: iid) }
def fingerprint=(value)
if value.blank?
super(nil)
......
# frozen_string_literal: true
module AlertManagement
class AlertPolicy < ::BasePolicy
delegate { @subject.project }
end
end
......@@ -226,6 +226,7 @@ class ProjectPolicy < BasePolicy
enable :read_alert_management
enable :read_prometheus
enable :read_metrics_dashboard_annotation
enable :read_alert_management_alerts
end
# We define `:public_user_access` separately because there are cases in gitlab-ee
......
---
title: Add GraphQL type for reading Alert Management Alerts
merge_request: 30140
author:
type: added
......@@ -103,6 +103,151 @@ type AdminSidekiqQueuesDeleteJobsPayload {
result: DeleteJobsResponse
}
"""
Describes an alert from the project's Alert Management
"""
type AlertManagementAlert {
"""
Timestamp the alert ended
"""
endedAt: Time
"""
Number of events of this alert
"""
eventCount: Int
"""
Internal ID of the alert
"""
iid: ID!
"""
Monitoring tool the alert came from
"""
monitoringTool: String
"""
Service the alert came from
"""
service: String
"""
Severity of the alert
"""
severity: AlertManagementSeverity
"""
Timestamp the alert was raised
"""
startedAt: Time
"""
Status of the alert
"""
status: AlertManagementStatus
"""
Title of the alert
"""
title: String
}
"""
The connection type for AlertManagementAlert.
"""
type AlertManagementAlertConnection {
"""
A list of edges.
"""
edges: [AlertManagementAlertEdge]
"""
A list of nodes.
"""
nodes: [AlertManagementAlert]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type AlertManagementAlertEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: AlertManagementAlert
}
"""
Alert severity values
"""
enum AlertManagementSeverity {
"""
Critical severity
"""
CRITICAL
"""
High severity
"""
HIGH
"""
Info severity
"""
INFO
"""
Low severity
"""
LOW
"""
Medium severity
"""
MEDIUM
"""
Unknown severity
"""
UNKNOWN
}
"""
Alert status values
"""
enum AlertManagementStatus {
"""
Acknowledged status
"""
ACKNOWLEDGED
"""
Ignored status
"""
IGNORED
"""
Resolved status
"""
RESOLVED
"""
Triggered status
"""
TRIGGERED
}
"""
An emoji awarded by a user.
"""
......@@ -6293,6 +6438,46 @@ enum PipelineStatusEnum {
}
type Project {
"""
A single Alert Management alert of the project
"""
alertManagementAlert(
"""
IID of the alert. For example, "1"
"""
iid: String
): AlertManagementAlert
"""
Alert Management alerts of the project
"""
alertManagementAlerts(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
IID of the alert. For example, "1"
"""
iid: String
"""
Returns the last _n_ elements from the list.
"""
last: Int
): AlertManagementAlertConnection
"""
Indicates the archived status of the project
"""
......
......@@ -36,6 +36,22 @@ Autogenerated return type of AdminSidekiqQueuesDeleteJobs
| `errors` | String! => Array | Reasons why the mutation failed. |
| `result` | DeleteJobsResponse | Information about the status of the deletion request |
## AlertManagementAlert
Describes an alert from the project's Alert Management
| Name | Type | Description |
| --- | ---- | ---------- |
| `endedAt` | Time | Timestamp the alert ended |
| `eventCount` | Int | Number of events of this alert |
| `iid` | ID! | Internal ID of the alert |
| `monitoringTool` | String | Monitoring tool the alert came from |
| `service` | String | Service the alert came from |
| `severity` | AlertManagementSeverity | Severity of the alert |
| `startedAt` | Time | Timestamp the alert was raised |
| `status` | AlertManagementStatus | Status of the alert |
| `title` | String | Title of the alert |
## AwardEmoji
An emoji awarded by a user.
......@@ -976,6 +992,7 @@ Information about pagination in a connection.
| Name | Type | Description |
| --- | ---- | ---------- |
| `alertManagementAlert` | AlertManagementAlert | A single Alert Management alert of the project |
| `archived` | Boolean | Indicates the archived status of the project |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
| `avatarUrl` | String | URL to avatar image file of the project |
......
......@@ -16,19 +16,32 @@ FactoryBot.define do
end
trait :with_service do
service { FFaker::App.name }
service { FFaker::Product.product_name }
end
trait :with_monitoring_tool do
monitoring_tool { FFaker::App.name }
monitoring_tool { FFaker::AWS.product_description }
end
trait :with_host do
hosts { FFaker::Internet.public_ip_v4_address }
hosts { FFaker::Internet.ip_v4_address }
end
trait :with_ended_at do
ended_at { Time.current }
end
trait :resolved do
status { :resolved }
end
trait :all_fields do
with_issue
with_fingerprint
with_service
with_monitoring_tool
with_host
with_ended_at
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe AlertManagement::AlertsFinder, '#execute' do
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:alert_1) { create(:alert_management_alert, project: project) }
let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
let_it_be(:alert_3) { create(:alert_management_alert) }
let(:params) { {} }
subject { described_class.new(current_user, project, params).execute }
context 'user is not a developer or above' do
it { is_expected.to be_empty }
end
context 'user is developer' do
before do
project.add_developer(current_user)
end
context 'empty params' do
it { is_expected.to contain_exactly(alert_1, alert_2) }
end
context 'iid given' do
let(:params) { { iid: alert_1.iid } }
it { is_expected.to match_array(alert_1) }
context 'unknown iid' do
let(:params) { { iid: 'unknown' } }
it { is_expected.to be_empty }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::AlertManagementAlertResolver do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:alert_1) { create(:alert_management_alert, project: project) }
let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
let_it_be(:alert_other_proj) { create(:alert_management_alert) }
let(:args) { {} }
subject { resolve_alerts(args) }
context 'user does not have permission' do
it { is_expected.to eq(AlertManagement::Alert.none) }
end
context 'user has permission' do
before do
project.add_developer(current_user)
end
it { is_expected.to contain_exactly(alert_1, alert_2) }
context 'finding by iid' do
let(:args) { { iid: alert_1.iid } }
it { is_expected.to contain_exactly(alert_1) }
end
end
private
def resolve_alerts(args = {}, context = { current_user: current_user })
resolve(described_class, obj: project, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['AlertManagementAlert'] do
it { expect(described_class.graphql_name).to eq('AlertManagementAlert') }
it { expect(described_class).to require_graphql_authorizations(:read_alert_management_alerts) }
it 'exposes the expected fields' do
expected_fields = %i[
iid
title
severity
status
service
monitoring_tool
started_at
ended_at
event_count
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['AlertManagementSeverity'] do
it { expect(described_class.graphql_name).to eq('AlertManagementSeverity') }
it 'exposes all the severity values' do
expect(described_class.values.keys).to include(*%w[CRITICAL HIGH MEDIUM LOW INFO UNKNOWN])
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['AlertManagementStatus'] do
it { expect(described_class.graphql_name).to eq('AlertManagementStatus') }
it 'exposes all the severity values' do
expect(described_class.values.keys).to include(*%w[TRIGGERED ACKNOWLEDGED RESOLVED IGNORED])
end
end
......@@ -116,4 +116,14 @@ describe AlertManagement::Alert do
end
end
end
describe '.for_iid' do
let_it_be(:project) { create(:project) }
let_it_be(:alert_1) { create(:alert_management_alert, project: project) }
let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
subject { AlertManagement::Alert.for_iid(alert_1.iid) }
it { is_expected.to match_array(alert_1) }
end
end
# frozen_string_literal: true
require 'spec_helper'
describe AlertManagement::AlertPolicy, :models do
let(:alert) { create(:alert_management_alert) }
let(:project) { alert.project }
let(:user) { create(:user) }
let(:policy) { described_class.new(user, alert) }
describe 'rules' do
it { expect(policy).to be_disallowed :read_alert_management_alerts }
context 'when developer' do
before do
project.add_developer(user)
end
it { expect(policy).to be_allowed :read_alert_management_alerts }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'getting Alert Management Alerts' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be(:alert_1) { create(:alert_management_alert, :all_fields, project: project) }
let_it_be(:alert_2) { create(:alert_management_alert, :all_fields, project: project) }
let(:fields) do
<<~QUERY
nodes {
#{all_graphql_fields_for('AlertManagementAlert'.classify)}
}
QUERY
end
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('alertManagementAlerts', {}, fields)
)
end
context 'with alert data' do
let(:alerts) { graphql_data.dig('project', 'alertManagementAlerts', 'nodes') }
context 'without project permissions' do
let(:user) { create(:user) }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it { expect(alerts).to be nil }
end
context 'with project permissions' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
let(:first_alert) { alerts.first }
it_behaves_like 'a working graphql query'
it { expect(alerts.size).to eq(2) }
it 'returns the correct properties of the alert' do
expect(first_alert).to include(
'iid' => alert_2.iid.to_s,
'title' => alert_2.title,
'severity' => alert_2.severity.upcase,
'status' => alert_2.status.upcase,
'monitoringTool' => alert_2.monitoring_tool,
'service' => alert_2.service,
'eventCount' => alert_2.events,
'startedAt' => alert_2.started_at.strftime('%Y-%m-%dT%H:%M:%SZ'),
'endedAt' => alert_2.ended_at.strftime('%Y-%m-%dT%H:%M:%SZ')
)
end
context 'with iid given' do
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('alertManagementAlerts', { iid: alert_1.iid.to_s }, fields)
)
end
it_behaves_like 'a working graphql query'
it { expect(alerts.size).to eq(1) }
it { expect(first_alert['iid']).to eq(alert_1.iid.to_s) }
end
end
end
end
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