Commit dfe97a68 authored by Mikołaj Wawrzyniak's avatar Mikołaj Wawrzyniak Committed by Igor Drozdov

Add Metrics::Dashboard::Annotation to grapQL api

In order to allow users to fetch their annotations made to different
metrics dashboards at gitlab, we need to provide grapqhl type and
resolver for that.
parent 0a0077c3
# 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
field :path, GraphQL::STRING_TYPE, null: true,
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
# rubocop: enable Graphql/AuthorizeTypes
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 {
}
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: 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.
"""
......
......@@ -15279,6 +15279,83 @@
"name": "MetricsDashboard",
"description": null,
"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",
"description": "Path to a file with the dashboard definition",
......@@ -15301,6 +15378,205 @@
"enumValues": 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",
"name": "Milestone",
......
......@@ -833,6 +833,16 @@ Autogenerated return type of MergeRequestSetWip
| --- | ---- | ---------- |
| `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
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
it 'has the expected fields' do
expected_fields = %w[
path
]
path annotations
]
expect(described_class).to have_graphql_fields(*expected_fields)
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
# 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