Commit 2da5c414 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch 'mwaw/211330-add-metrics-dashboard-graphql-resource' into 'master'

Add metrics dashboard graphql resource

See merge request gitlab-org/gitlab!29112
parents ee90a212 a74a03d4
# frozen_string_literal: true
module Resolvers
module Metrics
class DashboardResolver < Resolvers::BaseResolver
argument :path, GraphQL::STRING_TYPE,
required: true,
description: "Path to a file which defines metrics dashboard eg: 'config/prometheus/common_metrics.yml'"
type Types::Metrics::DashboardType, null: true
alias_method :environment, :object
def resolve(**args)
return unless environment
::PerformanceMonitoring::PrometheusDashboard.find_for(project: environment.project, user: context[:current_user], path: args[:path], options: { environment: environment })
end
end
end
end
...@@ -15,5 +15,9 @@ module Types ...@@ -15,5 +15,9 @@ module Types
field :state, GraphQL::STRING_TYPE, null: false, field :state, GraphQL::STRING_TYPE, null: false,
description: 'State of the environment, for example: available/stopped' description: 'State of the environment, for example: available/stopped'
field :metrics_dashboard, Types::Metrics::DashboardType, null: true,
description: 'Metrics dashboard schema for the environment',
resolver: Resolvers::Metrics::DashboardResolver
end end
end end
# frozen_string_literal: true
module Types
module Metrics
# rubocop: disable Graphql/AuthorizeTypes
# Authorization is performed at environment level
class DashboardType < ::Types::BaseObject
graphql_name 'MetricsDashboard'
field :path, GraphQL::STRING_TYPE, null: true,
description: 'Path to a file with the dashboard definition'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
...@@ -4,27 +4,41 @@ module PerformanceMonitoring ...@@ -4,27 +4,41 @@ module PerformanceMonitoring
class PrometheusDashboard class PrometheusDashboard
include ActiveModel::Model include ActiveModel::Model
attr_accessor :dashboard, :panel_groups attr_accessor :dashboard, :panel_groups, :path, :environment, :priority
validates :dashboard, presence: true validates :dashboard, presence: true
validates :panel_groups, presence: true validates :panel_groups, presence: true
def self.from_json(json_content) class << self
dashboard = new( def from_json(json_content)
dashboard: json_content['dashboard'], dashboard = new(
panel_groups: json_content['panel_groups'].map { |group| PrometheusPanelGroup.from_json(group) } dashboard: json_content['dashboard'],
) panel_groups: json_content['panel_groups'].map { |group| PrometheusPanelGroup.from_json(group) }
)
dashboard.tap(&:validate!)
dashboard.tap(&:validate!)
end
def find_for(project:, user:, path:, options: {})
dashboard_response = Gitlab::Metrics::Dashboard::Finder.find(project, user, options.merge(dashboard_path: path))
return unless dashboard_response[:status] == :success
new(
{
path: path,
environment: options[:environment]
}.merge(dashboard_response[:dashboard])
)
end
end end
def to_yaml def to_yaml
self.as_json(only: valid_attributes).to_yaml self.as_json(only: yaml_valid_attributes).to_yaml
end end
private private
def valid_attributes def yaml_valid_attributes
%w(panel_groups panels metrics group priority type title y_label weight id unit label query query_range dashboard) %w(panel_groups panels metrics group priority type title y_label weight id unit label query query_range dashboard)
end end
end end
......
---
title: Add graphQL interface to fetch metrics dashboard
merge_request: 29112
author:
type: added
...@@ -1882,6 +1882,16 @@ type Environment { ...@@ -1882,6 +1882,16 @@ type Environment {
""" """
id: ID! id: ID!
"""
Metrics dashboard schema for the environment
"""
metricsDashboard(
"""
Path to a file which defines metrics dashboard eg: 'config/prometheus/common_metrics.yml'
"""
path: String!
): MetricsDashboard
""" """
Human-readable name of the environment Human-readable name of the environment
""" """
...@@ -5278,6 +5288,13 @@ type Metadata { ...@@ -5278,6 +5288,13 @@ type Metadata {
version: String! version: String!
} }
type MetricsDashboard {
"""
Path to a file with the dashboard definition
"""
path: String
}
""" """
Represents a milestone. Represents a milestone.
""" """
......
...@@ -5580,6 +5580,33 @@ ...@@ -5580,6 +5580,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "metricsDashboard",
"description": "Metrics dashboard schema for the environment",
"args": [
{
"name": "path",
"description": "Path to a file which defines metrics dashboard eg: 'config/prometheus/common_metrics.yml'",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "MetricsDashboard",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "name", "name": "name",
"description": "Human-readable name of the environment", "description": "Human-readable name of the environment",
...@@ -15117,6 +15144,33 @@ ...@@ -15117,6 +15144,33 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "MetricsDashboard",
"description": null,
"fields": [
{
"name": "path",
"description": "Path to a file with the dashboard definition",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Milestone", "name": "Milestone",
......
...@@ -324,6 +324,7 @@ Describes where code is deployed for a project ...@@ -324,6 +324,7 @@ Describes where code is deployed for a project
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `id` | ID! | ID of the environment | | `id` | ID! | ID of the environment |
| `metricsDashboard` | MetricsDashboard | Metrics dashboard schema for the environment |
| `name` | String! | Human-readable name of the environment | | `name` | String! | Human-readable name of the environment |
| `state` | String! | State of the environment, for example: available/stopped | | `state` | String! | State of the environment, for example: available/stopped |
...@@ -815,6 +816,12 @@ Autogenerated return type of MergeRequestSetWip ...@@ -815,6 +816,12 @@ Autogenerated return type of MergeRequestSetWip
| `revision` | String! | Revision | | `revision` | String! | Revision |
| `version` | String! | Version | | `version` | String! | Version |
## MetricsDashboard
| Name | Type | Description |
| --- | ---- | ---------- |
| `path` | String | Path to a file with the dashboard definition |
## Milestone ## Milestone
Represents a milestone. Represents a milestone.
......
...@@ -55,15 +55,43 @@ describe PerformanceMonitoring::PrometheusDashboard do ...@@ -55,15 +55,43 @@ describe PerformanceMonitoring::PrometheusDashboard do
end end
end end
describe '.find_for' do
let(:project) { build_stubbed(:project) }
let(:user) { build_stubbed(:user) }
let(:environment) { build_stubbed(:environment) }
let(:path) { ::Metrics::Dashboard::SystemDashboardService::DASHBOARD_PATH }
context 'dashboard has been found' do
it 'uses dashboard finder to find and load dashboard data and returns dashboard instance', :aggregate_failures do
expect(Gitlab::Metrics::Dashboard::Finder).to receive(:find).with(project, user, environment: environment, dashboard_path: path).and_return(status: :success, dashboard: json_content)
dashboard_instance = described_class.find_for(project: project, user: user, path: path, options: { environment: environment })
expect(dashboard_instance).to be_instance_of described_class
expect(dashboard_instance.environment).to be environment
expect(dashboard_instance.path).to be path
end
end
context 'dashboard has NOT been found' do
it 'returns nil' do
allow(Gitlab::Metrics::Dashboard::Finder).to receive(:find).and_return(status: :error)
dashboard_instance = described_class.find_for(project: project, user: user, path: path, options: { environment: environment })
expect(dashboard_instance).to be_nil
end
end
end
describe '#to_yaml' do describe '#to_yaml' do
subject { prometheus_dashboard.to_yaml }
let(:prometheus_dashboard) { described_class.from_json(json_content) }
let(:expected_yaml) do let(:expected_yaml) do
"---\npanel_groups:\n- panels:\n - metrics:\n - id: metric_of_ages\n unit: count\n label: Metric of Ages\n query: \n query_range: http_requests_total\n type: area-chart\n title: Chart Title\n y_label: Y-Axis\n weight: \n group: Group Title\n priority: \ndashboard: Dashboard Title\n" "---\npanel_groups:\n- panels:\n - metrics:\n - id: metric_of_ages\n unit: count\n label: Metric of Ages\n query: \n query_range: http_requests_total\n type: area-chart\n title: Chart Title\n y_label: Y-Axis\n weight: \n group: Group Title\n priority: \ndashboard: Dashboard Title\n"
end end
let(:prometheus_dashboard) { described_class.from_json(json_content) }
let(:subject) { prometheus_dashboard.to_yaml }
it { is_expected.to eq(expected_yaml) } it { is_expected.to eq(expected_yaml) }
end end
end end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::Metrics::DashboardResolver do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
describe '#resolve' do
subject(:resolve_dashboard) { resolve(described_class, obj: parent_object, args: args, ctx: { current_user: current_user }) }
let(:args) do
{
path: 'config/prometheus/common_metrics.yml'
}
end
context 'for environment' do
let(:project) { create(:project) }
let(:parent_object) { create(:environment, project: project) }
before do
project.add_developer(current_user)
end
it 'use ActiveModel class to find matching dashboard', :aggregate_failures do
expected_arguments = { project: project, user: current_user, path: args[:path], options: { environment: parent_object } }
expect(PerformanceMonitoring::PrometheusDashboard).to receive(:find_for).with(expected_arguments).and_return(PerformanceMonitoring::PrometheusDashboard.new)
expect(resolve_dashboard).to be_instance_of PerformanceMonitoring::PrometheusDashboard
end
context 'without parent object' do
let(:parent_object) { nil }
it 'returns nil', :aggregate_failures do
expect(PerformanceMonitoring::PrometheusDashboard).not_to receive(:find_for)
expect(resolve_dashboard).to be_nil
end
end
end
end
end
...@@ -7,7 +7,7 @@ describe GitlabSchema.types['Environment'] do ...@@ -7,7 +7,7 @@ describe GitlabSchema.types['Environment'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
name id state name id state metrics_dashboard
] ]
expect(described_class).to have_graphql_fields(*expected_fields) expect(described_class).to have_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['MetricsDashboard'] do
it { expect(described_class.graphql_name).to eq('MetricsDashboard') }
it 'has the expected fields' do
expected_fields = %w[
path
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Getting Metrics Dashboard' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let(:project) { create(:project) }
let!(:environment) { create(:environment, project: project) }
let(:fields) do
<<~QUERY
#{all_graphql_fields_for('MetricsDashboard'.classify)}
QUERY
end
let(:query) do
%(
query {
project(fullPath:"#{project.full_path}") {
environments(name: "#{environment.name}") {
nodes {
metricsDashboard(path: "#{path}"){
#{fields}
}
}
}
}
}
)
end
context 'for anonymous user' do
before do
post_graphql(query, current_user: current_user)
end
context 'requested dashboard is available' do
let(:path) { 'config/prometheus/common_metrics.yml' }
it_behaves_like 'a working graphql query'
it 'returns nil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')
expect(dashboard).to be_nil
end
end
end
context 'for user with developer access' do
before do
project.add_developer(current_user)
post_graphql(query, current_user: current_user)
end
context 'requested dashboard is available' do
let(:path) { 'config/prometheus/common_metrics.yml' }
it_behaves_like 'a working graphql query'
it 'returns metrics dashboard' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
expect(dashboard).to eql("path" => path)
end
end
context 'requested dashboard can not be found' do
let(:path) { 'config/prometheus/i_am_not_here.yml' }
it_behaves_like 'a working graphql query'
it 'return snil' do
dashboard = graphql_data.dig('project', 'environments', 'nodes')[0]['metricsDashboard']
expect(dashboard).to be_nil
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