Commit 2e38ffe3 authored by Mike Greiling's avatar Mike Greiling

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

Switch charts to Prometheus API endpoint CE-3

Closes #58516

See merge request gitlab-org/gitlab-ce!29280
parents 95c16e48 66e65c60
...@@ -227,6 +227,7 @@ export default { ...@@ -227,6 +227,7 @@ export default {
[this.primaryColor] = chart.getOption().color; [this.primaryColor] = chart.getOption().color;
}, },
onResize() { onResize() {
if (!this.$refs.areaChart) return;
const { width } = this.$refs.areaChart.$el.getBoundingClientRect(); const { width } = this.$refs.areaChart.$el.getBoundingClientRect();
this.width = width; this.width = width;
}, },
......
...@@ -137,7 +137,12 @@ export default { ...@@ -137,7 +137,12 @@ export default {
'showEmptyState', 'showEmptyState',
'environments', 'environments',
'deploymentData', 'deploymentData',
'metricsWithData',
'useDashboardEndpoint',
]), ]),
groupsWithData() {
return this.groups.filter(group => this.chartsWithData(group.metrics).length > 0);
},
}, },
created() { created() {
this.setEndpoints({ this.setEndpoints({
...@@ -182,7 +187,16 @@ export default { ...@@ -182,7 +187,16 @@ export default {
'fetchData', 'fetchData',
'setGettingStartedEmptyState', 'setGettingStartedEmptyState',
'setEndpoints', 'setEndpoints',
'setDashboardEnabled',
]), ]),
chartsWithData(charts) {
if (!this.useDashboardEndpoint) {
return charts;
}
return charts.filter(chart =>
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
getGraphAlerts(queries) { getGraphAlerts(queries) {
if (!this.allAlerts) return {}; if (!this.allAlerts) return {};
const metricIdsForChart = queries.map(q => q.metricId); const metricIdsForChart = queries.map(q => q.metricId);
...@@ -308,13 +322,13 @@ export default { ...@@ -308,13 +322,13 @@ export default {
</div> </div>
</div> </div>
<graph-group <graph-group
v-for="(groupData, index) in groups" v-for="(groupData, index) in groupsWithData"
:key="index" :key="index"
:name="groupData.group" :name="groupData.group"
:show-panels="showPanels" :show-panels="showPanels"
> >
<monitor-area-chart <monitor-area-chart
v-for="(graphData, graphIndex) in groupData.metrics" v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex" :key="graphIndex"
:graph-data="graphData" :graph-data="graphData"
:deployment-data="deploymentData" :deployment-data="deploymentData"
......
...@@ -42,8 +42,9 @@ export const setDashboardEnabled = ({ commit }, enabled) => { ...@@ -42,8 +42,9 @@ export const setDashboardEnabled = ({ commit }, enabled) => {
export const requestMetricsDashboard = ({ commit }) => { export const requestMetricsDashboard = ({ commit }) => {
commit(types.REQUEST_METRICS_DATA); commit(types.REQUEST_METRICS_DATA);
}; };
export const receiveMetricsDashboardSuccess = ({ commit }, { response }) => { export const receiveMetricsDashboardSuccess = ({ commit, dispatch }, { response, params }) => {
commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups); commit(types.RECEIVE_METRICS_DATA_SUCCESS, response.dashboard.panel_groups);
dispatch('fetchPrometheusMetrics', params);
}; };
export const receiveMetricsDashboardFailure = ({ commit }, error) => { export const receiveMetricsDashboardFailure = ({ commit }, error) => {
commit(types.RECEIVE_METRICS_DATA_FAILURE, error); commit(types.RECEIVE_METRICS_DATA_FAILURE, error);
...@@ -98,7 +99,7 @@ export const fetchDashboard = ({ state, dispatch }, params) => { ...@@ -98,7 +99,7 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
.get(state.dashboardEndpoint, { params }) .get(state.dashboardEndpoint, { params })
.then(resp => resp.data) .then(resp => resp.data)
.then(response => { .then(response => {
dispatch('receiveMetricsDashboardSuccess', { response }); dispatch('receiveMetricsDashboardSuccess', { response, params });
}) })
.catch(error => { .catch(error => {
dispatch('receiveMetricsDashboardFailure', error); dispatch('receiveMetricsDashboardFailure', error);
...@@ -106,6 +107,62 @@ export const fetchDashboard = ({ state, dispatch }, params) => { ...@@ -106,6 +107,62 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
}); });
}; };
function fetchPrometheusResult(prometheusEndpoint, params) {
return backOffRequest(() => axios.get(prometheusEndpoint, { params }))
.then(res => res.data)
.then(response => {
if (response.status === 'error') {
throw new Error(response.error);
}
return response.data.result;
});
}
/**
* Returns list of metrics in data.result
* {"status":"success", "data":{"resultType":"matrix","result":[]}}
*
* @param {metric} metric
*/
export const fetchPrometheusMetric = ({ commit }, { metric, params }) => {
const { start, end } = params;
const timeDiff = end - start;
const minStep = 60;
const queryDataPoints = 600;
const step = Math.max(minStep, Math.ceil(timeDiff / queryDataPoints));
const queryParams = {
start,
end,
step,
};
return fetchPrometheusResult(metric.prometheus_endpoint_path, queryParams).then(result => {
commit(types.SET_QUERY_RESULT, { metricId: metric.metric_id, result });
});
};
export const fetchPrometheusMetrics = ({ state, commit, dispatch }, params) => {
commit(types.REQUEST_METRICS_DATA);
const promises = [];
state.groups.forEach(group => {
group.panels.forEach(panel => {
panel.metrics.forEach(metric => {
promises.push(dispatch('fetchPrometheusMetric', { metric, params }));
});
});
});
return Promise.all(promises).then(() => {
if (state.metricsWithData.length === 0) {
commit(types.SET_NO_DATA_EMPTY_STATE);
}
});
};
export const fetchDeploymentsData = ({ state, dispatch }) => { export const fetchDeploymentsData = ({ state, dispatch }) => {
if (!state.deploymentEndpoint) { if (!state.deploymentEndpoint) {
return Promise.resolve([]); return Promise.resolve([]);
......
...@@ -7,7 +7,9 @@ export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILUR ...@@ -7,7 +7,9 @@ export const RECEIVE_DEPLOYMENTS_DATA_FAILURE = 'RECEIVE_DEPLOYMENTS_DATA_FAILUR
export const REQUEST_ENVIRONMENTS_DATA = 'REQUEST_ENVIRONMENTS_DATA'; 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_QUERY_RESULT = 'SET_QUERY_RESULT';
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_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';
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
import Vue from 'vue';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { normalizeMetrics, sortMetrics } from './utils'; import { normalizeMetrics, sortMetrics, normalizeQueryResult } from './utils';
export default { export default {
[types.REQUEST_METRICS_DATA](state) { [types.REQUEST_METRICS_DATA](state) {
...@@ -48,6 +49,26 @@ export default { ...@@ -48,6 +49,26 @@ export default {
[types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) { [types.RECEIVE_ENVIRONMENTS_DATA_FAILURE](state) {
state.environments = []; state.environments = [];
}, },
[types.SET_QUERY_RESULT](state, { metricId, result }) {
if (!metricId || !result || result.length === 0) {
return;
}
state.showEmptyState = false;
state.groups.forEach(group => {
group.metrics.forEach(metric => {
metric.queries.forEach(query => {
if (query.metric_id === metricId) {
state.metricsWithData.push(metricId);
// ensure dates/numbers are correctly formatted for charts
const normalizedResults = result.map(normalizeQueryResult);
Vue.set(query, 'result', Object.freeze(normalizedResults));
}
});
});
});
},
[types.SET_ENDPOINTS](state, endpoints) { [types.SET_ENDPOINTS](state, endpoints) {
state.metricsEndpoint = endpoints.metricsEndpoint; state.metricsEndpoint = endpoints.metricsEndpoint;
state.environmentsEndpoint = endpoints.environmentsEndpoint; state.environmentsEndpoint = endpoints.environmentsEndpoint;
...@@ -60,4 +81,8 @@ export default { ...@@ -60,4 +81,8 @@ export default {
[types.SET_GETTING_STARTED_EMPTY_STATE](state) { [types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted'; state.emptyState = 'gettingStarted';
}, },
[types.SET_NO_DATA_EMPTY_STATE](state) {
state.showEmptyState = true;
state.emptyState = 'noData';
},
}; };
import invalidUrl from '~/lib/utils/invalid_url';
export default () => ({ export default () => ({
hasMetrics: false, hasMetrics: false,
showPanels: true, showPanels: true,
metricsEndpoint: null, metricsEndpoint: null,
environmentsEndpoint: null, environmentsEndpoint: null,
deploymentsEndpoint: null, deploymentsEndpoint: null,
dashboardEndpoint: null, dashboardEndpoint: invalidUrl,
useDashboardEndpoint: false, useDashboardEndpoint: false,
emptyState: 'gettingStarted', emptyState: 'gettingStarted',
showEmptyState: true, showEmptyState: true,
groups: [], groups: [],
deploymentData: [], deploymentData: [],
environments: [], environments: [],
metricsWithData: [],
}); });
...@@ -58,6 +58,14 @@ export const sortMetrics = metrics => ...@@ -58,6 +58,14 @@ export const sortMetrics = metrics =>
.sortBy('weight') .sortBy('weight')
.value(); .value();
export const normalizeQueryResult = timeSeries => ({
...timeSeries,
values: timeSeries.values.map(([timestamp, value]) => [
new Date(timestamp * 1000).toISOString(),
Number(value),
]),
});
export const normalizeMetrics = metrics => { export const normalizeMetrics = metrics => {
const groupedMetrics = groupQueriesByChartInfo(metrics); const groupedMetrics = groupQueriesByChartInfo(metrics);
...@@ -66,13 +74,7 @@ export const normalizeMetrics = metrics => { ...@@ -66,13 +74,7 @@ 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(timeSeries => ({ result: (query.result || []).map(normalizeQueryResult),
...timeSeries,
values: timeSeries.values.map(([timestamp, value]) => [
new Date(timestamp * 1000).toISOString(),
Number(value),
]),
})),
})); }));
return { return {
......
...@@ -880,6 +880,7 @@ export const metricsDashboardResponse = { ...@@ -880,6 +880,7 @@ export const metricsDashboardResponse = {
label: 'Total', label: 'Total',
unit: 'GB', unit: 'GB',
metric_id: 12, metric_id: 12,
prometheus_endpoint_path: 'http://test',
}, },
], ],
}, },
......
...@@ -8,6 +8,8 @@ import { ...@@ -8,6 +8,8 @@ import {
receiveMetricsDashboardFailure, receiveMetricsDashboardFailure,
fetchDeploymentsData, fetchDeploymentsData,
fetchEnvironmentsData, fetchEnvironmentsData,
fetchPrometheusMetrics,
fetchPrometheusMetric,
requestMetricsData, requestMetricsData,
setEndpoints, setEndpoints,
setGettingStartedEmptyState, setGettingStartedEmptyState,
...@@ -15,7 +17,12 @@ import { ...@@ -15,7 +17,12 @@ 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, metricsDashboardResponse } from '../mock_data'; import {
deploymentData,
environmentData,
metricsDashboardResponse,
metricsGroupsAPIResponse,
} from '../mock_data';
describe('Monitoring store actions', () => { describe('Monitoring store actions', () => {
let mock; let mock;
...@@ -179,6 +186,7 @@ describe('Monitoring store actions', () => { ...@@ -179,6 +186,7 @@ describe('Monitoring store actions', () => {
expect(dispatch).toHaveBeenCalledWith('requestMetricsDashboard'); expect(dispatch).toHaveBeenCalledWith('requestMetricsDashboard');
expect(dispatch).toHaveBeenCalledWith('receiveMetricsDashboardSuccess', { expect(dispatch).toHaveBeenCalledWith('receiveMetricsDashboardSuccess', {
response, response,
params,
}); });
done(); done();
}) })
...@@ -220,6 +228,8 @@ describe('Monitoring store actions', () => { ...@@ -220,6 +228,8 @@ describe('Monitoring store actions', () => {
types.RECEIVE_METRICS_DATA_SUCCESS, types.RECEIVE_METRICS_DATA_SUCCESS,
metricsDashboardResponse.dashboard.panel_groups, metricsDashboardResponse.dashboard.panel_groups,
); );
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetrics', params);
}); });
}); });
...@@ -242,4 +252,71 @@ describe('Monitoring store actions', () => { ...@@ -242,4 +252,71 @@ describe('Monitoring store actions', () => {
expect(commit).toHaveBeenCalledWith(types.RECEIVE_METRICS_DATA_FAILURE, 'uh-oh'); expect(commit).toHaveBeenCalledWith(types.RECEIVE_METRICS_DATA_FAILURE, 'uh-oh');
}); });
}); });
describe('fetchPrometheusMetrics', () => {
let commit;
let dispatch;
beforeEach(() => {
commit = jasmine.createSpy();
dispatch = jasmine.createSpy();
});
it('commits empty state when state.groups is empty', done => {
const state = storeState();
const params = {};
fetchPrometheusMetrics({ state, commit, dispatch }, params)
.then(() => {
expect(commit).toHaveBeenCalledWith(types.SET_NO_DATA_EMPTY_STATE);
expect(dispatch).not.toHaveBeenCalled();
done();
})
.catch(done.fail);
});
it('dispatches fetchPrometheusMetric for each panel query', done => {
const params = {};
const state = storeState();
state.groups = metricsDashboardResponse.dashboard.panel_groups;
const metric = state.groups[0].panels[0].metrics[0];
fetchPrometheusMetrics({ state, commit, dispatch }, params)
.then(() => {
expect(dispatch.calls.count()).toEqual(3);
expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { metric, params });
done();
})
.catch(done.fail);
done();
});
});
describe('fetchPrometheusMetric', () => {
it('commits prometheus query result', done => {
const commit = jasmine.createSpy();
const params = {
start: '1557216349.469',
end: '1557218149.469',
};
const metric = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics[0];
const state = storeState();
const data = metricsGroupsAPIResponse.data[0].metrics[0].queries[0];
const response = { data };
mock.onGet('http://test').reply(200, response);
fetchPrometheusMetric({ state, commit }, { metric, params });
setTimeout(() => {
expect(commit).toHaveBeenCalledWith(types.SET_QUERY_RESULT, {
metricId: metric.metric_id,
result: data.result,
});
done();
});
});
});
}); });
...@@ -118,4 +118,42 @@ describe('Monitoring mutations', () => { ...@@ -118,4 +118,42 @@ describe('Monitoring mutations', () => {
expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json'); expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
}); });
}); });
describe('SET_QUERY_RESULT', () => {
const metricId = 12;
const result = [{ values: [[0, 1], [1, 1], [1, 3]] }];
beforeEach(() => {
stateCopy.useDashboardEndpoint = true;
const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
});
it('clears empty state', () => {
mutations[types.SET_QUERY_RESULT](stateCopy, {
metricId,
result,
});
expect(stateCopy.showEmptyState).toBe(false);
});
it('sets metricsWithData value', () => {
mutations[types.SET_QUERY_RESULT](stateCopy, {
metricId,
result,
});
expect(stateCopy.metricsWithData).toEqual([12]);
});
it('does not store empty results', () => {
mutations[types.SET_QUERY_RESULT](stateCopy, {
metricId,
result: [],
});
expect(stateCopy.metricsWithData).toEqual([]);
});
});
}); });
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