summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/monitoring/stores/utils.js
blob: 7f75a501635c6db62a80a9cc8e470a68f73bb7f7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { DATETIME_RANGE_TYPES } from '~/lib/utils/constants';
import { timeRangeToParams, getRangeType } from '~/lib/utils/datetime_range';
import { slugify } from '~/lib/utils/text_utility';
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { isSafeURL, mergeUrlParams } from '~/lib/utils/url_utility';
import { NOT_IN_DB_PREFIX, linkTypes, OUT_OF_THE_BOX_DASHBOARDS_PATH_PREFIX } from '../constants';
import { mergeURLVariables, parseTemplatingVariables } from './variable_mapping';

export const gqClient = createGqClient(
  {},
  {
    fetchPolicy: fetchPolicies.NO_CACHE,
  },
);

/**
 * Metrics loaded from project-defined dashboards do not have a metric_id.
 * This method creates a unique ID combining metric_id and id, if either is present.
 * This is hopefully a temporary solution until BE processes metrics before passing to FE
 *
 * Related:
 * https://gitlab.com/gitlab-org/gitlab/-/issues/28241
 * https://gitlab.com/gitlab-org/gitlab/-/merge_requests/27447
 *
 * @param {Object} metric - metric
 * @param {Number} metric.metric_id - Database metric id
 * @param {String} metric.id - User-defined identifier
 * @returns {Object} - normalized metric with a uniqueID
 */
// eslint-disable-next-line camelcase
export const uniqMetricsId = ({ metric_id, id }) => `${metric_id || NOT_IN_DB_PREFIX}_${id}`;

/**
 * Project path has a leading slash that doesn't work well
 * with project full path resolver here
 * https://gitlab.com/gitlab-org/gitlab/blob/5cad4bd721ab91305af4505b2abc92b36a56ad6b/app/graphql/resolvers/full_path_resolver.rb#L10
 *
 * @param {String} str String with leading slash
 * @returns {String}
 */
export const removeLeadingSlash = (str) => (str || '').replace(/^\/+/, '');

/**
 * GraphQL environments API returns only id and name.
 * For the environments dropdown we need metrics_path.
 * This method parses the results and add necessary attrs
 *
 * @param {Array} response Environments API result
 * @param {String} projectPath Current project path
 * @returns {Array}
 */
export const parseEnvironmentsResponse = (response = [], projectPath) =>
  (response || []).map((env) => {
    const id = getIdFromGraphQLId(env.id);
    return {
      ...env,
      id,
      metrics_path: `${projectPath}/-/metrics?environment=${id}`,
    };
  });

/**
 * Annotation API returns time in UTC. This method
 * converts time to local time.
 *
 * startingAt always exists but endingAt does not.
 * If endingAt does not exist, a threshold line is
 * drawn.
 *
 * If endingAt exists, a threshold range is drawn.
 * But this is not supported as of %12.10
 *
 * @param {Array} response annotations response
 * @returns {Array} parsed responses
 */
export const parseAnnotationsResponse = (response) => {
  if (!response) {
    return [];
  }
  return response.map((annotation) => ({
    ...annotation,
    startingAt: new Date(annotation.startingAt),
    endingAt: annotation.endingAt ? new Date(annotation.endingAt) : null,
  }));
};

/**
 * Maps metrics to its view model
 *
 * This function difers from other in that is maps all
 * non-define properties as-is to the object. This is not
 * advisable as it could lead to unexpected side-effects.
 *
 * Related issue:
 * https://gitlab.com/gitlab-org/gitlab/issues/207198
 *
 * @param {Array} metrics - Array of prometheus metrics
 * @returns {Object}
 */
const mapToMetricsViewModel = (metrics) =>
  metrics.map(({ label, id, metric_id, query_range, prometheus_endpoint_path, ...metric }) => ({
    label,
    queryRange: query_range,
    prometheusEndpointPath: prometheus_endpoint_path,
    metricId: uniqMetricsId({ metric_id, id }),

    // metric data
    loading: false,
    result: null,
    state: null,

    ...metric,
  }));

/**
 * Maps X-axis view model
 *
 * @param {Object} axis
 */
const mapXAxisToViewModel = ({ name = '' }) => ({ name });

/**
 * Maps Y-axis view model
 *
 * Defaults to a 2 digit precision and `engineering` format. It only allows
 * formats in the SUPPORTED_FORMATS array.
 *
 * @param {Object} axis
 */
const mapYAxisToViewModel = ({
  name = '',
  format = SUPPORTED_FORMATS.engineering,
  precision = 2,
}) => {
  return {
    name,
    format: SUPPORTED_FORMATS[format] || SUPPORTED_FORMATS.engineering,
    precision,
  };
};

/**
 * Maps a link to its view model, expects an url and
 * (optionally) a title.
 *
 * Unsafe URLs are ignored.
 *
 * @param {Object} Link
 * @returns {Object} Link object with a `title`, `url` and `type`
 *
 */
const mapLinksToViewModel = ({ url = null, title = '', type } = {}) => {
  return {
    title: title || String(url),
    type,
    url: url && isSafeURL(url) ? String(url) : '#',
  };
};

/**
 * Maps a metrics panel to its view model
 *
 * @param {Object} panel - Metrics panel
 * @returns {Object}
 */
export const mapPanelToViewModel = ({
  id = null,
  title = '',
  type,
  x_axis = {}, // eslint-disable-line camelcase
  x_label,
  y_label,
  y_axis = {}, // eslint-disable-line camelcase
  field,
  metrics = [],
  links = [],
  min_value,
  max_value,
  split,
  thresholds,
  format,
}) => {
  // Both `x_axis.name` and `x_label` are supported for now
  // https://gitlab.com/gitlab-org/gitlab/issues/210521
  const xAxis = mapXAxisToViewModel({ name: x_label, ...x_axis }); // eslint-disable-line camelcase

  // Both `y_axis.name` and `y_label` are supported for now
  // https://gitlab.com/gitlab-org/gitlab/issues/208385
  const yAxis = mapYAxisToViewModel({ name: y_label, ...y_axis }); // eslint-disable-line camelcase

  return {
    id,
    title,
    type,
    xLabel: xAxis.name,
    y_label: yAxis.name, // Changing y_label to yLabel is pending https://gitlab.com/gitlab-org/gitlab/issues/207198
    yAxis,
    xAxis,
    field,
    minValue: min_value,
    maxValue: max_value,
    split,
    thresholds,
    format,
    links: links.map(mapLinksToViewModel),
    metrics: mapToMetricsViewModel(metrics),
  };
};

/**
 * Maps a metrics panel group to its view model
 *
 * @param {Object} panelGroup - Panel Group
 * @returns {Object}
 */
const mapToPanelGroupViewModel = ({ group = '', panels = [] }, i) => {
  return {
    key: `${slugify(group || 'default')}-${i}`,
    group,
    panels: panels.map(mapPanelToViewModel),
  };
};

/**
 * Convert dashboard time range to Grafana
 * dashboards time range.
 *
 * @param {Object} timeRange
 * @returns {Object}
 */
export const convertToGrafanaTimeRange = (timeRange) => {
  const timeRangeType = getRangeType(timeRange);
  if (timeRangeType === DATETIME_RANGE_TYPES.fixed) {
    return {
      from: new Date(timeRange.start).getTime(),
      to: new Date(timeRange.end).getTime(),
    };
  } else if (timeRangeType === DATETIME_RANGE_TYPES.rolling) {
    const { seconds } = timeRange.duration;
    return {
      from: `now-${seconds}s`,
      to: 'now',
    };
  }
  // fallback to returning the time range as is
  return timeRange;
};

/**
 * Convert dashboard time ranges to other supported
 * link formats.
 *
 * @param {Object} timeRange metrics dashboard time range
 * @param {String} type type of link
 * @returns {String}
 */
export const convertTimeRanges = (timeRange, type) => {
  if (type === linkTypes.GRAFANA) {
    return convertToGrafanaTimeRange(timeRange);
  }
  return timeRangeToParams(timeRange);
};

/**
 * Adds dashboard-related metadata to the user-defined links.
 *
 * As of %13.1, metadata only includes timeRange but in the
 * future more info will be added to the links.
 *
 * @param {Object} metadata
 * @returns {Function}
 */
export const addDashboardMetaDataToLink = (metadata) => (link) => {
  let modifiedLink = { ...link };
  if (metadata.timeRange) {
    modifiedLink = {
      ...modifiedLink,
      url: mergeUrlParams(convertTimeRanges(metadata.timeRange, link.type), link.url),
    };
  }
  return modifiedLink;
};

/**
 * Maps a dashboard json object to its view model
 *
 * @param {Object} dashboard - Dashboard object
 * @param {String} dashboard.dashboard - Dashboard name object
 * @param {Array} dashboard.panel_groups - Panel groups array
 * @returns {Object}
 */
export const mapToDashboardViewModel = ({
  dashboard = '',
  templating = {},
  links = [],
  panel_groups = [], // eslint-disable-line camelcase
}) => {
  return {
    dashboard,
    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] };
  });

/**
 * 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 {array} result
 * @returns {object} Normalized result.
 */
const normalizeResultMatrix = (result) =>
  result.map(({ metric, values }) => {
    const mappedValues = values.map(mapScalarValue);
    return {
      metric,
      value: mappedValues[mappedValues.length - 1],
      values: mappedValues,
    };
  });

/**
 * 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);
};

/**
 * Custom variables defined in the dashboard yml file are
 * eventually passed over the wire to the backend Prometheus
 * API proxy.
 *
 * This method adds a prefix to the URL param keys so that
 * the backend can differential these variables from the other
 * variables.
 *
 * This is currently only used by getters/getCustomVariablesParams
 *
 * @param {String} name Variable key that needs to be prefixed
 * @returns {String}
 */
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 overview dashboard.
 * If dashboard is set, it usually is a custom dashboard unless
 * explicitly it is set to overview 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;
};