Commit 7d20dde9 authored by Tristan Read's avatar Tristan Read Committed by Phil Hughes

Frontend for Grafana metric chart embeds

Allows Grafana charts to be embedded in GitLab Flavored Markdown.
Switches the metrics dashboard fetch from blocking to polling.
parent 607d955c
/* eslint-disable import/prefer-default-export */ /* eslint-disable import/prefer-default-export */
import _ from 'underscore';
/**
* @param {Array} queryResults - Array of Result objects
* @param {Object} defaultConfig - Default chart config values (e.g. lineStyle, name)
* @returns {Array} The formatted values
*/
export const makeDataSeries = (queryResults, defaultConfig) => export const makeDataSeries = (queryResults, defaultConfig) =>
queryResults.reduce((acc, result) => { queryResults
.map(result => {
const data = result.values.filter(([, value]) => !Number.isNaN(value)); const data = result.values.filter(([, value]) => !Number.isNaN(value));
if (!data.length) { if (!data.length) {
return acc; return null;
} }
const relevantMetric = defaultConfig.name.toLowerCase().replace(' ', '_'); const relevantMetric = defaultConfig.name.toLowerCase().replace(' ', '_');
const name = result.metric[relevantMetric]; const name = result.metric[relevantMetric];
const series = { data }; const series = { data };
if (name) { if (name) {
series.name = `${defaultConfig.name}: ${name}`; series.name = `${defaultConfig.name}: ${name}`;
} else {
const template = _.template(defaultConfig.name, {
interpolate: /\{\{(.+?)\}\}/g,
});
series.name = template(result.metric);
} }
return acc.concat({ ...defaultConfig, ...series }); return { ...defaultConfig, ...series };
}, []); })
.filter(series => series !== null);
...@@ -7,7 +7,7 @@ import { s__, __ } from '../../locale'; ...@@ -7,7 +7,7 @@ import { s__, __ } from '../../locale';
const MAX_REQUESTS = 3; const MAX_REQUESTS = 3;
function backOffRequest(makeRequestCallback) { export function backOffRequest(makeRequestCallback) {
let requestCounter = 0; let requestCounter = 0;
return backOff((next, stop) => { return backOff((next, stop) => {
makeRequestCallback() makeRequestCallback()
...@@ -111,8 +111,7 @@ export const fetchDashboard = ({ state, dispatch }, params) => { ...@@ -111,8 +111,7 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
params.dashboard = state.currentDashboard; params.dashboard = state.currentDashboard;
} }
return axios return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
.get(state.dashboardEndpoint, { params })
.then(resp => resp.data) .then(resp => resp.data)
.then(response => { .then(response => {
dispatch('receiveMetricsDashboardSuccess', { response, params }); dispatch('receiveMetricsDashboardSuccess', { response, params });
......
...@@ -41,5 +41,76 @@ describe('monitor helper', () => { ...@@ -41,5 +41,76 @@ describe('monitor helper', () => {
), ),
).toEqual([{ ...expectedDataSeries[0], data: [[1, 1]] }]); ).toEqual([{ ...expectedDataSeries[0], data: [[1, 1]] }]);
}); });
it('updates series name from templates', () => {
const config = {
...defaultConfig,
name: '{{cmd}}',
};
const [result] = monitorHelper.makeDataSeries(
[{ metric: { cmd: 'brpop' }, values: series }],
config,
);
expect(result.name).toEqual('brpop');
});
it('supports space-padded template expressions', () => {
const config = {
...defaultConfig,
name: 'backend: {{ backend }}',
};
const [result] = monitorHelper.makeDataSeries(
[{ metric: { backend: 'HA Server' }, values: series }],
config,
);
expect(result.name).toEqual('backend: HA Server');
});
it('supports repeated template variables', () => {
const config = { ...defaultConfig, name: '{{cmd}}, {{cmd}}' };
const [result] = monitorHelper.makeDataSeries(
[{ metric: { cmd: 'brpop' }, values: series }],
config,
);
expect(result.name).toEqual('brpop, brpop');
});
it('updates multiple series names from templates', () => {
const config = {
...defaultConfig,
name: '{{job}}: {{cmd}}',
};
const [result] = monitorHelper.makeDataSeries(
[{ metric: { cmd: 'brpop', job: 'redis' }, values: series }],
config,
);
expect(result.name).toEqual('redis: brpop');
});
it('updates name for each series', () => {
const config = {
...defaultConfig,
name: '{{cmd}}',
};
const [firstSeries, secondSeries] = monitorHelper.makeDataSeries(
[
{ metric: { cmd: 'brpop' }, values: series },
{ metric: { cmd: 'zrangebyscore' }, values: series },
],
config,
);
expect(firstSeries.name).toEqual('brpop');
expect(secondSeries.name).toEqual('zrangebyscore');
});
}); });
}); });
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import { backOffRequest } from '~/monitoring/stores/actions';
import statusCodes from '~/lib/utils/http_status';
import { backOff } from '~/lib/utils/common_utils';
jest.mock('~/lib/utils/common_utils');
const MAX_REQUESTS = 3;
describe('Monitoring store helpers', () => {
let mock;
// Mock underlying `backOff` function to remove in-built delay.
backOff.mockImplementation(
callback =>
new Promise((resolve, reject) => {
const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
const next = () => callback(next, stop);
callback(next, stop);
}),
);
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('backOffRequest', () => {
it('returns immediately when recieving a 200 status code', () => {
mock.onGet(TEST_HOST).reply(200);
return backOffRequest(() => axios.get(TEST_HOST)).then(() => {
expect(mock.history.get.length).toBe(1);
});
});
it(`repeats the network call ${MAX_REQUESTS} times when receiving a 204 response`, done => {
mock.onGet(TEST_HOST).reply(statusCodes.NO_CONTENT, {});
backOffRequest(() => axios.get(TEST_HOST))
.then(done.fail)
.catch(() => {
expect(mock.history.get.length).toBe(MAX_REQUESTS);
done();
});
});
});
});
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