summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/monitoring/stores
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 12:26:25 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-07-20 12:26:25 +0000
commita09983ae35713f5a2bbb100981116d31ce99826e (patch)
tree2ee2af7bd104d57086db360a7e6d8c9d5d43667a /app/assets/javascripts/monitoring/stores
parent18c5ab32b738c0b6ecb4d0df3994000482f34bd8 (diff)
downloadgitlab-ce-a09983ae35713f5a2bbb100981116d31ce99826e.tar.gz
Add latest changes from gitlab-org/gitlab@13-2-stable-ee
Diffstat (limited to 'app/assets/javascripts/monitoring/stores')
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js124
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js35
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js13
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js67
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js31
-rw-r--r--app/assets/javascripts/monitoring/stores/utils.js198
-rw-r--r--app/assets/javascripts/monitoring/stores/variable_mapping.js158
7 files changed, 494 insertions, 132 deletions
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 3a9cccec438..a441882a47d 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -12,6 +12,7 @@ import {
import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
import getAnnotations from '../queries/getAnnotations.query.graphql';
+import getDashboardValidationWarnings from '../queries/getDashboardValidationWarnings.query.graphql';
import statusCodes from '../../lib/utils/http_status';
import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
@@ -20,6 +21,7 @@ import {
PROMETHEUS_TIMEOUT,
ENVIRONMENT_AVAILABLE_STATE,
DEFAULT_DASHBOARD_PATH,
+ VARIABLE_TYPES,
} from '../constants';
function prometheusMetricQueryParams(timeRange) {
@@ -50,15 +52,14 @@ function backOffRequest(makeRequestCallback) {
}, PROMETHEUS_TIMEOUT);
}
-function getPrometheusMetricResult(prometheusEndpoint, params) {
+function getPrometheusQueryData(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;
+ return response.data;
});
}
@@ -76,10 +77,6 @@ export const setTimeRange = ({ commit }, timeRange) => {
commit(types.SET_TIME_RANGE, timeRange);
};
-export const setVariables = ({ commit }, variables) => {
- commit(types.SET_VARIABLES, variables);
-};
-
export const filterEnvironments = ({ commit, dispatch }, searchTerm) => {
commit(types.SET_ENVIRONMENTS_FILTER, searchTerm);
dispatch('fetchEnvironmentsData');
@@ -100,6 +97,10 @@ export const clearExpandedPanel = ({ commit }) => {
});
};
+export const setCurrentDashboard = ({ commit }, { currentDashboard }) => {
+ commit(types.SET_CURRENT_DASHBOARD, currentDashboard);
+};
+
// All Data
/**
@@ -117,17 +118,27 @@ export const fetchData = ({ dispatch }) => {
// Metrics dashboard
-export const fetchDashboard = ({ state, commit, dispatch }) => {
+export const fetchDashboard = ({ state, commit, dispatch, getters }) => {
dispatch('requestMetricsDashboard');
const params = {};
- if (state.currentDashboard) {
- params.dashboard = state.currentDashboard;
+ if (getters.fullDashboardPath) {
+ params.dashboard = getters.fullDashboardPath;
}
return backOffRequest(() => axios.get(state.dashboardEndpoint, { params }))
.then(resp => resp.data)
- .then(response => dispatch('receiveMetricsDashboardSuccess', { response }))
+ .then(response => {
+ dispatch('receiveMetricsDashboardSuccess', { response });
+ /**
+ * After the dashboard is fetched, there can be non-blocking invalid syntax
+ * in the dashboard file. This call will fetch such syntax warnings
+ * and surface a warning on the UI. If the invalid syntax is blocking,
+ * the `fetchDashboard` returns a 404 with error messages that are displayed
+ * on the UI.
+ */
+ dispatch('fetchDashboardValidationWarnings');
+ })
.catch(error => {
Sentry.captureException(error);
@@ -181,8 +192,12 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
return Promise.reject();
}
+ // Time range params must be pre-calculated once for all metrics and options
+ // A subsequent call, may calculate a different time range
const defaultQueryParams = prometheusMetricQueryParams(state.timeRange);
+ dispatch('fetchVariableMetricLabelValues', { defaultQueryParams });
+
const promises = [];
state.dashboard.panelGroups.forEach(group => {
group.panels.forEach(panel => {
@@ -194,7 +209,7 @@ export const fetchDashboardData = ({ state, dispatch, getters }) => {
return Promise.all(promises)
.then(() => {
- const dashboardType = state.currentDashboard === '' ? 'default' : 'custom';
+ const dashboardType = getters.fullDashboardPath === '' ? 'default' : 'custom';
trackDashboardLoad({
label: `${dashboardType}_metrics_dashboard`,
value: getters.metricsWithData().length,
@@ -220,7 +235,7 @@ export const fetchPrometheusMetric = (
queryParams.step = metric.step;
}
- if (Object.keys(state.variables).length > 0) {
+ if (state.variables.length > 0) {
queryParams = {
...queryParams,
...getters.getCustomVariablesParams,
@@ -229,9 +244,9 @@ export const fetchPrometheusMetric = (
commit(types.REQUEST_METRIC_RESULT, { metricId: metric.metricId });
- return getPrometheusMetricResult(metric.prometheusEndpointPath, queryParams)
- .then(result => {
- commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metricId, result });
+ return getPrometheusQueryData(metric.prometheusEndpointPath, queryParams)
+ .then(data => {
+ commit(types.RECEIVE_METRIC_RESULT_SUCCESS, { metricId: metric.metricId, data });
})
.catch(error => {
Sentry.captureException(error);
@@ -312,9 +327,9 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE);
};
-export const fetchAnnotations = ({ state, dispatch }) => {
+export const fetchAnnotations = ({ state, dispatch, getters }) => {
const { start } = convertToFixedRange(state.timeRange);
- const dashboardPath = state.currentDashboard || DEFAULT_DASHBOARD_PATH;
+ const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
return gqClient
.mutate({
mutation: getAnnotations,
@@ -345,6 +360,46 @@ export const receiveAnnotationsSuccess = ({ commit }, data) =>
commit(types.RECEIVE_ANNOTATIONS_SUCCESS, data);
export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_ANNOTATIONS_FAILURE);
+export const fetchDashboardValidationWarnings = ({ state, dispatch, getters }) => {
+ /**
+ * Normally, the default dashboard won't throw any validation warnings.
+ *
+ * However, if a bug sneaks into the default dashboard making it invalid,
+ * this might come handy for our clients
+ */
+ const dashboardPath = getters.fullDashboardPath || DEFAULT_DASHBOARD_PATH;
+ return gqClient
+ .mutate({
+ mutation: getDashboardValidationWarnings,
+ variables: {
+ projectPath: removeLeadingSlash(state.projectPath),
+ environmentName: state.currentEnvironmentName,
+ dashboardPath,
+ },
+ })
+ .then(resp => resp.data?.project?.environments?.nodes?.[0]?.metricsDashboard)
+ .then(({ schemaValidationWarnings } = {}) => {
+ const hasWarnings = schemaValidationWarnings && schemaValidationWarnings.length !== 0;
+ /**
+ * The payload of the dispatch is a boolean, because at the moment a standard
+ * warning message is shown instead of the warnings the BE returns
+ */
+ dispatch('receiveDashboardValidationWarningsSuccess', hasWarnings || false);
+ })
+ .catch(err => {
+ Sentry.captureException(err);
+ dispatch('receiveDashboardValidationWarningsFailure');
+ createFlash(
+ s__('Metrics|There was an error getting dashboard validation warnings information.'),
+ );
+ });
+};
+
+export const receiveDashboardValidationWarningsSuccess = ({ commit }, hasWarnings) =>
+ commit(types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS, hasWarnings);
+export const receiveDashboardValidationWarningsFailure = ({ commit }) =>
+ commit(types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_FAILURE);
+
// Dashboard manipulation
export const toggleStarredValue = ({ commit, state, getters }) => {
@@ -416,10 +471,41 @@ export const duplicateSystemDashboard = ({ state }, payload) => {
// Variables manipulation
export const updateVariablesAndFetchData = ({ commit, dispatch }, updatedVariable) => {
- commit(types.UPDATE_VARIABLES, updatedVariable);
+ commit(types.UPDATE_VARIABLE_VALUE, updatedVariable);
return dispatch('fetchDashboardData');
};
+export const fetchVariableMetricLabelValues = ({ state, commit }, { defaultQueryParams }) => {
+ const { start_time, end_time } = defaultQueryParams;
+ const optionsRequests = [];
+
+ state.variables.forEach(variable => {
+ if (variable.type === VARIABLE_TYPES.metric_label_values) {
+ const { prometheusEndpointPath, label } = variable.options;
+
+ const optionsRequest = backOffRequest(() =>
+ axios.get(prometheusEndpointPath, {
+ params: { start_time, end_time },
+ }),
+ )
+ .then(({ data }) => data.data)
+ .then(data => {
+ commit(types.UPDATE_VARIABLE_METRIC_LABEL_VALUES, { variable, label, data });
+ })
+ .catch(() => {
+ createFlash(
+ sprintf(s__('Metrics|There was an error getting options for variable "%{name}".'), {
+ name: variable.name,
+ }),
+ );
+ });
+ optionsRequests.push(optionsRequest);
+ }
+ });
+
+ return Promise.all(optionsRequests);
+};
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index b7681012472..3aa711a0509 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -1,5 +1,9 @@
import { NOT_IN_DB_PREFIX } from '../constants';
-import { addPrefixToCustomVariableParams, addDashboardMetaDataToLink } from './utils';
+import {
+ addPrefixToCustomVariableParams,
+ addDashboardMetaDataToLink,
+ normalizeCustomDashboardPath,
+} from './utils';
const metricsIdsInPanel = panel =>
panel.metrics.filter(metric => metric.metricId && metric.result).map(metric => metric.metricId);
@@ -10,10 +14,10 @@ const metricsIdsInPanel = panel =>
*
* @param {Object} state
*/
-export const selectedDashboard = state => {
+export const selectedDashboard = (state, getters) => {
const { allDashboards } = state;
return (
- allDashboards.find(d => d.path === state.currentDashboard) ||
+ allDashboards.find(d => d.path === getters.fullDashboardPath) ||
allDashboards.find(d => d.default) ||
null
);
@@ -129,8 +133,8 @@ export const linksWithMetadata = state => {
};
/**
- * Maps an variables object to an array along with stripping
- * the variable prefix.
+ * Maps a variables array to an object for replacement in
+ * prometheus queries.
*
* This method outputs an object in the below format
*
@@ -143,16 +147,29 @@ export const linksWithMetadata = state => {
* user-defined variables coming through the URL and differentiate
* from other variables used for Prometheus API endpoint.
*
- * @param {Object} variables - Custom variables provided by the user
- * @returns {Array} The custom variables array to be send to the API
+ * @param {Object} state - State containing variables provided by the user
+ * @returns {Array} The custom variables object to be send to the API
* in the format of {variables[key1]=value1, variables[key2]=value2}
*/
export const getCustomVariablesParams = state =>
- Object.keys(state.variables).reduce((acc, variable) => {
- acc[addPrefixToCustomVariableParams(variable)] = state.variables[variable]?.value;
+ state.variables.reduce((acc, variable) => {
+ const { name, value } = variable;
+ if (value !== null) {
+ acc[addPrefixToCustomVariableParams(name)] = value;
+ }
return acc;
}, {});
+/**
+ * For a given custom dashboard file name, this method
+ * returns the full file path.
+ *
+ * @param {Object} state
+ * @returns {String} full dashboard path
+ */
+export const fullDashboardPath = state =>
+ normalizeCustomDashboardPath(state.currentDashboard, state.customDashboardBasePath);
+
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 4593461776b..d408628fc4d 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -2,17 +2,25 @@
export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
-export const SET_VARIABLES = 'SET_VARIABLES';
-export const UPDATE_VARIABLES = 'UPDATE_VARIABLES';
+export const UPDATE_VARIABLE_VALUE = 'UPDATE_VARIABLE_VALUE';
+export const UPDATE_VARIABLE_METRIC_LABEL_VALUES = 'UPDATE_VARIABLE_METRIC_LABEL_VALUES';
export const REQUEST_DASHBOARD_STARRING = 'REQUEST_DASHBOARD_STARRING';
export const RECEIVE_DASHBOARD_STARRING_SUCCESS = 'RECEIVE_DASHBOARD_STARRING_SUCCESS';
export const RECEIVE_DASHBOARD_STARRING_FAILURE = 'RECEIVE_DASHBOARD_STARRING_FAILURE';
+export const SET_CURRENT_DASHBOARD = 'SET_CURRENT_DASHBOARD';
+
// Annotations
export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
export const RECEIVE_ANNOTATIONS_FAILURE = 'RECEIVE_ANNOTATIONS_FAILURE';
+// Dashboard validation warnings
+export const RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS =
+ 'RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS';
+export const RECEIVE_DASHBOARD_VALIDATION_WARNINGS_FAILURE =
+ 'RECEIVE_DASHBOARD_VALIDATION_WARNINGS_FAILURE';
+
// Git project deployments
export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA';
export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS';
@@ -34,7 +42,6 @@ export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_INITIAL_STATE = 'SET_INITIAL_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';
export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
export const SET_PANEL_GROUP_METRICS = 'SET_PANEL_GROUP_METRICS';
export const SET_ENVIRONMENTS_FILTER = 'SET_ENVIRONMENTS_FILTER';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 2d63fdd6e34..744441c8935 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -1,10 +1,11 @@
import Vue from 'vue';
import { pick } from 'lodash';
import * as types from './mutation_types';
-import { mapToDashboardViewModel, normalizeQueryResult } from './utils';
-import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
-import { endpointKeys, initialStateKeys, metricStates } from '../constants';
+import { mapToDashboardViewModel, normalizeQueryResponseData } from './utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import { BACKOFF_TIMEOUT } from '../../lib/utils/common_utils';
+import { dashboardEmptyStates, endpointKeys, initialStateKeys, metricStates } from '../constants';
+import { optionsFromSeriesData } from './variable_mapping';
/**
* Locate and return a metric in the dashboard by its id
@@ -57,8 +58,7 @@ export default {
* Dashboard panels structure and global state
*/
[types.REQUEST_METRICS_DASHBOARD](state) {
- state.emptyState = 'loading';
- state.showEmptyState = true;
+ state.emptyState = dashboardEmptyStates.LOADING;
},
[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, dashboardYML) {
const { dashboard, panelGroups, variables, links } = mapToDashboardViewModel(dashboardYML);
@@ -70,12 +70,15 @@ export default {
state.links = links;
if (!state.dashboard.panelGroups.length) {
- state.emptyState = 'noData';
+ state.emptyState = dashboardEmptyStates.NO_DATA;
+ } else {
+ state.emptyState = null;
}
},
[types.RECEIVE_METRICS_DASHBOARD_FAILURE](state, error) {
- state.emptyState = error ? 'unableToConnect' : 'noData';
- state.showEmptyState = true;
+ state.emptyState = error
+ ? dashboardEmptyStates.UNABLE_TO_CONNECT
+ : dashboardEmptyStates.NO_DATA;
},
[types.REQUEST_DASHBOARD_STARRING](state) {
@@ -94,6 +97,10 @@ export default {
state.isUpdatingStarredValue = false;
},
+ [types.SET_CURRENT_DASHBOARD](state, currentDashboard) {
+ state.currentDashboard = currentDashboard;
+ },
+
/**
* Deployments and environments
*/
@@ -126,6 +133,16 @@ export default {
},
/**
+ * Dashboard Validation Warnings
+ */
+ [types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS](state, hasDashboardValidationWarnings) {
+ state.hasDashboardValidationWarnings = hasDashboardValidationWarnings;
+ },
+ [types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_FAILURE](state) {
+ state.hasDashboardValidationWarnings = false;
+ },
+
+ /**
* Individual panel/metric results
*/
[types.REQUEST_METRIC_RESULT](state, { metricId }) {
@@ -135,19 +152,18 @@ export default {
metric.state = metricStates.LOADING;
}
},
- [types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, result }) {
+ [types.RECEIVE_METRIC_RESULT_SUCCESS](state, { metricId, data }) {
const metric = findMetricInDashboard(metricId, state.dashboard);
metric.loading = false;
- state.showEmptyState = false;
- if (!result || result.length === 0) {
+ if (!data.result || data.result.length === 0) {
metric.state = metricStates.NO_DATA;
metric.result = null;
} else {
- const normalizedResults = result.map(normalizeQueryResult);
+ const result = normalizeQueryResponseData(data);
metric.state = metricStates.OK;
- metric.result = Object.freeze(normalizedResults);
+ metric.result = Object.freeze(result);
}
},
[types.RECEIVE_METRIC_RESULT_FAILURE](state, { metricId, error }) {
@@ -169,11 +185,7 @@ export default {
state.timeRange = timeRange;
},
[types.SET_GETTING_STARTED_EMPTY_STATE](state) {
- state.emptyState = 'gettingStarted';
- },
- [types.SET_NO_DATA_EMPTY_STATE](state) {
- state.showEmptyState = true;
- state.emptyState = 'noData';
+ state.emptyState = dashboardEmptyStates.GETTING_STARTED;
},
[types.SET_ALL_DASHBOARDS](state, dashboards) {
state.allDashboards = dashboards || [];
@@ -192,13 +204,18 @@ export default {
state.expandedPanel.group = group;
state.expandedPanel.panel = panel;
},
- [types.SET_VARIABLES](state, variables) {
- state.variables = variables;
+ [types.UPDATE_VARIABLE_VALUE](state, { name, value }) {
+ const variable = state.variables.find(v => v.name === name);
+ if (variable) {
+ Object.assign(variable, {
+ value,
+ });
+ }
},
- [types.UPDATE_VARIABLES](state, updatedVariable) {
- Object.assign(state.variables[updatedVariable.key], {
- ...state.variables[updatedVariable.key],
- value: updatedVariable.value,
- });
+ [types.UPDATE_VARIABLE_METRIC_LABEL_VALUES](state, { variable, label, data = [] }) {
+ const values = optionsFromSeriesData({ label, data });
+
+ // Add new options with assign to ensure Vue reactivity
+ Object.assign(variable.options, { values });
},
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index 8000f27c0d5..89738756ffe 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -1,5 +1,6 @@
import invalidUrl from '~/lib/utils/invalid_url';
import { timezones } from '../format_date';
+import { dashboardEmptyStates } from '../constants';
export default () => ({
// API endpoints
@@ -9,11 +10,24 @@ export default () => ({
// Dashboard request parameters
timeRange: null,
+ /**
+ * Currently selected dashboard. For custom dashboards,
+ * this could be the filename or the file path.
+ *
+ * If this is the filename and full path is required,
+ * getters.fullDashboardPath should be used.
+ */
currentDashboard: null,
// Dashboard data
- emptyState: 'gettingStarted',
- showEmptyState: true,
+ hasDashboardValidationWarnings: false,
+
+ /**
+ * {?String} If set, dashboard should display a global
+ * empty state, there is no way to interact (yet)
+ * with the dashboard.
+ */
+ emptyState: dashboardEmptyStates.GETTING_STARTED,
showErrorBanner: true,
isUpdatingStarredValue: false,
dashboard: {
@@ -39,7 +53,7 @@ export default () => ({
* User-defined custom variables are passed
* via the dashboard yml file.
*/
- variables: {},
+ variables: [],
/**
* User-defined custom links are passed
* via the dashboard yml file.
@@ -56,5 +70,16 @@ export default () => ({
// GitLab paths to other pages
projectPath: null,
+ operationsSettingsPath: '',
logsPath: invalidUrl,
+
+ // static paths
+ customDashboardBasePath: '',
+
+ // current user data
+ /**
+ * Flag that denotes if the currently logged user can access
+ * the project Settings -> Operations
+ */
+ canAccessOperationsSettings: false,
});
diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js
index 5795e756282..51562593ee8 100644
--- a/app/assets/javascripts/monitoring/stores/utils.js
+++ b/app/assets/javascripts/monitoring/stores/utils.js
@@ -2,11 +2,11 @@ import { slugify } from '~/lib/utils/text_utility';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { NOT_IN_DB_PREFIX, linkTypes } from '../constants';
import { mergeURLVariables, parseTemplatingVariables } from './variable_mapping';
import { DATETIME_RANGE_TYPES } from '~/lib/utils/constants';
import { timeRangeToParams, getRangeType } from '~/lib/utils/datetime_range';
import { isSafeURL, mergeUrlParams } from '~/lib/utils/url_utility';
+import { NOT_IN_DB_PREFIX, linkTypes, OUT_OF_THE_BOX_DASHBOARDS_PATH_PREFIX } from '../constants';
export const gqClient = createGqClient(
{},
@@ -165,7 +165,7 @@ const mapLinksToViewModel = ({ url = null, title = '', type } = {}) => {
* @param {Object} panel - Metrics panel
* @returns {Object}
*/
-const mapPanelToViewModel = ({
+export const mapPanelToViewModel = ({
id = null,
title = '',
type,
@@ -173,6 +173,7 @@ const mapPanelToViewModel = ({
x_label,
y_label,
y_axis = {},
+ field,
metrics = [],
links = [],
max_value,
@@ -193,6 +194,7 @@ const mapPanelToViewModel = ({
y_label: yAxis.name, // Changing y_label to yLabel is pending https://gitlab.com/gitlab-org/gitlab/issues/207198
yAxis,
xAxis,
+ field,
maxValue: max_value,
links: links.map(mapLinksToViewModel),
metrics: mapToMetricsViewModel(metrics),
@@ -289,49 +291,157 @@ export const mapToDashboardViewModel = ({
}) => {
return {
dashboard,
- variables: mergeURLVariables(parseTemplatingVariables(templating)),
+ variables: mergeURLVariables(parseTemplatingVariables(templating.variables)),
links: links.map(mapLinksToViewModel),
panelGroups: panel_groups.map(mapToPanelGroupViewModel),
};
};
+// Prometheus Results Parsing
+
+const dateTimeFromUnixTime = unixTime => new Date(unixTime * 1000).toISOString();
+
+const mapScalarValue = ([unixTime, value]) => [dateTimeFromUnixTime(unixTime), Number(value)];
+
+// Note: `string` value type is unused as of prometheus 2.19.
+const mapStringValue = ([unixTime, value]) => [dateTimeFromUnixTime(unixTime), value];
+
+/**
+ * Processes a scalar result.
+ *
+ * The corresponding result property has the following format:
+ *
+ * [ <unix_time>, "<scalar_value>" ]
+ *
+ * @param {array} result
+ * @returns {array}
+ */
+const normalizeScalarResult = result => [
+ {
+ metric: {},
+ value: mapScalarValue(result),
+ values: [mapScalarValue(result)],
+ },
+];
+
+/**
+ * Processes a string result.
+ *
+ * The corresponding result property has the following format:
+ *
+ * [ <unix_time>, "<string_value>" ]
+ *
+ * Note: This value type is unused as of prometheus 2.19.
+ *
+ * @param {array} result
+ * @returns {array}
+ */
+const normalizeStringResult = result => [
+ {
+ metric: {},
+ value: mapStringValue(result),
+ values: [mapStringValue(result)],
+ },
+];
+
+/**
+ * Proccesses an instant vector.
+ *
+ * Instant vectors are returned as result type `vector`.
+ *
+ * The corresponding result property has the following format:
+ *
+ * [
+ * {
+ * "metric": { "<label_name>": "<label_value>", ... },
+ * "value": [ <unix_time>, "<sample_value>" ],
+ * "values": [ [ <unix_time>, "<sample_value>" ] ]
+ * },
+ * ...
+ * ]
+ *
+ * `metric` - Key-value pairs object representing metric measured
+ * `value` - The vector result
+ * `values` - An array with a single value representing the result
+ *
+ * This method also adds the matrix version of the vector
+ * by introducing a `values` array with a single element. This
+ * allows charts to default to `values` if needed.
+ *
+ * @param {array} result
+ * @returns {array}
+ */
+const normalizeVectorResult = result =>
+ result.map(({ metric, value }) => {
+ const scalar = mapScalarValue(value);
+ // Add a single element to `values`, to support matrix
+ // style charts.
+ return { metric, value: scalar, values: [scalar] };
+ });
+
/**
- * Processes a single Range vector, part of the result
- * of type `matrix` in the form:
+ * Range vectors are returned as result type matrix.
+ *
+ * The corresponding result property has the following format:
*
* {
* "metric": { "<label_name>": "<label_value>", ... },
+ * "value": [ <unix_time>, "<sample_value>" ],
* "values": [ [ <unix_time>, "<sample_value>" ], ... ]
* },
*
+ * `metric` - Key-value pairs object representing metric measured
+ * `value` - The last (more recent) result
+ * `values` - A range of results for the metric
+ *
* See https://prometheus.io/docs/prometheus/latest/querying/api/#range-vectors
*
- * @param {*} timeSeries
+ * @param {array} result
+ * @returns {object} Normalized result.
*/
-export const normalizeQueryResult = timeSeries => {
- let normalizedResult = {};
-
- if (timeSeries.values) {
- normalizedResult = {
- ...timeSeries,
- values: timeSeries.values.map(([timestamp, value]) => [
- new Date(timestamp * 1000).toISOString(),
- Number(value),
- ]),
- };
- // Check result for empty data
- normalizedResult.values = normalizedResult.values.filter(series => {
- const hasValue = d => !Number.isNaN(d[1]) && (d[1] !== null || d[1] !== undefined);
- return series.find(hasValue);
- });
- } else if (timeSeries.value) {
- normalizedResult = {
- ...timeSeries,
- value: [new Date(timeSeries.value[0] * 1000).toISOString(), Number(timeSeries.value[1])],
+const normalizeResultMatrix = result =>
+ result.map(({ metric, values }) => {
+ const mappedValues = values.map(mapScalarValue);
+ return {
+ metric,
+ value: mappedValues[mappedValues.length - 1],
+ values: mappedValues,
};
- }
+ });
- return normalizedResult;
+/**
+ * Parse response data from a Prometheus Query that comes
+ * in the format:
+ *
+ * {
+ * "resultType": "matrix" | "vector" | "scalar" | "string",
+ * "result": <value>
+ * }
+ *
+ * @see https://prometheus.io/docs/prometheus/latest/querying/api/#expression-query-result-formats
+ *
+ * @param {object} data - Data containing results and result type.
+ * @returns {object} - A result array of metric results:
+ * [
+ * {
+ * metric: { ... },
+ * value: ['2015-07-01T20:10:51.781Z', '1'],
+ * values: [['2015-07-01T20:10:51.781Z', '1'] , ... ],
+ * },
+ * ...
+ * ]
+ *
+ */
+export const normalizeQueryResponseData = data => {
+ const { resultType, result } = data;
+ if (resultType === 'vector') {
+ return normalizeVectorResult(result);
+ } else if (resultType === 'scalar') {
+ return normalizeScalarResult(result);
+ } else if (resultType === 'string') {
+ return normalizeStringResult(result);
+ }
+ return normalizeResultMatrix(result);
};
/**
@@ -345,7 +455,35 @@ export const normalizeQueryResult = timeSeries => {
*
* This is currently only used by getters/getCustomVariablesParams
*
- * @param {String} key Variable key that needs to be prefixed
+ * @param {String} name Variable key that needs to be prefixed
* @returns {String}
*/
-export const addPrefixToCustomVariableParams = key => `variables[${key}]`;
+export const addPrefixToCustomVariableParams = name => `variables[${name}]`;
+
+/**
+ * Normalize custom dashboard paths. This method helps support
+ * metrics dashboard to work with custom dashboard file names instead
+ * of the entire path.
+ *
+ * If dashboard is empty, it is the default dashboard.
+ * If dashboard is set, it usually is a custom dashboard unless
+ * explicitly it is set to default dashboard path.
+ *
+ * @param {String} dashboard dashboard path
+ * @param {String} dashboardPrefix custom dashboard directory prefix
+ * @returns {String} normalized dashboard path
+ */
+export const normalizeCustomDashboardPath = (dashboard, dashboardPrefix = '') => {
+ const currDashboard = dashboard || '';
+ let dashboardPath = `${dashboardPrefix}/${currDashboard}`;
+
+ if (!currDashboard) {
+ dashboardPath = '';
+ } else if (
+ currDashboard.startsWith(dashboardPrefix) ||
+ currDashboard.startsWith(OUT_OF_THE_BOX_DASHBOARDS_PATH_PREFIX)
+ ) {
+ dashboardPath = currDashboard;
+ }
+ return dashboardPath;
+};
diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js
index c0a8150063b..9245ffdb3b9 100644
--- a/app/assets/javascripts/monitoring/stores/variable_mapping.js
+++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js
@@ -46,7 +46,7 @@ const textAdvancedVariableParser = advTextVar => ({
* @param {Object} custom variable option
* @returns {Object} normalized custom variable options
*/
-const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, value }) => ({
+const normalizeVariableValues = ({ default: defaultOpt = false, text, value = null }) => ({
default: defaultOpt,
text: text || value,
value,
@@ -59,17 +59,19 @@ const normalizeCustomVariableOptions = ({ default: defaultOpt = false, text, val
* The default value is the option with default set to true or the first option
* if none of the options have default prop true.
*
- * @param {Object} advVariable advance custom variable
+ * @param {Object} advVariable advanced custom variable
* @returns {Object}
*/
const customAdvancedVariableParser = advVariable => {
- const options = (advVariable?.options?.values ?? []).map(normalizeCustomVariableOptions);
- const defaultOpt = options.find(opt => opt.default === true) || options[0];
+ const values = (advVariable?.options?.values ?? []).map(normalizeVariableValues);
+ const defaultValue = values.find(opt => opt.default === true) || values[0];
return {
type: VARIABLE_TYPES.custom,
label: advVariable.label,
- value: defaultOpt?.value,
- options,
+ options: {
+ values,
+ },
+ value: defaultValue?.value || null,
};
};
@@ -80,7 +82,7 @@ const customAdvancedVariableParser = advVariable => {
* @param {String} opt option from simple custom variable
* @returns {Object}
*/
-const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
+export const parseSimpleCustomValues = opt => ({ text: opt, value: opt });
/**
* Custom simple variables are rendered as dropdown elements in the dashboard
@@ -95,15 +97,28 @@ const parseSimpleCustomOptions = opt => ({ text: opt, value: opt });
* @returns {Object}
*/
const customSimpleVariableParser = simpleVar => {
- const options = (simpleVar || []).map(parseSimpleCustomOptions);
+ const values = (simpleVar || []).map(parseSimpleCustomValues);
return {
type: VARIABLE_TYPES.custom,
- value: options[0].value,
label: null,
- options: options.map(normalizeCustomVariableOptions),
+ value: values[0].value || null,
+ options: {
+ values: values.map(normalizeVariableValues),
+ },
};
};
+const metricLabelValuesVariableParser = ({ label, options = {} }) => ({
+ type: VARIABLE_TYPES.metric_label_values,
+ label,
+ value: null,
+ options: {
+ prometheusEndpointPath: options.prometheus_endpoint_path || '',
+ label: options.label || null,
+ values: [], // values are initially empty
+ },
+});
+
/**
* Utility method to determine if a custom variable is
* simple or not. If its not simple, it is advanced.
@@ -123,14 +138,16 @@ const isSimpleCustomVariable = customVar => Array.isArray(customVar);
* @return {Function} parser method
*/
const getVariableParser = variable => {
- if (isSimpleCustomVariable(variable)) {
+ if (isString(variable)) {
+ return textSimpleVariableParser;
+ } else if (isSimpleCustomVariable(variable)) {
return customSimpleVariableParser;
- } else if (variable.type === VARIABLE_TYPES.custom) {
- return customAdvancedVariableParser;
} else if (variable.type === VARIABLE_TYPES.text) {
return textAdvancedVariableParser;
- } else if (isString(variable)) {
- return textSimpleVariableParser;
+ } else if (variable.type === VARIABLE_TYPES.custom) {
+ return customAdvancedVariableParser;
+ } else if (variable.type === VARIABLE_TYPES.metric_label_values) {
+ return metricLabelValuesVariableParser;
}
return () => null;
};
@@ -141,29 +158,26 @@ const getVariableParser = variable => {
* for the user to edit. The values from input elements are relayed to
* backend and eventually Prometheus API.
*
- * This method currently is not used anywhere. Once the issue
- * https://gitlab.com/gitlab-org/gitlab/-/issues/214536 is completed,
- * this method will have been used by the monitoring dashboard.
- *
- * @param {Object} templating templating variables from the dashboard yml file
- * @returns {Object} a map of processed templating variables
+ * @param {Object} templating variables from the dashboard yml file
+ * @returns {array} An array of variables to display as inputs
*/
-export const parseTemplatingVariables = ({ variables = {} } = {}) =>
- Object.entries(variables).reduce((acc, [key, variable]) => {
+export const parseTemplatingVariables = (ymlVariables = {}) =>
+ Object.entries(ymlVariables).reduce((acc, [name, ymlVariable]) => {
// get the parser
- const parser = getVariableParser(variable);
+ const parser = getVariableParser(ymlVariable);
// parse the variable
- const parsedVar = parser(variable);
+ const variable = parser(ymlVariable);
// for simple custom variable label is null and it should be
// replace with key instead
- if (parsedVar) {
- acc[key] = {
- ...parsedVar,
- label: parsedVar.label || key,
- };
+ if (variable) {
+ acc.push({
+ ...variable,
+ name,
+ label: variable.label || name,
+ });
}
return acc;
- }, {});
+ }, []);
/**
* Custom variables are defined in the dashboard yml file
@@ -181,23 +195,81 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) =>
* This method can be improved further. See the below issue
* https://gitlab.com/gitlab-org/gitlab/-/issues/217713
*
- * @param {Object} varsFromYML template variables from yml file
+ * @param {array} parsedYmlVariables - template variables from yml file
* @returns {Object}
*/
-export const mergeURLVariables = (varsFromYML = {}) => {
+export const mergeURLVariables = (parsedYmlVariables = []) => {
const varsFromURL = templatingVariablesFromUrl();
- const variables = {};
- Object.keys(varsFromYML).forEach(key => {
- if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) {
- variables[key] = {
- ...varsFromYML[key],
- value: varsFromURL[key],
- };
- } else {
- variables[key] = varsFromYML[key];
+ parsedYmlVariables.forEach(variable => {
+ const { name } = variable;
+ if (Object.prototype.hasOwnProperty.call(varsFromURL, name)) {
+ Object.assign(variable, { value: varsFromURL[name] });
}
});
- return variables;
+ return parsedYmlVariables;
+};
+
+/**
+ * Converts series data to options that can be added to a
+ * variable. Series data is returned from the Prometheus API
+ * `/api/v1/series`.
+ *
+ * Finds a `label` in the series data, so it can be used as
+ * a filter.
+ *
+ * For example, for the arguments:
+ *
+ * {
+ * "label": "job"
+ * "data" : [
+ * {
+ * "__name__" : "up",
+ * "job" : "prometheus",
+ * "instance" : "localhost:9090"
+ * },
+ * {
+ * "__name__" : "up",
+ * "job" : "node",
+ * "instance" : "localhost:9091"
+ * },
+ * {
+ * "__name__" : "process_start_time_seconds",
+ * "job" : "prometheus",
+ * "instance" : "localhost:9090"
+ * }
+ * ]
+ * }
+ *
+ * It returns all the different "job" values:
+ *
+ * [
+ * {
+ * "label": "node",
+ * "value": "node"
+ * },
+ * {
+ * "label": "prometheus",
+ * "value": "prometheus"
+ * }
+ * ]
+ *
+ * @param {options} options object
+ * @param {options.seriesLabel} name of the searched series label
+ * @param {options.data} series data from the series API
+ * @return {array} Options objects with the shape `{ label, value }`
+ *
+ * @see https://prometheus.io/docs/prometheus/latest/querying/api/#finding-series-by-label-matchers
+ */
+export const optionsFromSeriesData = ({ label, data = [] }) => {
+ const optionsSet = data.reduce((set, seriesObject) => {
+ // Use `new Set` to deduplicate options
+ if (seriesObject[label]) {
+ set.add(seriesObject[label]);
+ }
+ return set;
+ }, new Set());
+
+ return [...optionsSet].map(parseSimpleCustomValues);
};
export default {};