Commit 7adc64a2 authored by Igor Drozdov's avatar Igor Drozdov

Merge branch 'mwaw/211330-backend-expose-an-endpoint-to-fetch-all-annotations' into 'master'

Resolve "Backend: Expose an endpoint to fetch all annotations"

See merge request gitlab-org/gitlab!28550
parents c0ced540 dfe97a68
# frozen_string_literal: true
module Resolvers
module Metrics
module Dashboards
class AnnotationResolver < Resolvers::BaseResolver
argument :from, Types::TimeType,
required: true,
description: "Timestamp marking date and time from which annotations need to be fetched"
argument :to, Types::TimeType,
required: false,
description: "Timestamp marking date and time to which annotations need to be fetched"
type Types::Metrics::Dashboards::AnnotationType, null: true
alias_method :dashboard, :object
def resolve(**args)
return [] unless dashboard
return [] unless Feature.enabled?(:metrics_dashboard_annotations, dashboard.environment&.project)
::Metrics::Dashboards::AnnotationsFinder.new(dashboard: dashboard, params: args).execute
end
end
end
end
end
...@@ -9,6 +9,11 @@ module Types ...@@ -9,6 +9,11 @@ module Types
field :path, GraphQL::STRING_TYPE, null: true, field :path, GraphQL::STRING_TYPE, null: true,
description: 'Path to a file with the dashboard definition' description: 'Path to a file with the dashboard definition'
field :annotations, Types::Metrics::Dashboards::AnnotationType.connection_type, null: true,
description: 'Annotations added to the dashboard. Will always return `null` ' \
'if `metrics_dashboard_annotations` feature flag is disabled',
resolver: Resolvers::Metrics::Dashboards::AnnotationResolver
end end
# rubocop: enable Graphql/AuthorizeTypes # rubocop: enable Graphql/AuthorizeTypes
end end
......
# frozen_string_literal: true
module Types
module Metrics
module Dashboards
class AnnotationType < ::Types::BaseObject
authorize :read_metrics_dashboard_annotation
graphql_name 'MetricsDashboardAnnotation'
field :description, GraphQL::STRING_TYPE, null: true,
description: 'Description of the annotation'
field :id, GraphQL::ID_TYPE, null: false,
description: 'ID of the annotation'
field :panel_id, GraphQL::STRING_TYPE, null: true,
description: 'ID of a dashboard panel to which the annotation should be scoped'
field :starting_at, GraphQL::STRING_TYPE, null: true,
description: 'Timestamp marking start of annotated time span'
field :ending_at, GraphQL::STRING_TYPE, null: true,
description: 'Timestamp marking end of annotated time span'
def panel_id
object.panel_xid
end
end
end
end
end
---
title: Add metrics dashboard annotations to GraphQL API
merge_request: 28550
author:
type: added
...@@ -5334,12 +5334,109 @@ type Metadata { ...@@ -5334,12 +5334,109 @@ type Metadata {
} }
type MetricsDashboard { type MetricsDashboard {
"""
Annotations added to the dashboard. Will always return `null` if `metrics_dashboard_annotations` feature flag is disabled
"""
annotations(
"""
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
"""
Timestamp marking date and time from which annotations need to be fetched
"""
from: Time!
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Timestamp marking date and time to which annotations need to be fetched
"""
to: Time
): MetricsDashboardAnnotationConnection
""" """
Path to a file with the dashboard definition Path to a file with the dashboard definition
""" """
path: String path: String
} }
type MetricsDashboardAnnotation {
"""
Description of the annotation
"""
description: String
"""
Timestamp marking end of annotated time span
"""
endingAt: String
"""
ID of the annotation
"""
id: ID!
"""
ID of a dashboard panel to which the annotation should be scoped
"""
panelId: String
"""
Timestamp marking start of annotated time span
"""
startingAt: String
}
"""
The connection type for MetricsDashboardAnnotation.
"""
type MetricsDashboardAnnotationConnection {
"""
A list of edges.
"""
edges: [MetricsDashboardAnnotationEdge]
"""
A list of nodes.
"""
nodes: [MetricsDashboardAnnotation]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type MetricsDashboardAnnotationEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: MetricsDashboardAnnotation
}
""" """
Represents a milestone. Represents a milestone.
""" """
......
...@@ -15279,6 +15279,83 @@ ...@@ -15279,6 +15279,83 @@
"name": "MetricsDashboard", "name": "MetricsDashboard",
"description": null, "description": null,
"fields": [ "fields": [
{
"name": "annotations",
"description": "Annotations added to the dashboard. Will always return `null` if `metrics_dashboard_annotations` feature flag is disabled",
"args": [
{
"name": "from",
"description": "Timestamp marking date and time from which annotations need to be fetched",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "to",
"description": "Timestamp marking date and time to which annotations need to be fetched",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "MetricsDashboardAnnotationConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "path", "name": "path",
"description": "Path to a file with the dashboard definition", "description": "Path to a file with the dashboard definition",
...@@ -15301,6 +15378,205 @@ ...@@ -15301,6 +15378,205 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "MetricsDashboardAnnotation",
"description": null,
"fields": [
{
"name": "description",
"description": "Description of the annotation",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "endingAt",
"description": "Timestamp marking end of annotated time span",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the annotation",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "panelId",
"description": "ID of a dashboard panel to which the annotation should be scoped",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "startingAt",
"description": "Timestamp marking start of annotated time span",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "MetricsDashboardAnnotationConnection",
"description": "The connection type for MetricsDashboardAnnotation.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "MetricsDashboardAnnotationEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "MetricsDashboardAnnotation",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "MetricsDashboardAnnotationEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "MetricsDashboardAnnotation",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Milestone", "name": "Milestone",
......
...@@ -833,6 +833,16 @@ Autogenerated return type of MergeRequestSetWip ...@@ -833,6 +833,16 @@ Autogenerated return type of MergeRequestSetWip
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `path` | String | Path to a file with the dashboard definition | | `path` | String | Path to a file with the dashboard definition |
## MetricsDashboardAnnotation
| Name | Type | Description |
| --- | ---- | ---------- |
| `description` | String | Description of the annotation |
| `endingAt` | String | Timestamp marking end of annotated time span |
| `id` | ID! | ID of the annotation |
| `panelId` | String | ID of a dashboard panel to which the annotation should be scoped |
| `startingAt` | String | Timestamp marking start of annotated time span |
## Milestone ## Milestone
Represents a milestone. Represents a milestone.
......
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::Metrics::Dashboards::AnnotationResolver do
include GraphqlHelpers
describe '#resolve' do
context 'user with developer access' do
subject(:resolve_annotations) { resolve(described_class, obj: dashboard, args: args, ctx: { current_user: current_user }) }
let_it_be(:current_user) { create(:user) }
let_it_be(:environment) { create(:environment) }
let_it_be(:path) { 'config/prometheus/common_metrics.yml' }
let(:dashboard) { PerformanceMonitoring::PrometheusDashboard.new(path: path, environment: environment) }
let(:args) do
{
from: 10.minutes.ago,
to: 5.minutes.ago
}
end
before_all do
environment.project.add_developer(current_user)
end
context 'with annotation records' do
let_it_be(:annotation_1) { create(:metrics_dashboard_annotation, environment: environment, starting_at: 9.minutes.ago, dashboard_path: path) }
it 'loads annotations with usage of finder class', :aggregate_failures do
expect_next_instance_of(::Metrics::Dashboards::AnnotationsFinder, dashboard: dashboard, params: args) do |finder|
expect(finder).to receive(:execute).and_return [annotation_1]
end
expect(resolve_annotations).to eql [annotation_1]
end
context 'dashboard is missing' do
let(:dashboard) { nil }
it 'returns empty array', :aggregate_failures do
expect(::Metrics::Dashboards::AnnotationsFinder).not_to receive(:new)
expect(resolve_annotations).to be_empty
end
end
context 'there are no annotations records' do
it 'returns empty array' do
allow_next_instance_of(::Metrics::Dashboards::AnnotationsFinder) do |finder|
allow(finder).to receive(:execute).and_return []
end
expect(resolve_annotations).to be_empty
end
end
end
end
end
end
...@@ -7,9 +7,16 @@ describe GitlabSchema.types['MetricsDashboard'] do ...@@ -7,9 +7,16 @@ describe GitlabSchema.types['MetricsDashboard'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
path path annotations
] ]
expect(described_class).to have_graphql_fields(*expected_fields) expect(described_class).to have_graphql_fields(*expected_fields)
end end
describe 'annotations field' do
subject { described_class.fields['annotations'] }
it { is_expected.to have_graphql_type(Types::Metrics::Dashboards::AnnotationType.connection_type) }
it { is_expected.to have_graphql_resolver(Resolvers::Metrics::Dashboards::AnnotationResolver) }
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['MetricsDashboardAnnotation'] do
it { expect(described_class.graphql_name).to eq('MetricsDashboardAnnotation') }
it 'has the expected fields' do
expected_fields = %w[
description id panel_id starting_at ending_at
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
it { expect(described_class).to require_graphql_authorizations(:read_metrics_dashboard_annotation) }
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Getting Metrics Dashboard Annotations' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:environment) { create(:environment, project: project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:path) { 'config/prometheus/common_metrics.yml' }
let_it_be(:from) { "2020-04-01T03:29:25Z" }
let_it_be(:to) { Time.zone.now.advance(minutes: 5) }
let_it_be(:annotation) { create(:metrics_dashboard_annotation, environment: environment, dashboard_path: path) }
let_it_be(:annotation_for_different_env) { create(:metrics_dashboard_annotation, dashboard_path: path) }
let_it_be(:annotation_for_different_dashboard) { create(:metrics_dashboard_annotation, environment: environment, dashboard_path: ".gitlab/dashboards/test.yml") }
let_it_be(:to_old_annotation) do
create(:metrics_dashboard_annotation, environment: environment, starting_at: Time.parse(from).advance(minutes: -5), dashboard_path: path)
end
let_it_be(:to_new_annotation) do
create(:metrics_dashboard_annotation, environment: environment, starting_at: to.advance(minutes: 5), dashboard_path: path)
end
let(:fields) do
<<~QUERY
#{all_graphql_fields_for('MetricsDashboardAnnotation'.classify)}
QUERY
end
let(:query) do
%(
query {
project(fullPath:"#{project.full_path}") {
environments(name: "#{environment.name}") {
nodes {
metricsDashboard(path: "#{path}"){
annotations(#{args}){
nodes {
#{fields}
}
}
}
}
}
}
}
)
end
context 'feature flag metrics_dashboard_annotations' do
let(:args) { "from: \"#{from}\", to: \"#{to}\"" }
before do
project.add_developer(current_user)
end
context 'is off' do
before do
stub_feature_flags(metrics_dashboard_annotations: false)
post_graphql(query, current_user: current_user)
end
it 'returns empty nodes array' do
annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes')
expect(annotations).to be_empty
end
end
context 'is on' do
before do
stub_feature_flags(metrics_dashboard_annotations: true)
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it 'returns annotations' do
annotations = graphql_data.dig('project', 'environments', 'nodes')[0].dig('metricsDashboard', 'annotations', 'nodes')
expect(annotations).to match_array [{
"description" => annotation.description,
"id" => annotation.to_global_id.to_s,
"panelId" => annotation.panel_xid,
"startingAt" => annotation.starting_at.to_s,
"endingAt" => nil
}]
end
context 'arguments' do
context 'from is missing' do
let(:args) { "to: \"#{from}\"" }
it 'returns error' do
post_graphql(query, current_user: current_user)
expect(graphql_errors[0]).to include("message" => "Field 'annotations' is missing required arguments: from")
end
end
context 'to is missing' do
let(:args) { "from: \"#{from}\"" }
it_behaves_like 'a working graphql query'
end
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