Commit eabd1b9e authored by Mike Greiling's avatar Mike Greiling

Merge branch 'prom-api-2' into 'master'

Switch charts to Prometheus API endpoint CE-2

See merge request gitlab-org/gitlab-ce!29226
parents ac01310f fba991dc
/**
* Invalid URL that ensures we don't make a network request
* Can be used as a default value for URLs. Using an empty
* string can still result in request being made to the current page
*/
export default 'https://invalid';
...@@ -6,6 +6,7 @@ import { s__ } from '~/locale'; ...@@ -6,6 +6,7 @@ import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import '~/vue_shared/mixins/is_ee'; import '~/vue_shared/mixins/is_ee';
import { getParameterValues } from '~/lib/utils/url_utility'; import { getParameterValues } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url';
import MonitorAreaChart from './charts/area.vue'; import MonitorAreaChart from './charts/area.vue';
import GraphGroup from './graph_group.vue'; import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue'; import EmptyState from './empty_state.vue';
...@@ -111,6 +112,11 @@ export default { ...@@ -111,6 +112,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
dashboardEndpoint: {
type: String,
required: false,
default: invalidUrl,
},
}, },
data() { data() {
return { return {
...@@ -138,6 +144,7 @@ export default { ...@@ -138,6 +144,7 @@ export default {
metricsEndpoint: this.metricsEndpoint, metricsEndpoint: this.metricsEndpoint,
environmentsEndpoint: this.environmentsEndpoint, environmentsEndpoint: this.environmentsEndpoint,
deploymentsEndpoint: this.deploymentEndpoint, deploymentsEndpoint: this.deploymentEndpoint,
dashboardEndpoint: this.dashboardEndpoint,
}); });
this.timeWindows = timeWindows; this.timeWindows = timeWindows;
......
...@@ -7,6 +7,11 @@ export default (props = {}) => { ...@@ -7,6 +7,11 @@ export default (props = {}) => {
const el = document.getElementById('prometheus-graphs'); const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) { if (el && el.dataset) {
store.dispatch(
'monitoringDashboard/setDashboardEnabled',
gon.features.environmentMetricsUsePrometheusEndpoint,
);
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el, el,
......
...@@ -35,6 +35,20 @@ export const setEndpoints = ({ commit }, endpoints) => { ...@@ -35,6 +35,20 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints); commit(types.SET_ENDPOINTS, endpoints);
}; };
export const setDashboardEnabled = ({ commit }, enabled) => {
commit(types.SET_DASHBOARD_ENABLED, enabled);
};
export const requestMetricsDashboard = ({ commit }) => {
commit(types.REQUEST_METRICS_DATA);
};
export const receiveMetricsDashboardSuccess = ({ commit }, { response }) => {
commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups);
};
export const receiveMetricsDashboardFailure = ({ commit }, error) => {
commit(types.RECEIVE_METRICS_DATA_FAILURE, error);
};
export const requestMetricsData = ({ commit }) => commit(types.REQUEST_METRICS_DATA); export const requestMetricsData = ({ commit }) => commit(types.REQUEST_METRICS_DATA);
export const receiveMetricsDataSuccess = ({ commit }, data) => export const receiveMetricsDataSuccess = ({ commit }, data) =>
commit(types.RECEIVE_METRICS_DATA_SUCCESS, data); commit(types.RECEIVE_METRICS_DATA_SUCCESS, data);
...@@ -56,6 +70,10 @@ export const fetchData = ({ dispatch }, params) => { ...@@ -56,6 +70,10 @@ export const fetchData = ({ dispatch }, params) => {
}; };
export const fetchMetricsData = ({ state, dispatch }, params) => { export const fetchMetricsData = ({ state, dispatch }, params) => {
if (state.useDashboardEndpoint) {
return dispatch('fetchDashboard', params);
}
dispatch('requestMetricsData'); dispatch('requestMetricsData');
return backOffRequest(() => axios.get(state.metricsEndpoint, { params })) return backOffRequest(() => axios.get(state.metricsEndpoint, { params }))
...@@ -73,6 +91,21 @@ export const fetchMetricsData = ({ state, dispatch }, params) => { ...@@ -73,6 +91,21 @@ export const fetchMetricsData = ({ state, dispatch }, params) => {
}); });
}; };
export const fetchDashboard = ({ state, dispatch }, params) => {
dispatch('requestMetricsDashboard');
return axios
.get(state.dashboardEndpoint, { params })
.then(resp => resp.data)
.then(response => {
dispatch('receiveMetricsDashboardSuccess', { response });
})
.catch(error => {
dispatch('receiveMetricsDashboardFailure', error);
createFlash(s__('Metrics|There was an error while retrieving metrics'));
});
};
export const fetchDeploymentsData = ({ state, dispatch }) => { export const fetchDeploymentsData = ({ state, dispatch }) => {
if (!state.deploymentEndpoint) { if (!state.deploymentEndpoint) {
return Promise.resolve([]); return Promise.resolve([]);
......
...@@ -8,5 +8,6 @@ export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; ...@@ -8,5 +8,6 @@ export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA';
export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS'; export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCCESS';
export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE'; export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW'; export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_DASHBOARD_ENABLED = 'SET_DASHBOARD_ENABLED';
export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
...@@ -7,10 +7,24 @@ export default { ...@@ -7,10 +7,24 @@ export default {
state.showEmptyState = true; state.showEmptyState = true;
}, },
[types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) { [types.RECEIVE_METRICS_DATA_SUCCESS](state, groupData) {
state.groups = groupData.map(group => ({ state.groups = groupData.map(group => {
...group, let { metrics } = group;
metrics: normalizeMetrics(sortMetrics(group.metrics)),
// for backwards compatibility, and to limit Vue template changes:
// for each group alias panels to metrics
// for each panel alias metrics to queries
if (state.useDashboardEndpoint) {
metrics = group.panels.map(panel => ({
...panel,
queries: panel.metrics,
})); }));
}
return {
...group,
metrics: normalizeMetrics(sortMetrics(metrics)),
};
});
if (!state.groups.length) { if (!state.groups.length) {
state.emptyState = 'noData'; state.emptyState = 'noData';
...@@ -38,6 +52,10 @@ export default { ...@@ -38,6 +52,10 @@ export default {
state.metricsEndpoint = endpoints.metricsEndpoint; state.metricsEndpoint = endpoints.metricsEndpoint;
state.environmentsEndpoint = endpoints.environmentsEndpoint; state.environmentsEndpoint = endpoints.environmentsEndpoint;
state.deploymentsEndpoint = endpoints.deploymentsEndpoint; state.deploymentsEndpoint = endpoints.deploymentsEndpoint;
state.dashboardEndpoint = endpoints.dashboardEndpoint;
},
[types.SET_DASHBOARD_ENABLED](state, enabled) {
state.useDashboardEndpoint = enabled;
}, },
[types.SET_GETTING_STARTED_EMPTY_STATE](state) { [types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted'; state.emptyState = 'gettingStarted';
......
...@@ -4,6 +4,8 @@ export default () => ({ ...@@ -4,6 +4,8 @@ export default () => ({
metricsEndpoint: null, metricsEndpoint: null,
environmentsEndpoint: null, environmentsEndpoint: null,
deploymentsEndpoint: null, deploymentsEndpoint: null,
dashboardEndpoint: null,
useDashboardEndpoint: false,
emptyState: 'gettingStarted', emptyState: 'gettingStarted',
showEmptyState: true, showEmptyState: true,
groups: [], groups: [],
......
...@@ -66,9 +66,9 @@ export const normalizeMetrics = metrics => { ...@@ -66,9 +66,9 @@ export const normalizeMetrics = metrics => {
...query, ...query,
// custom metrics do not require a label, so we should ensure this attribute is defined // custom metrics do not require a label, so we should ensure this attribute is defined
label: query.label || metric.y_label, label: query.label || metric.y_label,
result: query.result.map(result => ({ result: (query.result || []).map(timeSeries => ({
...result, ...timeSeries,
values: result.values.map(([timestamp, value]) => [ values: timeSeries.values.map(([timestamp, value]) => [
new Date(timestamp * 1000).toISOString(), new Date(timestamp * 1000).toISOString(),
Number(value), Number(value),
]), ]),
......
...@@ -26,6 +26,7 @@ module EnvironmentsHelper ...@@ -26,6 +26,7 @@ module EnvironmentsHelper
"empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'), "empty-no-data-svg-path" => image_path('illustrations/monitoring/no_data.svg'),
"empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'), "empty-unable-to-connect-svg-path" => image_path('illustrations/monitoring/unable_to_connect.svg'),
"metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json), "metrics-endpoint" => additional_metrics_project_environment_path(project, environment, format: :json),
"dashboard-endpoint" => metrics_dashboard_project_environment_path(project, environment, format: :json),
"deployment-endpoint" => project_environment_deployments_path(project, environment, format: :json), "deployment-endpoint" => project_environment_deployments_path(project, environment, format: :json),
"environments-endpoint": project_environments_path(project, format: :json), "environments-endpoint": project_environments_path(project, format: :json),
"project-path" => project_path(project), "project-path" => project_path(project),
......
...@@ -857,3 +857,67 @@ export const environmentData = [ ...@@ -857,3 +857,67 @@ export const environmentData = [
updated_at: '2018-07-04T18:44:54.010Z', updated_at: '2018-07-04T18:44:54.010Z',
}, },
]; ];
export const metricsDashboardResponse = {
dashboard: {
dashboard: 'Environment metrics',
priority: 1,
panel_groups: [
{
group: 'System metrics (Kubernetes)',
priority: 5,
panels: [
{
title: 'Memory Usage (Total)',
type: 'area-chart',
y_label: 'Total Memory Used',
weight: 4,
metrics: [
{
id: 'system_metrics_kubernetes_container_memory_total',
query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
label: 'Total',
unit: 'GB',
metric_id: 12,
},
],
},
{
title: 'Core Usage (Total)',
type: 'area-chart',
y_label: 'Total Cores',
weight: 3,
metrics: [
{
id: 'system_metrics_kubernetes_container_cores_total',
query_range:
'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)',
label: 'Total',
unit: 'cores',
metric_id: 13,
},
],
},
{
title: 'Memory Usage (Pod average)',
type: 'area-chart',
y_label: 'Memory Used per Pod',
weight: 2,
metrics: [
{
id: 'system_metrics_kubernetes_container_memory_average',
query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024',
label: 'Pod average',
unit: 'MB',
metric_id: 14,
},
],
},
],
},
],
},
status: 'success',
};
...@@ -3,6 +3,9 @@ import MockAdapter from 'axios-mock-adapter'; ...@@ -3,6 +3,9 @@ import MockAdapter from 'axios-mock-adapter';
import store from '~/monitoring/stores'; import store from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import { import {
fetchDashboard,
receiveMetricsDashboardSuccess,
receiveMetricsDashboardFailure,
fetchDeploymentsData, fetchDeploymentsData,
fetchEnvironmentsData, fetchEnvironmentsData,
requestMetricsData, requestMetricsData,
...@@ -12,7 +15,7 @@ import { ...@@ -12,7 +15,7 @@ import {
import storeState from '~/monitoring/stores/state'; import storeState from '~/monitoring/stores/state';
import testAction from 'spec/helpers/vuex_action_helper'; import testAction from 'spec/helpers/vuex_action_helper';
import { resetStore } from '../helpers'; import { resetStore } from '../helpers';
import { deploymentData, environmentData } from '../mock_data'; import { deploymentData, environmentData, metricsDashboardResponse } from '../mock_data';
describe('Monitoring store actions', () => { describe('Monitoring store actions', () => {
let mock; let mock;
...@@ -155,4 +158,88 @@ describe('Monitoring store actions', () => { ...@@ -155,4 +158,88 @@ describe('Monitoring store actions', () => {
); );
}); });
}); });
describe('fetchDashboard', () => {
let dispatch;
let state;
const response = metricsDashboardResponse;
beforeEach(() => {
dispatch = jasmine.createSpy();
state = storeState();
state.dashboardEndpoint = '/dashboard';
});
it('dispatches receive and success actions', done => {
const params = {};
mock.onGet(state.dashboardEndpoint).reply(200, response);
fetchDashboard({ state, dispatch }, params)
.then(() => {
expect(dispatch).toHaveBeenCalledWith('requestMetricsDashboard');
expect(dispatch).toHaveBeenCalledWith('receiveMetricsDashboardSuccess', {
response,
});
done();
})
.catch(done.fail);
});
it('dispatches failure action', done => {
const params = {};
mock.onGet(state.dashboardEndpoint).reply(500);
fetchDashboard({ state, dispatch }, params)
.then(() => {
expect(dispatch).toHaveBeenCalledWith(
'receiveMetricsDashboardFailure',
new Error('Request failed with status code 500'),
);
done();
})
.catch(done.fail);
});
});
describe('receiveMetricsDashboardSuccess', () => {
let commit;
let dispatch;
beforeEach(() => {
commit = jasmine.createSpy();
dispatch = jasmine.createSpy();
});
it('stores groups ', () => {
const params = {};
const response = metricsDashboardResponse;
receiveMetricsDashboardSuccess({ commit, dispatch }, { response, params });
expect(commit).toHaveBeenCalledWith(
types.RECEIVE_METRICS_DATA_SUCCESS,
metricsDashboardResponse.dashboard.panel_groups,
);
});
});
describe('receiveMetricsDashboardFailure', () => {
let commit;
beforeEach(() => {
commit = jasmine.createSpy();
});
it('commits failure action', () => {
receiveMetricsDashboardFailure({ commit });
expect(commit).toHaveBeenCalledWith(types.RECEIVE_METRICS_DATA_FAILURE, undefined);
});
it('commits failure action with error', () => {
receiveMetricsDashboardFailure({ commit }, 'uh-oh');
expect(commit).toHaveBeenCalledWith(types.RECEIVE_METRICS_DATA_FAILURE, 'uh-oh');
});
});
}); });
import mutations from '~/monitoring/stores/mutations'; import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types'; import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state'; import state from '~/monitoring/stores/state';
import { metricsGroupsAPIResponse, deploymentData } from '../mock_data'; import { metricsGroupsAPIResponse, deploymentData, metricsDashboardResponse } from '../mock_data';
describe('Monitoring mutations', () => { describe('Monitoring mutations', () => {
let stateCopy; let stateCopy;
...@@ -11,14 +11,16 @@ describe('Monitoring mutations', () => { ...@@ -11,14 +11,16 @@ describe('Monitoring mutations', () => {
}); });
describe(types.RECEIVE_METRICS_DATA_SUCCESS, () => { describe(types.RECEIVE_METRICS_DATA_SUCCESS, () => {
let groups;
beforeEach(() => { beforeEach(() => {
stateCopy.groups = []; stateCopy.groups = [];
const groups = metricsGroupsAPIResponse.data; groups = metricsGroupsAPIResponse.data;
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
}); });
it('normalizes values', () => { it('normalizes values', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
const expectedTimestamp = '2017-05-25T08:22:34.925Z'; const expectedTimestamp = '2017-05-25T08:22:34.925Z';
const expectedValue = 0.0010794445585559514; const expectedValue = 0.0010794445585559514;
const [timestamp, value] = stateCopy.groups[0].metrics[0].queries[0].result[0].values[0]; const [timestamp, value] = stateCopy.groups[0].metrics[0].queries[0].result[0].values[0];
...@@ -28,22 +30,27 @@ describe('Monitoring mutations', () => { ...@@ -28,22 +30,27 @@ describe('Monitoring mutations', () => {
}); });
it('contains two groups that contains, one of which has two queries sorted by priority', () => { it('contains two groups that contains, one of which has two queries sorted by priority', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
expect(stateCopy.groups).toBeDefined(); expect(stateCopy.groups).toBeDefined();
expect(stateCopy.groups.length).toEqual(2); expect(stateCopy.groups.length).toEqual(2);
expect(stateCopy.groups[0].metrics.length).toEqual(2); expect(stateCopy.groups[0].metrics.length).toEqual(2);
}); });
it('assigns queries a metric id', () => { it('assigns queries a metric id', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
expect(stateCopy.groups[1].metrics[0].queries[0].metricId).toEqual('100'); expect(stateCopy.groups[1].metrics[0].queries[0].metricId).toEqual('100');
}); });
it('removes the data if all the values from a query are not defined', () => { it('removes the data if all the values from a query are not defined', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
expect(stateCopy.groups[1].metrics[0].queries[0].result.length).toEqual(0); expect(stateCopy.groups[1].metrics[0].queries[0].result.length).toEqual(0);
}); });
it('assigns metric id of null if metric has no id', () => { it('assigns metric id of null if metric has no id', () => {
stateCopy.groups = []; stateCopy.groups = [];
const groups = metricsGroupsAPIResponse.data;
const noId = groups.map(group => ({ const noId = groups.map(group => ({
...group, ...group,
...{ ...{
...@@ -63,6 +70,26 @@ describe('Monitoring mutations', () => { ...@@ -63,6 +70,26 @@ describe('Monitoring mutations', () => {
}); });
}); });
}); });
describe('dashboard endpoint enabled', () => {
const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
beforeEach(() => {
stateCopy.useDashboardEndpoint = true;
});
it('aliases group panels to metrics for backwards compatibility', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
expect(stateCopy.groups[0].metrics[0]).toBeDefined();
});
it('aliases panel metrics to queries for backwards compatibility', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
expect(stateCopy.groups[0].metrics[0].queries).toBeDefined();
});
});
}); });
describe(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, () => { describe(types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS, () => {
...@@ -82,11 +109,13 @@ describe('Monitoring mutations', () => { ...@@ -82,11 +109,13 @@ describe('Monitoring mutations', () => {
metricsEndpoint: 'additional_metrics.json', metricsEndpoint: 'additional_metrics.json',
environmentsEndpoint: 'environments.json', environmentsEndpoint: 'environments.json',
deploymentsEndpoint: 'deployments.json', deploymentsEndpoint: 'deployments.json',
dashboardEndpoint: 'dashboard.json',
}); });
expect(stateCopy.metricsEndpoint).toEqual('additional_metrics.json'); expect(stateCopy.metricsEndpoint).toEqual('additional_metrics.json');
expect(stateCopy.environmentsEndpoint).toEqual('environments.json'); expect(stateCopy.environmentsEndpoint).toEqual('environments.json');
expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json'); expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
}); });
}); });
}); });
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