diff options
author | Grzegorz Bizon <grzegorz@gitlab.com> | 2017-06-22 16:35:05 +0000 |
---|---|---|
committer | Grzegorz Bizon <grzegorz@gitlab.com> | 2017-06-22 16:35:05 +0000 |
commit | 8fe5602c1a4c3ca1815d31e6845dd3d3c2895ec9 (patch) | |
tree | ad77c8644ac9e237429193148deb380423be572a /spec | |
parent | 3bbf0898c1aae90fdd030e41019b9c7b1dbb6215 (diff) | |
parent | 97c42df3b804a37e659c3cda6bd8a52570f31366 (diff) | |
download | gitlab-ce-8fe5602c1a4c3ca1815d31e6845dd3d3c2895ec9.tar.gz |
Merge branch '28717-additional-metrics-review-branch' into 'master'
Support additional prometheus metrics - review branch
Closes #28717
See merge request !11712
Diffstat (limited to 'spec')
21 files changed, 1271 insertions, 6 deletions
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 3f99e2ff596..a2720c9b81e 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -99,6 +99,36 @@ describe ApplicationController do end end + describe 'response format' do + controller(described_class) do + def index + respond_to do |format| + format.json do + head :ok + end + end + end + end + + context 'when format is handled' do + let(:requested_format) { :json } + + it 'returns 200 response' do + get :index, private_token: user.private_token, format: requested_format + + expect(response).to have_http_status 200 + end + end + + context 'when format is not handled' do + it 'returns 404 response' do + get :index, private_token: user.private_token + + expect(response).to have_http_status 404 + end + end + end + describe '#authenticate_user_from_rss_token' do describe "authenticating a user from an RSS token" do controller(described_class) do diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb index 4c69443314d..0dbfcf97f6f 100644 --- a/spec/controllers/projects/deployments_controller_spec.rb +++ b/spec/controllers/projects/deployments_controller_spec.rb @@ -42,6 +42,7 @@ describe Projects::DeploymentsController do before do allow(controller).to receive(:deployment).and_return(deployment) end + context 'when metrics are disabled' do before do allow(deployment).to receive(:has_metrics?).and_return false @@ -108,6 +109,69 @@ describe Projects::DeploymentsController do end end + describe 'GET #additional_metrics' do + let(:deployment) { create(:deployment, project: project, environment: environment) } + + before do + allow(controller).to receive(:deployment).and_return(deployment) + end + + context 'when metrics are disabled' do + before do + allow(deployment).to receive(:has_metrics?).and_return false + end + + it 'responds with not found' do + get :metrics, deployment_params(id: deployment.id) + + expect(response).to be_not_found + end + end + + context 'when metrics are enabled' do + let(:prometheus_service) { double('prometheus_service') } + + before do + allow(deployment.project).to receive(:prometheus_service).and_return(prometheus_service) + end + + context 'when environment has no metrics' do + before do + expect(deployment).to receive(:additional_metrics).and_return({}) + end + + it 'returns a empty response 204 response' do + get :additional_metrics, deployment_params(id: deployment.id, format: :json) + expect(response).to have_http_status(204) + expect(response.body).to eq('') + end + end + + context 'when environment has some metrics' do + let(:empty_metrics) do + { + success: true, + metrics: {}, + last_update: 42 + } + end + + before do + expect(deployment).to receive(:additional_metrics).and_return(empty_metrics) + end + + it 'returns a metrics JSON document' do + get :additional_metrics, deployment_params(id: deployment.id, format: :json) + + expect(response).to be_ok + expect(json_response['success']).to be(true) + expect(json_response['metrics']).to eq({}) + expect(json_response['last_update']).to eq(42) + end + end + end + end + def deployment_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project, diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb index 9ec3c53174e..ad0b046742d 100644 --- a/spec/controllers/projects/environments_controller_spec.rb +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -318,6 +318,48 @@ describe Projects::EnvironmentsController do end end + describe 'GET #additional_metrics' do + before do + allow(controller).to receive(:environment).and_return(environment) + end + + context 'when environment has no metrics' do + before do + expect(environment).to receive(:additional_metrics).and_return(nil) + end + + context 'when requesting metrics as JSON' do + it 'returns a metrics JSON document' do + get :additional_metrics, environment_params(format: :json) + + expect(response).to have_http_status(204) + expect(json_response).to eq({}) + end + end + end + + context 'when environment has some metrics' do + before do + expect(environment) + .to receive(:additional_metrics) + .and_return({ + success: true, + data: {}, + last_update: 42 + }) + end + + it 'returns a metrics JSON document' do + get :additional_metrics, environment_params(format: :json) + + expect(response).to be_ok + expect(json_response['success']).to be(true) + expect(json_response['data']).to eq({}) + expect(json_response['last_update']).to eq(42) + end + end + end + def environment_params(opts = {}) opts.reverse_merge(namespace_id: project.namespace, project_id: project, diff --git a/spec/controllers/projects/prometheus_controller_spec.rb b/spec/controllers/projects/prometheus_controller_spec.rb new file mode 100644 index 00000000000..eddf7275975 --- /dev/null +++ b/spec/controllers/projects/prometheus_controller_spec.rb @@ -0,0 +1,59 @@ +require('spec_helper') + +describe Projects::PrometheusController do + let(:user) { create(:user) } + let!(:project) { create(:empty_project) } + + let(:prometheus_service) { double('prometheus_service') } + + before do + allow(controller).to receive(:project).and_return(project) + allow(project).to receive(:prometheus_service).and_return(prometheus_service) + + project.add_master(user) + sign_in(user) + end + + describe 'GET #active_metrics' do + context 'when prometheus metrics are enabled' do + context 'when data is not present' do + before do + allow(prometheus_service).to receive(:matched_metrics).and_return({}) + end + + it 'returns no content response' do + get :active_metrics, project_params(format: :json) + + expect(response).to have_http_status(204) + end + end + + context 'when data is available' do + let(:sample_response) { { some_data: 1 } } + + before do + allow(prometheus_service).to receive(:matched_metrics).and_return(sample_response) + end + + it 'returns no content response' do + get :active_metrics, project_params(format: :json) + + expect(response).to have_http_status(200) + expect(json_response).to eq(sample_response.deep_stringify_keys) + end + end + + context 'when requesting non json response' do + it 'returns not found response' do + get :active_metrics, project_params + + expect(response).to have_http_status(404) + end + end + end + end + + def project_params(opts = {}) + opts.reverse_merge(namespace_id: project.namespace, project_id: project) + end +end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index e7366a7fd1c..30bc25cf88a 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -25,6 +25,14 @@ FactoryGirl.define do }) end + factory :prometheus_service do + project factory: :empty_project + active true + properties({ + api_url: 'https://prometheus.example.com/' + }) + end + factory :jira_service do project factory: :empty_project active true diff --git a/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json b/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json new file mode 100644 index 00000000000..47b5d283b8c --- /dev/null +++ b/spec/fixtures/api/schemas/prometheus/additional_metrics_query_result.json @@ -0,0 +1,58 @@ +{ + "items": { + "properties": { + "group": { + "type": "string" + }, + "metrics": { + "items": { + "properties": { + "queries": { + "items": { + "properties": { + "query_range": { + "type": "string" + }, + "query": { + "type": "string" + }, + "result": { + "type": "any" + } + }, + "type": "object" + }, + "type": "array" + }, + "title": { + "type": "string" + }, + "weight": { + "type": "integer" + }, + "y_label": { + "type": "string" + } + }, + "type": "object" + }, + "required": [ + "metrics", + "title", + "weight" + ], + "type": "array" + }, + "priority": { + "type": "integer" + } + }, + "type": "object" + }, + "required": [ + "group", + "priority", + "metrics" + ], + "type": "array" +}
\ No newline at end of file diff --git a/spec/javascripts/fixtures/prometheus_service.rb b/spec/javascripts/fixtures/prometheus_service.rb new file mode 100644 index 00000000000..3200577b326 --- /dev/null +++ b/spec/javascripts/fixtures/prometheus_service.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, name: 'frontend-fixtures' )} + let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') } + let!(:service) { create(:prometheus_service, project: project) } + + render_views + + before(:all) do + clean_frontend_fixtures('services/prometheus') + end + + before(:each) do + sign_in(admin) + end + + it 'services/prometheus/prometheus_service.html.raw' do |example| + get :edit, + namespace_id: namespace, + project_id: project, + id: service.to_param + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/prometheus_metrics/mock_data.js b/spec/javascripts/prometheus_metrics/mock_data.js new file mode 100644 index 00000000000..3af56df92e2 --- /dev/null +++ b/spec/javascripts/prometheus_metrics/mock_data.js @@ -0,0 +1,41 @@ +export const metrics = [ + { + group: 'Kubernetes', + priority: 1, + active_metrics: 4, + metrics_missing_requirements: 0, + }, + { + group: 'HAProxy', + priority: 2, + active_metrics: 3, + metrics_missing_requirements: 0, + }, + { + group: 'Apache', + priority: 3, + active_metrics: 5, + metrics_missing_requirements: 0, + }, +]; + +export const missingVarMetrics = [ + { + group: 'Kubernetes', + priority: 1, + active_metrics: 4, + metrics_missing_requirements: 0, + }, + { + group: 'HAProxy', + priority: 2, + active_metrics: 3, + metrics_missing_requirements: 1, + }, + { + group: 'Apache', + priority: 3, + active_metrics: 5, + metrics_missing_requirements: 3, + }, +]; diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js new file mode 100644 index 00000000000..2b3a821dbd9 --- /dev/null +++ b/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js @@ -0,0 +1,158 @@ +import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics'; +import PANEL_STATE from '~/prometheus_metrics/constants'; +import { metrics, missingVarMetrics } from './mock_data'; + +describe('PrometheusMetrics', () => { + const FIXTURE = 'services/prometheus/prometheus_service.html.raw'; + preloadFixtures(FIXTURE); + + beforeEach(() => { + loadFixtures(FIXTURE); + }); + + describe('constructor', () => { + let prometheusMetrics; + + beforeEach(() => { + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + }); + + it('should initialize wrapper element refs on class object', () => { + expect(prometheusMetrics.$wrapper).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsPanel).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsCount).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsLoading).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsEmpty).toBeDefined(); + expect(prometheusMetrics.$monitoredMetricsList).toBeDefined(); + expect(prometheusMetrics.$missingEnvVarPanel).toBeDefined(); + expect(prometheusMetrics.$panelToggle).toBeDefined(); + expect(prometheusMetrics.$missingEnvVarMetricCount).toBeDefined(); + expect(prometheusMetrics.$missingEnvVarMetricsList).toBeDefined(); + }); + + it('should initialize metadata on class object', () => { + expect(prometheusMetrics.backOffRequestCounter).toEqual(0); + expect(prometheusMetrics.activeMetricsEndpoint).toContain('/test'); + }); + }); + + describe('showMonitoringMetricsPanelState', () => { + let prometheusMetrics; + + beforeEach(() => { + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + }); + + it('should show loading state when called with `loading`', () => { + prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy(); + }); + + it('should show metrics list when called with `list`', () => { + prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.LIST); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy(); + }); + + it('should show empty state when called with `empty`', () => { + prometheusMetrics.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeTruthy(); + }); + }); + + describe('populateActiveMetrics', () => { + let prometheusMetrics; + + beforeEach(() => { + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + }); + + it('should show monitored metrics list', () => { + prometheusMetrics.populateActiveMetrics(metrics); + + const $metricsListLi = prometheusMetrics.$monitoredMetricsList.find('li'); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsList.hasClass('hidden')).toBeFalsy(); + + expect(prometheusMetrics.$monitoredMetricsCount.text()).toEqual('12'); + expect($metricsListLi.length).toEqual(metrics.length); + expect($metricsListLi.first().find('.badge').text()).toEqual(`${metrics[0].active_metrics}`); + }); + + it('should show missing environment variables list', () => { + prometheusMetrics.populateActiveMetrics(missingVarMetrics); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$missingEnvVarPanel.hasClass('hidden')).toBeFalsy(); + + expect(prometheusMetrics.$missingEnvVarMetricCount.text()).toEqual('2'); + expect(prometheusMetrics.$missingEnvVarPanel.find('li').length).toEqual(2); + expect(prometheusMetrics.$missingEnvVarPanel.find('.flash-container')).toBeDefined(); + }); + }); + + describe('loadActiveMetrics', () => { + let prometheusMetrics; + + beforeEach(() => { + prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring'); + }); + + it('should show loader animation while response is being loaded and hide it when request is complete', (done) => { + const deferred = $.Deferred(); + spyOn($, 'getJSON').and.returnValue(deferred.promise()); + + prometheusMetrics.loadActiveMetrics(); + + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy(); + expect($.getJSON).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint); + + deferred.resolve({ data: metrics, success: true }); + + setTimeout(() => { + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + done(); + }); + }); + + it('should show empty state if response failed to load', (done) => { + const deferred = $.Deferred(); + spyOn($, 'getJSON').and.returnValue(deferred.promise()); + spyOn(prometheusMetrics, 'populateActiveMetrics'); + + prometheusMetrics.loadActiveMetrics(); + + deferred.reject(); + + setTimeout(() => { + expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy(); + expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy(); + done(); + }); + }); + + it('should populate metrics list once response is loaded', (done) => { + const deferred = $.Deferred(); + spyOn($, 'getJSON').and.returnValue(deferred.promise()); + spyOn(prometheusMetrics, 'populateActiveMetrics'); + + prometheusMetrics.loadActiveMetrics(); + + deferred.resolve({ data: metrics, success: true }); + + setTimeout(() => { + expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics); + done(); + }); + }); + }); +}); diff --git a/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb new file mode 100644 index 00000000000..61d48b05454 --- /dev/null +++ b/spec/lib/gitlab/prometheus/additional_metrics_parser_spec.rb @@ -0,0 +1,246 @@ +require 'spec_helper' + +describe Gitlab::Prometheus::AdditionalMetricsParser, lib: true do + include Prometheus::MetricBuilders + + let(:parser_error_class) { Gitlab::Prometheus::ParsingError } + + describe '#load_groups_from_yaml' do + subject { described_class.load_groups_from_yaml } + + describe 'parsing sample yaml' do + let(:sample_yaml) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: "title" + required_metrics: [ metric_a, metric_b ] + weight: 1 + queries: [{ query_range: 'query_range_a', label: label, unit: unit }] + - title: "title" + required_metrics: [metric_a] + weight: 1 + queries: [{ query_range: 'query_range_empty' }] + - group: group_b + priority: 1 + metrics: + - title: title + required_metrics: ['metric_a'] + weight: 1 + queries: [{query_range: query_range_a}] + EOF + end + + before do + allow(described_class).to receive(:load_yaml_file) { YAML.load(sample_yaml) } + end + + it 'parses to two metric groups with 2 and 1 metric respectively' do + expect(subject.count).to eq(2) + expect(subject[0].metrics.count).to eq(2) + expect(subject[1].metrics.count).to eq(1) + end + + it 'provide group data' do + expect(subject[0]).to have_attributes(name: 'group_a', priority: 1) + expect(subject[1]).to have_attributes(name: 'group_b', priority: 1) + end + + it 'provides metrics data' do + metrics = subject.flat_map(&:metrics) + + expect(metrics.count).to eq(3) + expect(metrics[0]).to have_attributes(title: 'title', required_metrics: %w(metric_a metric_b), weight: 1) + expect(metrics[1]).to have_attributes(title: 'title', required_metrics: %w(metric_a), weight: 1) + expect(metrics[2]).to have_attributes(title: 'title', required_metrics: %w{metric_a}, weight: 1) + end + + it 'provides query data' do + queries = subject.flat_map(&:metrics).flat_map(&:queries) + + expect(queries.count).to eq(3) + expect(queries[0]).to eq(query_range: 'query_range_a', label: 'label', unit: 'unit') + expect(queries[1]).to eq(query_range: 'query_range_empty') + expect(queries[2]).to eq(query_range: 'query_range_a') + end + end + + shared_examples 'required field' do |field_name| + context "when #{field_name} is nil" do + before do + allow(described_class).to receive(:load_yaml_file) { YAML.load(field_missing) } + end + + it 'throws parsing error' do + expect { subject }.to raise_error(parser_error_class, /#{field_name} can't be blank/i) + end + end + + context "when #{field_name} are not specified" do + before do + allow(described_class).to receive(:load_yaml_file) { YAML.load(field_nil) } + end + + it 'throws parsing error' do + expect { subject }.to raise_error(parser_error_class, /#{field_name} can't be blank/i) + end + end + end + + describe 'group required fields' do + it_behaves_like 'required field', 'metrics' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + EOF + end + end + + it_behaves_like 'required field', 'name' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: + priority: 1 + metrics: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - priority: 1 + metrics: [] + EOF + end + end + + it_behaves_like 'required field', 'priority' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: + metrics: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + metrics: [] + EOF + end + end + end + + describe 'metrics fields parsing' do + it_behaves_like 'required field', 'title' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: + required_metrics: [] + weight: 1 + queries: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - required_metrics: [] + weight: 1 + queries: [] + EOF + end + end + + it_behaves_like 'required field', 'required metrics' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: + weight: 1 + queries: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + weight: 1 + queries: [] + EOF + end + end + + it_behaves_like 'required field', 'weight' do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: [] + weight: + queries: [] + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: [] + queries: [] + EOF + end + end + + it_behaves_like 'required field', :queries do + let(:field_nil) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: [] + weight: 1 + queries: + EOF + end + + let(:field_missing) do + <<-EOF.strip_heredoc + - group: group_a + priority: 1 + metrics: + - title: title + required_metrics: [] + weight: 1 + EOF + end + end + end + end +end diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb new file mode 100644 index 00000000000..4909aec5a4d --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_deployment_query_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery, lib: true do + include Prometheus::MetricBuilders + + let(:client) { double('prometheus_client') } + let(:environment) { create(:environment, slug: 'environment-slug') } + let(:deployment) { create(:deployment, environment: environment) } + + subject(:query_result) { described_class.new(client).query(deployment.id) } + + around do |example| + Timecop.freeze(Time.local(2008, 9, 1, 12, 0, 0)) { example.run } + end + + include_examples 'additional metrics query' do + it 'queries using specific time' do + expect(client).to receive(:query_range).with(anything, + start: (deployment.created_at - 30.minutes).to_f, + stop: (deployment.created_at + 30.minutes).to_f) + + expect(query_result).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb new file mode 100644 index 00000000000..8e6e3bb5946 --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/additional_metrics_environment_query_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery, lib: true do + include Prometheus::MetricBuilders + + let(:client) { double('prometheus_client') } + let(:environment) { create(:environment, slug: 'environment-slug') } + + subject(:query_result) { described_class.new(client).query(environment.id) } + + around do |example| + Timecop.freeze { example.run } + end + + include_examples 'additional metrics query' do + it 'queries using specific time' do + expect(client).to receive(:query_range).with(anything, start: 8.hours.ago.to_f, stop: Time.now.to_f) + expect(query_result).not_to be_nil + end + end +end diff --git a/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb b/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb new file mode 100644 index 00000000000..d2796ab72da --- /dev/null +++ b/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe Gitlab::Prometheus::Queries::MatchedMetricsQuery, lib: true do + include Prometheus::MetricBuilders + + let(:metric_group_class) { Gitlab::Prometheus::MetricGroup } + let(:metric_class) { Gitlab::Prometheus::Metric } + + def series_info_with_environment(*more_metrics) + %w{metric_a metric_b}.concat(more_metrics).map { |metric_name| { '__name__' => metric_name, 'environment' => '' } } + end + + let(:metric_names) { %w{metric_a metric_b} } + let(:series_info_without_environment) do + [{ '__name__' => 'metric_a' }, + { '__name__' => 'metric_b' }] + end + let(:partialy_empty_series_info) { [{ '__name__' => 'metric_a', 'environment' => '' }] } + let(:empty_series_info) { [] } + + let(:client) { double('prometheus_client') } + + subject { described_class.new(client) } + + context 'with one group where two metrics is found' do + before do + allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + allow(client).to receive(:label_values).and_return(metric_names) + end + + context 'both metrics in the group pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_with_environment) + end + + it 'responds with both metrics as actve' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 2, metrics_missing_requirements: 0 }]) + end + end + + context 'none of the metrics pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_without_environment) + end + + it 'responds with both metrics missing requirements' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 2 }]) + end + end + + context 'no series information found about the metrics' do + before do + allow(client).to receive(:series).and_return(empty_series_info) + end + + it 'responds with both metrics missing requirements' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 2 }]) + end + end + + context 'one of the series info was not found' do + before do + allow(client).to receive(:series).and_return(partialy_empty_series_info) + end + it 'responds with one active and one missing metric' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 1 }]) + end + end + end + + context 'with one group where only one metric is found' do + before do + allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + allow(client).to receive(:label_values).and_return('metric_a') + end + + context 'both metrics in the group pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_with_environment) + end + + it 'responds with one metrics as active and no missing requiremens' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 0 }]) + end + end + + context 'no metrics in group pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_without_environment) + end + + it 'responds with one metrics as active and no missing requiremens' do + expect(subject.query).to eq([{ group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 1 }]) + end + end + end + + context 'with two groups where metrics are found in each group' do + let(:second_metric_group) { simple_metric_group(name: 'nameb', metrics: simple_metrics(added_metric_name: 'metric_c')) } + + before do + allow(metric_group_class).to receive(:all).and_return([simple_metric_group, second_metric_group]) + allow(client).to receive(:label_values).and_return('metric_c') + end + + context 'all metrics in both groups pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_with_environment('metric_c')) + end + + it 'responds with one metrics as active and no missing requiremens' do + expect(subject.query).to eq([ + { group: 'name', priority: 1, active_metrics: 1, metrics_missing_requirements: 0 }, + { group: 'nameb', priority: 1, active_metrics: 2, metrics_missing_requirements: 0 } + ] + ) + end + end + + context 'no metrics in groups pass requirements' do + before do + allow(client).to receive(:series).and_return(series_info_without_environment) + end + + it 'responds with one metrics as active and no missing requiremens' do + expect(subject.query).to eq([ + { group: 'name', priority: 1, active_metrics: 0, metrics_missing_requirements: 1 }, + { group: 'nameb', priority: 1, active_metrics: 0, metrics_missing_requirements: 2 } + ] + ) + end + end + end +end diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index 2d8bd2f6b97..46eaadae206 100644 --- a/spec/lib/gitlab/prometheus_client_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -119,6 +119,36 @@ describe Gitlab::PrometheusClient, lib: true do end end + describe '#series' do + let(:query_url) { prometheus_series_url('series_name', 'other_service') } + + around do |example| + Timecop.freeze { example.run } + end + + it 'calls endpoint and returns list of series' do + req_stub = stub_prometheus_request(query_url, body: prometheus_series('series_name')) + expected = prometheus_series('series_name').deep_stringify_keys['data'] + + expect(subject.series('series_name', 'other_service')).to eq(expected) + + expect(req_stub).to have_been_requested + end + end + + describe '#label_values' do + let(:query_url) { prometheus_label_values_url('__name__') } + + it 'calls endpoint and returns label values' do + req_stub = stub_prometheus_request(query_url, body: prometheus_label_values) + expected = prometheus_label_values.deep_stringify_keys['data'] + + expect(subject.label_values('__name__')).to eq(expected) + + expect(req_stub).to have_been_requested + end + end + describe '#query_range' do let(:prometheus_query) { prometheus_memory_query('env-slug') } let(:query_url) { prometheus_query_range_url(prometheus_query) } diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index aad215d5f41..bb84d3fc13d 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -30,7 +30,7 @@ describe Deployment, models: true do end describe '#includes_commit?' do - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository) } let(:environment) { create(:environment, project: project) } let(:deployment) do create(:deployment, environment: environment, sha: project.commit.id) @@ -90,6 +90,36 @@ describe Deployment, models: true do end end + describe '#additional_metrics' do + let(:project) { create(:project) } + let(:deployment) { create(:deployment, project: project) } + + subject { deployment.additional_metrics } + + context 'metrics are disabled' do + it { is_expected.to eq({}) } + end + + context 'metrics are enabled' do + let(:simple_metrics) do + { + success: true, + metrics: {}, + last_update: 42 + } + end + + let(:prometheus_service) { double('prometheus_service') } + + before do + allow(project).to receive(:prometheus_service).and_return(prometheus_service) + allow(prometheus_service).to receive(:additional_deployment_metrics).and_return(simple_metrics) + end + + it { is_expected.to eq(simple_metrics.merge({ deployment_time: deployment.created_at.to_i })) } + end + end + describe '#stop_action' do let(:build) { create(:ci_build) } diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index f8123cb518e..b0635c6a90a 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -432,6 +432,99 @@ describe Environment, models: true do end end + describe '#has_metrics?' do + subject { environment.has_metrics? } + + context 'when the enviroment is available' do + context 'with a deployment service' do + let(:project) { create(:prometheus_project) } + + context 'and a deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + it { is_expected.to be_truthy } + end + + context 'but no deployments' do + it { is_expected.to be_falsy } + end + end + + context 'without a monitoring service' do + it { is_expected.to be_falsy } + end + end + + context 'when the environment is unavailable' do + let(:project) { create(:prometheus_project) } + + before do + environment.stop + end + + it { is_expected.to be_falsy } + end + end + + describe '#additional_metrics' do + let(:project) { create(:prometheus_project) } + subject { environment.additional_metrics } + + context 'when the environment has additional metrics' do + before do + allow(environment).to receive(:has_additional_metrics?).and_return(true) + end + + it 'returns the additional metrics from the deployment service' do + expect(project.prometheus_service).to receive(:additional_environment_metrics) + .with(environment) + .and_return(:fake_metrics) + + is_expected.to eq(:fake_metrics) + end + end + + context 'when the environment does not have metrics' do + before do + allow(environment).to receive(:has_additional_metrics?).and_return(false) + end + + it { is_expected.to be_nil } + end + end + + describe '#has_additional_metrics??' do + subject { environment.has_additional_metrics? } + + context 'when the enviroment is available' do + context 'with a deployment service' do + let(:project) { create(:prometheus_project) } + + context 'and a deployment' do + let!(:deployment) { create(:deployment, environment: environment) } + it { is_expected.to be_truthy } + end + + context 'but no deployments' do + it { is_expected.to be_falsy } + end + end + + context 'without a monitoring service' do + it { is_expected.to be_falsy } + end + end + + context 'when the environment is unavailable' do + let(:project) { create(:prometheus_project) } + + before do + environment.stop + end + + it { is_expected.to be_falsy } + end + end + describe '#slug' do it "is automatically generated" do expect(environment.slug).not_to be_nil diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index 71b53732164..c914b32f7a4 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -65,7 +65,7 @@ describe PrometheusService, models: true, caching: true do end it 'returns reactive data' do - is_expected.to eq(prometheus_data) + is_expected.to eq(prometheus_metrics_data) end end end @@ -86,7 +86,7 @@ describe PrometheusService, models: true, caching: true do end it 'returns reactive data' do - is_expected.to eq(prometheus_data.merge(deployment_time: deployment.created_at.to_i)) + is_expected.to eq(prometheus_metrics_data.merge(deployment_time: deployment.created_at.to_i)) end end end @@ -116,6 +116,7 @@ describe PrometheusService, models: true, caching: true do end it { expect(subject.to_json).to eq(prometheus_data.to_json) } + it { expect(subject.to_json).to eq(prometheus_data.to_json) } end [404, 500].each do |status| diff --git a/spec/support/api/schema_matcher.rb b/spec/support/api/schema_matcher.rb index e42d727672b..dff0dfba675 100644 --- a/spec/support/api/schema_matcher.rb +++ b/spec/support/api/schema_matcher.rb @@ -1,8 +1,16 @@ +def schema_path(schema) + schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas" + "#{schema_directory}/#{schema}.json" +end + RSpec::Matchers.define :match_response_schema do |schema, **options| match do |response| - schema_directory = "#{Dir.pwd}/spec/fixtures/api/schemas" - schema_path = "#{schema_directory}/#{schema}.json" + JSON::Validator.validate!(schema_path(schema), response.body, options) + end +end - JSON::Validator.validate!(schema_path, response.body, options) +RSpec::Matchers.define :match_schema do |schema, **options| + match do |data| + JSON::Validator.validate!(schema_path(schema), data, options) end end diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb new file mode 100644 index 00000000000..016e16fc8d4 --- /dev/null +++ b/spec/support/prometheus/additional_metrics_shared_examples.rb @@ -0,0 +1,101 @@ +RSpec.shared_examples 'additional metrics query' do + include Prometheus::MetricBuilders + + let(:metric_group_class) { Gitlab::Prometheus::MetricGroup } + let(:metric_class) { Gitlab::Prometheus::Metric } + + let(:metric_names) { %w{metric_a metric_b} } + + let(:query_range_result) do + [{ 'metric': {}, 'values': [[1488758662.506, '0.00002996364761904785'], [1488758722.506, '0.00003090239047619091']] }] + end + + before do + allow(client).to receive(:label_values).and_return(metric_names) + allow(metric_group_class).to receive(:all).and_return([simple_metric_group(metrics: [simple_metric])]) + end + + context 'with one group where two metrics is found' do + before do + allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + end + + context 'some queries return results' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_empty', any_args).and_return([]) + end + + it 'return group data only for queries with results' do + expected = [ + { + group: 'name', + priority: 1, + metrics: [ + { + title: 'title', weight: 1, y_label: 'Values', queries: [ + { query_range: 'query_range_a', result: query_range_result }, + { query_range: 'query_range_b', label: 'label', unit: 'unit', result: query_range_result } + ] + } + ] + } + ] + + expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + expect(query_result).to eq(expected) + end + end + end + + context 'with two groups with one metric each' do + let(:metrics) { [simple_metric(queries: [simple_query])] } + before do + allow(metric_group_class).to receive(:all).and_return( + [ + simple_metric_group(name: 'group_a', metrics: [simple_metric(queries: [simple_query])]), + simple_metric_group(name: 'group_b', metrics: [simple_metric(title: 'title_b', queries: [simple_query('b')])]) + ]) + allow(client).to receive(:label_values).and_return(metric_names) + end + + context 'both queries return results' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return(query_range_result) + end + + it 'return group data both queries' do + queries_with_result_a = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } + queries_with_result_b = { queries: [{ query_range: 'query_range_b', result: query_range_result }] } + + expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + + expect(query_result.count).to eq(2) + expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) + + expect(query_result[0][:metrics].first).to include(queries_with_result_a) + expect(query_result[1][:metrics].first).to include(queries_with_result_b) + end + end + + context 'one query returns result' do + before do + allow(client).to receive(:query_range).with('query_range_a', any_args).and_return(query_range_result) + allow(client).to receive(:query_range).with('query_range_b', any_args).and_return([]) + end + + it 'return group data only for query with results' do + queries_with_result = { queries: [{ query_range: 'query_range_a', result: query_range_result }] } + + expect(query_result).to match_schema('prometheus/additional_metrics_query_result') + + expect(query_result.count).to eq(1) + expect(query_result).to all(satisfy { |r| r[:metrics].count == 1 }) + + expect(query_result.first[:metrics].first).to include(queries_with_result) + end + end + end +end diff --git a/spec/support/prometheus/metric_builders.rb b/spec/support/prometheus/metric_builders.rb new file mode 100644 index 00000000000..c8d056d3fc8 --- /dev/null +++ b/spec/support/prometheus/metric_builders.rb @@ -0,0 +1,27 @@ +module Prometheus + module MetricBuilders + def simple_query(suffix = 'a', **opts) + { query_range: "query_range_#{suffix}" }.merge(opts) + end + + def simple_queries + [simple_query, simple_query('b', label: 'label', unit: 'unit')] + end + + def simple_metric(title: 'title', required_metrics: [], queries: [simple_query]) + Gitlab::Prometheus::Metric.new(title: title, required_metrics: required_metrics, weight: 1, queries: queries) + end + + def simple_metrics(added_metric_name: 'metric_a') + [ + simple_metric(required_metrics: %W(#{added_metric_name} metric_b), queries: simple_queries), + simple_metric(required_metrics: [added_metric_name], queries: [simple_query('empty')]), + simple_metric(required_metrics: %w{metric_c}) + ] + end + + def simple_metric_group(name: 'name', metrics: simple_metrics) + Gitlab::Prometheus::MetricGroup.new(name: name, priority: 1, metrics: metrics) + end + end +end diff --git a/spec/support/prometheus_helpers.rb b/spec/support/prometheus_helpers.rb index 6b9ebcf2bb3..4212be2cc88 100644 --- a/spec/support/prometheus_helpers.rb +++ b/spec/support/prometheus_helpers.rb @@ -36,6 +36,19 @@ module PrometheusHelpers "https://prometheus.example.com/api/v1/query_range?#{query}" end + def prometheus_label_values_url(name) + "https://prometheus.example.com/api/v1/label/#{name}/values" + end + + def prometheus_series_url(*matches, start: 8.hours.ago, stop: Time.now) + query = { + match: matches, + start: start.to_f, + end: stop.to_f + }.to_query + "https://prometheus.example.com/api/v1/series?#{query}" + end + def stub_prometheus_request(url, body: {}, status: 200) WebMock.stub_request(:get, url) .to_return({ @@ -85,6 +98,19 @@ module PrometheusHelpers def prometheus_data(last_update: Time.now.utc) { success: true, + data: { + memory_values: prometheus_values_body('matrix').dig(:data, :result), + memory_current: prometheus_value_body('vector').dig(:data, :result), + cpu_values: prometheus_values_body('matrix').dig(:data, :result), + cpu_current: prometheus_value_body('vector').dig(:data, :result) + }, + last_update: last_update + } + end + + def prometheus_metrics_data(last_update: Time.now.utc) + { + success: true, metrics: { memory_values: prometheus_values_body('matrix').dig(:data, :result), memory_current: prometheus_value_body('vector').dig(:data, :result), @@ -140,4 +166,37 @@ module PrometheusHelpers } } end + + def prometheus_label_values + { + 'status': 'success', + 'data': %w(job_adds job_controller_rate_limiter_use job_depth job_queue_latency job_work_duration_sum up) + } + end + + def prometheus_series(name) + { + 'status': 'success', + 'data': [ + { + '__name__': name, + 'container_name': 'gitlab', + 'environment': 'mattermost', + 'id': '/docker/9953982f95cf5010dfc59d7864564d5f188aaecddeda343699783009f89db667', + 'image': 'gitlab/gitlab-ce:8.15.4-ce.1', + 'instance': 'minikube', + 'job': 'kubernetes-nodes', + 'name': 'k8s_gitlab.e6611886_mattermost-4210310111-77z8r_gitlab_2298ae6b-da24-11e6-baee-8e7f67d0eb3a_43536cb6', + 'namespace': 'gitlab', + 'pod_name': 'mattermost-4210310111-77z8r' + }, + { + '__name__': name, + 'id': '/docker', + 'instance': 'minikube', + 'job': 'kubernetes-nodes' + } + ] + } + end end |