mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
* Delete errors projection * Remove `getMetricsProjection` * Remove `getServiceNodesProjection` * Fix tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Søren Louv-Jansen <soren.louv@elastic.co>
This commit is contained in:
parent
24f8717345
commit
0365722184
22 changed files with 292 additions and 553 deletions
|
@ -6,11 +6,15 @@
|
|||
*/
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { SERVICE_ENVIRONMENT } from '../elasticsearch_fieldnames';
|
||||
import {
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NODE_NAME,
|
||||
} from '../elasticsearch_fieldnames';
|
||||
import {
|
||||
ENVIRONMENT_ALL,
|
||||
ENVIRONMENT_NOT_DEFINED,
|
||||
} from '../environment_filter_values';
|
||||
import { SERVICE_NODE_NAME_MISSING } from '../service_nodes';
|
||||
|
||||
export function environmentQuery(
|
||||
environment: string
|
||||
|
@ -25,3 +29,17 @@ export function environmentQuery(
|
|||
|
||||
return [{ term: { [SERVICE_ENVIRONMENT]: environment } }];
|
||||
}
|
||||
|
||||
export function serviceNodeNameQuery(
|
||||
serviceNodeName?: string
|
||||
): QueryDslQueryContainer[] {
|
||||
if (!serviceNodeName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (serviceNodeName === SERVICE_NODE_NAME_MISSING) {
|
||||
return [{ bool: { must_not: [{ exists: { field: SERVICE_NODE_NAME } }] } }];
|
||||
}
|
||||
|
||||
return [{ term: { [SERVICE_NODE_NAME]: serviceNodeName } }];
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EuiTitle } from '@elastic/eui';
|
||||
import React from 'react';
|
||||
import { APIReturnType } from '../../../../services/rest/createCallApmApi';
|
||||
import {
|
||||
asDecimal,
|
||||
asInteger,
|
||||
|
@ -14,8 +15,6 @@ import {
|
|||
getDurationFormatter,
|
||||
getFixedByteFormatter,
|
||||
} from '../../../../../common/utils/formatters';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform_metrics_chart';
|
||||
import { Maybe } from '../../../../../typings/common';
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { TimeseriesChart } from '../timeseries_chart';
|
||||
|
@ -24,7 +23,11 @@ import {
|
|||
getResponseTimeTickFormatter,
|
||||
} from '../transaction_charts/helper';
|
||||
|
||||
function getYTickFormatter(chart: GenericMetricsChart) {
|
||||
type MetricChartApiResponse =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>;
|
||||
type MetricChart = MetricChartApiResponse['charts'][0];
|
||||
|
||||
function getYTickFormatter(chart: MetricChart) {
|
||||
const max = getMaxY(chart.series);
|
||||
|
||||
switch (chart.yUnit) {
|
||||
|
@ -50,7 +53,7 @@ function getYTickFormatter(chart: GenericMetricsChart) {
|
|||
interface Props {
|
||||
start: Maybe<number | string>;
|
||||
end: Maybe<number | string>;
|
||||
chart: GenericMetricsChart;
|
||||
chart: MetricChart;
|
||||
fetchStatus: FETCH_STATUS;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,14 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { MetricsChartsByAgentAPIResponse } from '../../server/lib/metrics/get_metrics_chart_data_by_agent';
|
||||
import type { APIReturnType } from '../services/rest/createCallApmApi';
|
||||
import { useApmServiceContext } from '../context/apm_service/use_apm_service_context';
|
||||
import { useFetcher } from './use_fetcher';
|
||||
import { useTimeRange } from './use_time_range';
|
||||
import { useApmParams } from './use_apm_params';
|
||||
|
||||
const INITIAL_DATA: MetricsChartsByAgentAPIResponse = {
|
||||
type MetricChartApiResponse =
|
||||
APIReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>;
|
||||
|
||||
const INITIAL_DATA: MetricChartApiResponse = {
|
||||
charts: [],
|
||||
};
|
||||
|
||||
|
|
|
@ -5,6 +5,10 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AggregationsTermsAggregationOrder } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { kqlQuery, rangeQuery } from '../../../../observability/server';
|
||||
import {
|
||||
ERROR_CULPRIT,
|
||||
ERROR_EXC_HANDLED,
|
||||
|
@ -12,9 +16,8 @@ import {
|
|||
ERROR_EXC_TYPE,
|
||||
ERROR_GROUP_ID,
|
||||
ERROR_LOG_MESSAGE,
|
||||
SERVICE_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { getErrorGroupsProjection } from '../../projections/errors';
|
||||
import { mergeProjection } from '../../projections/util/merge_projection';
|
||||
import { getErrorName } from '../helpers/get_error_name';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
|
||||
|
@ -42,27 +45,31 @@ export async function getErrorGroups({
|
|||
// sort buckets by last occurrence of error
|
||||
const sortByLatestOccurrence = sortField === 'latestOccurrenceAt';
|
||||
|
||||
const projection = getErrorGroupsProjection({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const order = sortByLatestOccurrence
|
||||
? {
|
||||
max_timestamp: sortDirection,
|
||||
}
|
||||
const maxTimestampAggKey = 'max_timestamp';
|
||||
const order: AggregationsTermsAggregationOrder = sortByLatestOccurrence
|
||||
? { [maxTimestampAggKey]: sortDirection }
|
||||
: { _count: sortDirection };
|
||||
|
||||
const params = mergeProjection(projection, {
|
||||
const params = {
|
||||
apm: {
|
||||
events: [ProcessorEvent.error as const],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
error_groups: {
|
||||
terms: {
|
||||
...projection.body.aggs.error_groups.terms,
|
||||
field: ERROR_GROUP_ID,
|
||||
size: 500,
|
||||
order,
|
||||
},
|
||||
|
@ -83,19 +90,13 @@ export async function getErrorGroups({
|
|||
},
|
||||
},
|
||||
...(sortByLatestOccurrence
|
||||
? {
|
||||
max_timestamp: {
|
||||
max: {
|
||||
field: '@timestamp',
|
||||
},
|
||||
},
|
||||
}
|
||||
? { [maxTimestampAggKey]: { max: { field: '@timestamp' } } }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const resp = await apmEventClient.search('get_error_groups', params);
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Setup } from '../../helpers/setup_request';
|
|||
import { getCPUChartData } from './shared/cpu';
|
||||
import { getMemoryChartData } from './shared/memory';
|
||||
|
||||
export async function getDefaultMetricsCharts({
|
||||
export function getDefaultMetricsCharts({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
|
@ -24,10 +24,8 @@ export async function getDefaultMetricsCharts({
|
|||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const charts = await Promise.all([
|
||||
return Promise.all([
|
||||
getCPUChartData({ environment, kuery, setup, serviceName, start, end }),
|
||||
getMemoryChartData({ environment, kuery, setup, serviceName, start, end }),
|
||||
]);
|
||||
|
||||
return { charts };
|
||||
}
|
||||
|
|
|
@ -11,17 +11,26 @@ import { isFiniteNumber } from '../../../../../../common/utils/is_finite_number'
|
|||
import { Setup } from '../../../../helpers/setup_request';
|
||||
import { getMetricsDateHistogramParams } from '../../../../helpers/metrics';
|
||||
import { ChartBase } from '../../../types';
|
||||
import { getMetricsProjection } from '../../../../../projections/metrics';
|
||||
import { mergeProjection } from '../../../../../projections/util/merge_projection';
|
||||
|
||||
import {
|
||||
AGENT_NAME,
|
||||
LABEL_NAME,
|
||||
METRIC_JAVA_GC_COUNT,
|
||||
METRIC_JAVA_GC_TIME,
|
||||
SERVICE_NAME,
|
||||
} from '../../../../../../common/elasticsearch_fieldnames';
|
||||
import { getBucketSize } from '../../../../helpers/get_bucket_size';
|
||||
import { getVizColorForIndex } from '../../../../../../common/viz_colors';
|
||||
import { JAVA_AGENT_NAMES } from '../../../../../../common/agent_name';
|
||||
import {
|
||||
environmentQuery,
|
||||
serviceNodeNameQuery,
|
||||
} from '../../../../../../common/utils/environment_query';
|
||||
import {
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
} from '../../../../../../../observability/server';
|
||||
import { ProcessorEvent } from '../../../../../../common/processor_event';
|
||||
|
||||
export async function fetchAndTransformGcMetrics({
|
||||
environment,
|
||||
|
@ -50,26 +59,24 @@ export async function fetchAndTransformGcMetrics({
|
|||
|
||||
const { bucketSize } = getBucketSize({ start, end });
|
||||
|
||||
const projection = getMetricsProjection({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
serviceNodeName,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
// GC rate and time are reported by the agents as monotonically
|
||||
// increasing counters, which means that we have to calculate
|
||||
// the delta in an es query. In the future agent might start
|
||||
// reporting deltas.
|
||||
const params = mergeProjection(projection, {
|
||||
const params = {
|
||||
apm: {
|
||||
events: [ProcessorEvent.metric],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...projection.body.query.bool.filter,
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...serviceNodeNameQuery(serviceNodeName),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
{ exists: { field: fieldName } },
|
||||
{ terms: { [AGENT_NAME]: JAVA_AGENT_NAMES } },
|
||||
],
|
||||
|
@ -114,7 +121,7 @@ export async function fetchAndTransformGcMetrics({
|
|||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(operationName, params);
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ export function getJavaMetricsCharts({
|
|||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
return withApmSpan('get_java_system_metric_charts', async () => {
|
||||
return withApmSpan('get_java_system_metric_charts', () => {
|
||||
const options = {
|
||||
environment,
|
||||
kuery,
|
||||
|
@ -43,7 +43,7 @@ export function getJavaMetricsCharts({
|
|||
end,
|
||||
};
|
||||
|
||||
const charts = await Promise.all([
|
||||
return Promise.all([
|
||||
getCPUChartData(options),
|
||||
getMemoryChartData(options),
|
||||
getHeapMemoryChart(options),
|
||||
|
@ -52,7 +52,5 @@ export function getJavaMetricsCharts({
|
|||
getGcRateChart(options),
|
||||
getGcTimeChart(options),
|
||||
]);
|
||||
|
||||
return { charts };
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,15 +5,23 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Overwrite, Unionize } from 'utility-types';
|
||||
import { Unionize } from 'utility-types';
|
||||
import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme';
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { getVizColorForIndex } from '../../../common/viz_colors';
|
||||
import { AggregationOptionsByType } from '../../../../../../src/core/types/elasticsearch';
|
||||
import { getMetricsProjection } from '../../projections/metrics';
|
||||
import { mergeProjection } from '../../projections/util/merge_projection';
|
||||
import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client';
|
||||
import { getMetricsDateHistogramParams } from '../helpers/metrics';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
import { transformDataToMetricsChart } from './transform_metrics_chart';
|
||||
import { ChartBase } from './types';
|
||||
import {
|
||||
environmentQuery,
|
||||
serviceNodeNameQuery,
|
||||
} from '../../../common/utils/environment_query';
|
||||
import { kqlQuery, rangeQuery } from '../../../../observability/server';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames';
|
||||
import { APMEventESSearchRequest } from '../helpers/create_es_client/create_apm_event_client';
|
||||
import { PromiseReturnType } from '../../../../observability/typings/common';
|
||||
|
||||
type MetricsAggregationMap = Unionize<{
|
||||
min: AggregationOptionsByType['min'];
|
||||
|
@ -24,31 +32,20 @@ type MetricsAggregationMap = Unionize<{
|
|||
|
||||
type MetricAggs = Record<string, MetricsAggregationMap>;
|
||||
|
||||
export type GenericMetricsRequest = Overwrite<
|
||||
APMEventESSearchRequest,
|
||||
{
|
||||
body: {
|
||||
aggs: {
|
||||
timeseriesData: {
|
||||
date_histogram: AggregationOptionsByType['date_histogram'];
|
||||
aggs: MetricAggs;
|
||||
};
|
||||
} & MetricAggs;
|
||||
};
|
||||
}
|
||||
>;
|
||||
export type GenericMetricsRequest = APMEventESSearchRequest & {
|
||||
body: {
|
||||
aggs: {
|
||||
timeseriesData: {
|
||||
date_histogram: AggregationOptionsByType['date_histogram'];
|
||||
aggs: MetricAggs;
|
||||
};
|
||||
} & MetricAggs;
|
||||
};
|
||||
};
|
||||
|
||||
interface Filter {
|
||||
exists?: {
|
||||
field: string;
|
||||
};
|
||||
term?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
terms?: {
|
||||
[key: string]: string[];
|
||||
};
|
||||
}
|
||||
export type GenericMetricsChart = PromiseReturnType<
|
||||
typeof fetchAndTransformMetrics
|
||||
>;
|
||||
|
||||
export async function fetchAndTransformMetrics<T extends MetricAggs>({
|
||||
environment,
|
||||
|
@ -72,26 +69,27 @@ export async function fetchAndTransformMetrics<T extends MetricAggs>({
|
|||
end: number;
|
||||
chartBase: ChartBase;
|
||||
aggs: T;
|
||||
additionalFilters?: Filter[];
|
||||
additionalFilters?: QueryDslQueryContainer[];
|
||||
operationName: string;
|
||||
}) {
|
||||
const { apmEventClient, config } = setup;
|
||||
|
||||
const projection = getMetricsProjection({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
serviceNodeName,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const params: GenericMetricsRequest = mergeProjection(projection, {
|
||||
const params: GenericMetricsRequest = {
|
||||
apm: {
|
||||
events: [ProcessorEvent.metric],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [...projection.body.query.bool.filter, ...additionalFilters],
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...serviceNodeNameQuery(serviceNodeName),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
...additionalFilters,
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
|
@ -106,9 +104,43 @@ export async function fetchAndTransformMetrics<T extends MetricAggs>({
|
|||
...aggs,
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(operationName, params);
|
||||
const { hits, aggregations } = await apmEventClient.search(
|
||||
operationName,
|
||||
params
|
||||
);
|
||||
const timeseriesData = aggregations?.timeseriesData;
|
||||
|
||||
return transformDataToMetricsChart(response, chartBase);
|
||||
return {
|
||||
title: chartBase.title,
|
||||
key: chartBase.key,
|
||||
yUnit: chartBase.yUnit,
|
||||
series:
|
||||
hits.total.value === 0
|
||||
? []
|
||||
: Object.keys(chartBase.series).map((seriesKey, i) => {
|
||||
// @ts-ignore
|
||||
const overallValue = aggregations?.[seriesKey]?.value as number;
|
||||
|
||||
return {
|
||||
title: chartBase.series[seriesKey].title,
|
||||
key: seriesKey,
|
||||
type: chartBase.type,
|
||||
color:
|
||||
chartBase.series[seriesKey].color ||
|
||||
getVizColorForIndex(i, theme),
|
||||
overallValue,
|
||||
data:
|
||||
timeseriesData?.buckets.map((bucket) => {
|
||||
const { value } = bucket[seriesKey];
|
||||
const y = value === null || isNaN(value) ? null : value;
|
||||
return {
|
||||
x: bucket.key,
|
||||
y,
|
||||
};
|
||||
}) || [],
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,12 +8,8 @@
|
|||
import { Setup } from '../helpers/setup_request';
|
||||
import { getJavaMetricsCharts } from './by_agent/java';
|
||||
import { getDefaultMetricsCharts } from './by_agent/default';
|
||||
import { GenericMetricsChart } from './transform_metrics_chart';
|
||||
import { isJavaAgentName } from '../../../common/agent_name';
|
||||
|
||||
export interface MetricsChartsByAgentAPIResponse {
|
||||
charts: GenericMetricsChart[];
|
||||
}
|
||||
import { GenericMetricsChart } from './fetch_and_transform_metrics';
|
||||
|
||||
export async function getMetricsChartDataByAgent({
|
||||
environment,
|
||||
|
@ -33,7 +29,7 @@ export async function getMetricsChartDataByAgent({
|
|||
agentName: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<MetricsChartsByAgentAPIResponse> {
|
||||
}): Promise<GenericMetricsChart[]> {
|
||||
if (isJavaAgentName(agentName)) {
|
||||
return getJavaMetricsCharts({
|
||||
environment,
|
||||
|
|
|
@ -1,132 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { transformDataToMetricsChart } from './transform_metrics_chart';
|
||||
import { ChartType, YUnit } from '../../../typings/timeseries';
|
||||
|
||||
test('transformDataToMetricsChart should transform an ES result into a chart object', () => {
|
||||
const response = {
|
||||
hits: { total: { value: 5000 } },
|
||||
aggregations: {
|
||||
a: { value: 1000 },
|
||||
b: { value: 1000 },
|
||||
c: { value: 1000 },
|
||||
timeseriesData: {
|
||||
buckets: [
|
||||
{
|
||||
a: { value: 10 },
|
||||
b: { value: 10 },
|
||||
c: { value: 10 },
|
||||
key: 1,
|
||||
doc_count: 0,
|
||||
},
|
||||
{
|
||||
a: { value: 20 },
|
||||
b: { value: 20 },
|
||||
c: { value: 20 },
|
||||
key: 2,
|
||||
doc_count: 0,
|
||||
},
|
||||
{
|
||||
a: { value: 30 },
|
||||
b: { value: 30 },
|
||||
c: { value: 30 },
|
||||
key: 3,
|
||||
doc_count: 0,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
} as any;
|
||||
|
||||
const chartBase = {
|
||||
title: 'Test Chart Title',
|
||||
type: 'linemark' as ChartType,
|
||||
key: 'test_chart_key',
|
||||
yUnit: 'number' as YUnit,
|
||||
series: {
|
||||
a: { title: 'Series A', color: 'red' },
|
||||
b: { title: 'Series B', color: 'blue' },
|
||||
c: { title: 'Series C', color: 'green' },
|
||||
},
|
||||
};
|
||||
|
||||
const chart = transformDataToMetricsChart(response, chartBase);
|
||||
|
||||
expect(chart).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"key": "test_chart_key",
|
||||
"series": Array [
|
||||
Object {
|
||||
"color": "red",
|
||||
"data": Array [
|
||||
Object {
|
||||
"x": 1,
|
||||
"y": 10,
|
||||
},
|
||||
Object {
|
||||
"x": 2,
|
||||
"y": 20,
|
||||
},
|
||||
Object {
|
||||
"x": 3,
|
||||
"y": 30,
|
||||
},
|
||||
],
|
||||
"key": "a",
|
||||
"overallValue": 1000,
|
||||
"title": "Series A",
|
||||
"type": "linemark",
|
||||
},
|
||||
Object {
|
||||
"color": "blue",
|
||||
"data": Array [
|
||||
Object {
|
||||
"x": 1,
|
||||
"y": 10,
|
||||
},
|
||||
Object {
|
||||
"x": 2,
|
||||
"y": 20,
|
||||
},
|
||||
Object {
|
||||
"x": 3,
|
||||
"y": 30,
|
||||
},
|
||||
],
|
||||
"key": "b",
|
||||
"overallValue": 1000,
|
||||
"title": "Series B",
|
||||
"type": "linemark",
|
||||
},
|
||||
Object {
|
||||
"color": "green",
|
||||
"data": Array [
|
||||
Object {
|
||||
"x": 1,
|
||||
"y": 10,
|
||||
},
|
||||
Object {
|
||||
"x": 2,
|
||||
"y": 20,
|
||||
},
|
||||
Object {
|
||||
"x": 3,
|
||||
"y": 30,
|
||||
},
|
||||
],
|
||||
"key": "c",
|
||||
"overallValue": 1000,
|
||||
"title": "Series C",
|
||||
"type": "linemark",
|
||||
},
|
||||
],
|
||||
"title": "Test Chart Title",
|
||||
"yUnit": "number",
|
||||
}
|
||||
`);
|
||||
});
|
|
@ -1,55 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { euiLightVars as theme } from '@kbn/ui-shared-deps-src/theme';
|
||||
import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch';
|
||||
import { getVizColorForIndex } from '../../../common/viz_colors';
|
||||
import { GenericMetricsRequest } from './fetch_and_transform_metrics';
|
||||
import { ChartBase } from './types';
|
||||
|
||||
export type GenericMetricsChart = ReturnType<
|
||||
typeof transformDataToMetricsChart
|
||||
>;
|
||||
|
||||
export function transformDataToMetricsChart(
|
||||
result: ESSearchResponse<unknown, GenericMetricsRequest>,
|
||||
chartBase: ChartBase
|
||||
) {
|
||||
const { aggregations } = result;
|
||||
const timeseriesData = aggregations?.timeseriesData;
|
||||
|
||||
return {
|
||||
title: chartBase.title,
|
||||
key: chartBase.key,
|
||||
yUnit: chartBase.yUnit,
|
||||
series:
|
||||
result.hits.total.value > 0
|
||||
? Object.keys(chartBase.series).map((seriesKey, i) => {
|
||||
const overallValue = aggregations?.[seriesKey]?.value;
|
||||
|
||||
return {
|
||||
title: chartBase.series[seriesKey].title,
|
||||
key: seriesKey,
|
||||
type: chartBase.type,
|
||||
color:
|
||||
chartBase.series[seriesKey].color ||
|
||||
getVizColorForIndex(i, theme),
|
||||
overallValue,
|
||||
data:
|
||||
timeseriesData?.buckets.map((bucket) => {
|
||||
const { value } = bucket[seriesKey];
|
||||
const y = value === null || isNaN(value) ? null : value;
|
||||
return {
|
||||
x: bucket.key,
|
||||
y,
|
||||
};
|
||||
}) || [],
|
||||
};
|
||||
})
|
||||
: [],
|
||||
};
|
||||
}
|
|
@ -156,7 +156,6 @@ async function getServicesData(options: IEnvOptions) {
|
|||
|
||||
export type ConnectionsResponse = PromiseReturnType<typeof getConnectionData>;
|
||||
export type ServicesResponse = PromiseReturnType<typeof getServicesData>;
|
||||
export type ServiceMapAPIResponse = PromiseReturnType<typeof getServiceMap>;
|
||||
|
||||
export function getServiceMap(options: IEnvOptions) {
|
||||
return withApmSpan('get_service_map', async () => {
|
||||
|
|
|
@ -35,11 +35,6 @@ Object {
|
|||
"service.name": "foo",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.node.name": "bar",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
|
@ -49,6 +44,11 @@ Object {
|
|||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"service.node.name": "bar",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -92,6 +92,15 @@ Object {
|
|||
"service.name": "foo",
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"format": "epoch_millis",
|
||||
"gte": 0,
|
||||
"lte": 50000,
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Array [
|
||||
|
@ -103,15 +112,6 @@ Object {
|
|||
],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"format": "epoch_millis",
|
||||
"gte": 0,
|
||||
"lte": 50000,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
@ -191,6 +191,7 @@ Object {
|
|||
],
|
||||
},
|
||||
},
|
||||
"size": 0,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -14,8 +14,13 @@ import {
|
|||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes';
|
||||
import { asMutableArray } from '../../../common/utils/as_mutable_array';
|
||||
import { getServiceNodesProjection } from '../../projections/service_nodes';
|
||||
import { mergeProjection } from '../../projections/util/merge_projection';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { kqlQuery, rangeQuery } from '../../../../observability/server';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { Setup } from '../helpers/setup_request';
|
||||
|
||||
const getServiceNodes = async ({
|
||||
|
@ -35,20 +40,26 @@ const getServiceNodes = async ({
|
|||
}) => {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const projection = getServiceNodesProjection({
|
||||
kuery,
|
||||
serviceName,
|
||||
environment,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const params = mergeProjection(projection, {
|
||||
const params = {
|
||||
apm: {
|
||||
events: [ProcessorEvent.metric],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
nodes: {
|
||||
terms: {
|
||||
...projection.body.aggs.nodes.terms,
|
||||
field: SERVICE_NODE_NAME,
|
||||
size: 10000,
|
||||
missing: SERVICE_NODE_NAME_MISSING,
|
||||
},
|
||||
|
@ -57,7 +68,7 @@ const getServiceNodes = async ({
|
|||
top_metrics: {
|
||||
metrics: asMutableArray([{ field: HOST_NAME }] as const),
|
||||
sort: {
|
||||
'@timestamp': 'desc',
|
||||
'@timestamp': 'desc' as const,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -85,7 +96,7 @@ const getServiceNodes = async ({
|
|||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search('get_service_nodes', params);
|
||||
|
||||
|
|
|
@ -11,8 +11,16 @@ import {
|
|||
CONTAINER_ID,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { NOT_AVAILABLE_LABEL } from '../../../common/i18n';
|
||||
import { mergeProjection } from '../../projections/util/merge_projection';
|
||||
import { getServiceNodesProjection } from '../../projections/service_nodes';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { kqlQuery, rangeQuery } from '../../../../observability/server';
|
||||
import {
|
||||
environmentQuery,
|
||||
serviceNodeNameQuery,
|
||||
} from '../../../common/utils/environment_query';
|
||||
import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values';
|
||||
|
||||
export async function getServiceNodeMetadata({
|
||||
|
@ -32,39 +40,48 @@ export async function getServiceNodeMetadata({
|
|||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const query = mergeProjection(
|
||||
getServiceNodesProjection({
|
||||
kuery,
|
||||
serviceName,
|
||||
serviceNodeName,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
start,
|
||||
end,
|
||||
}),
|
||||
{
|
||||
body: {
|
||||
size: 0,
|
||||
aggs: {
|
||||
host: {
|
||||
terms: {
|
||||
field: HOST_NAME,
|
||||
size: 1,
|
||||
},
|
||||
const params = {
|
||||
apm: {
|
||||
events: [ProcessorEvent.metric],
|
||||
},
|
||||
body: {
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(ENVIRONMENT_ALL.value),
|
||||
...kqlQuery(kuery),
|
||||
...serviceNodeNameQuery(serviceNodeName),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
nodes: {
|
||||
terms: {
|
||||
field: SERVICE_NODE_NAME,
|
||||
},
|
||||
containerId: {
|
||||
terms: {
|
||||
field: CONTAINER_ID,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
host: {
|
||||
terms: {
|
||||
field: HOST_NAME,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
containerId: {
|
||||
terms: {
|
||||
field: CONTAINER_ID,
|
||||
size: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const response = await apmEventClient.search(
|
||||
'get_service_node_metadata',
|
||||
query
|
||||
params
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
@ -8,14 +8,9 @@
|
|||
import { withApmSpan } from '../../../../utils/with_apm_span';
|
||||
import { getAllEnvironments } from '../../../environments/get_all_environments';
|
||||
import { Setup } from '../../../helpers/setup_request';
|
||||
import { PromiseReturnType } from '../../../../../../observability/typings/common';
|
||||
import { getExistingEnvironmentsForService } from './get_existing_environments_for_service';
|
||||
import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration/all_option';
|
||||
|
||||
export type AgentConfigurationEnvironmentsAPIResponse = PromiseReturnType<
|
||||
typeof getEnvironments
|
||||
>;
|
||||
|
||||
export async function getEnvironments({
|
||||
serviceName,
|
||||
setup,
|
||||
|
|
|
@ -7,15 +7,10 @@
|
|||
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { Setup } from '../../helpers/setup_request';
|
||||
import { PromiseReturnType } from '../../../../../observability/typings/common';
|
||||
import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames';
|
||||
import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration/all_option';
|
||||
import { getProcessorEventForTransactions } from '../../helpers/transactions';
|
||||
|
||||
export type AgentConfigurationServicesAPIResponse = PromiseReturnType<
|
||||
typeof getServiceNames
|
||||
>;
|
||||
|
||||
export async function getServiceNames({
|
||||
setup,
|
||||
searchAggregatedTransactions,
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
ERROR_GROUP_ID,
|
||||
} from '../../common/elasticsearch_fieldnames';
|
||||
import { rangeQuery, kqlQuery } from '../../../observability/server';
|
||||
import { environmentQuery } from '../../common/utils/environment_query';
|
||||
import { ProcessorEvent } from '../../common/processor_event';
|
||||
|
||||
export function getErrorGroupsProjection({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
environment: string;
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
return {
|
||||
apm: {
|
||||
events: [ProcessorEvent.error as const],
|
||||
},
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
error_groups: {
|
||||
terms: {
|
||||
field: ERROR_GROUP_ID,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SERVICE_NODE_NAME,
|
||||
} from '../../common/elasticsearch_fieldnames';
|
||||
import { rangeQuery, kqlQuery } from '../../../observability/server';
|
||||
import { environmentQuery } from '../../common/utils/environment_query';
|
||||
import { SERVICE_NODE_NAME_MISSING } from '../../common/service_nodes';
|
||||
import { ProcessorEvent } from '../../common/processor_event';
|
||||
|
||||
function getServiceNodeNameFilters(serviceNodeName?: string) {
|
||||
if (!serviceNodeName) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (serviceNodeName === SERVICE_NODE_NAME_MISSING) {
|
||||
return [{ bool: { must_not: [{ exists: { field: SERVICE_NODE_NAME } }] } }];
|
||||
}
|
||||
|
||||
return [{ term: { [SERVICE_NODE_NAME]: serviceNodeName } }];
|
||||
}
|
||||
|
||||
export function getMetricsProjection({
|
||||
environment,
|
||||
kuery,
|
||||
serviceName,
|
||||
serviceNodeName,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
environment: string;
|
||||
kuery: string;
|
||||
serviceName: string;
|
||||
serviceNodeName?: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const filter = [
|
||||
{ term: { [SERVICE_NAME]: serviceName } },
|
||||
...getServiceNodeNameFilters(serviceNodeName),
|
||||
...rangeQuery(start, end),
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
] as QueryDslQueryContainer[];
|
||||
|
||||
return {
|
||||
apm: {
|
||||
events: [ProcessorEvent.metric],
|
||||
},
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SERVICE_NODE_NAME } from '../../common/elasticsearch_fieldnames';
|
||||
import { mergeProjection } from './util/merge_projection';
|
||||
import { getMetricsProjection } from './metrics';
|
||||
|
||||
export function getServiceNodesProjection({
|
||||
serviceName,
|
||||
serviceNodeName,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
serviceName: string;
|
||||
serviceNodeName?: string;
|
||||
environment: string;
|
||||
kuery: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
return mergeProjection(
|
||||
getMetricsProjection({
|
||||
serviceName,
|
||||
serviceNodeName,
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
}),
|
||||
{
|
||||
body: {
|
||||
aggs: {
|
||||
nodes: {
|
||||
terms: {
|
||||
field: SERVICE_NODE_NAME,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
|
@ -37,7 +37,8 @@ const metricsChartsRoute = createApmServerRoute({
|
|||
const { serviceName } = params.path;
|
||||
const { agentName, environment, kuery, serviceNodeName, start, end } =
|
||||
params.query;
|
||||
return await getMetricsChartDataByAgent({
|
||||
|
||||
const charts = await getMetricsChartDataByAgent({
|
||||
environment,
|
||||
kuery,
|
||||
setup,
|
||||
|
@ -47,6 +48,8 @@ const metricsChartsRoute = createApmServerRoute({
|
|||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return { charts };
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -7,35 +7,39 @@
|
|||
|
||||
import expect from '@kbn/expect';
|
||||
import { first } from 'lodash';
|
||||
import { MetricsChartsByAgentAPIResponse } from '../../../../plugins/apm/server/lib/metrics/get_metrics_chart_data_by_agent';
|
||||
import { GenericMetricsChart } from '../../../../plugins/apm/server/lib/metrics/transform_metrics_chart';
|
||||
import { GenericMetricsChart } from '../../../../plugins/apm/server/lib/metrics/fetch_and_transform_metrics';
|
||||
import { SupertestReturnType } from '../../common/apm_api_supertest';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
|
||||
interface ChartResponse {
|
||||
body: MetricsChartsByAgentAPIResponse;
|
||||
status: number;
|
||||
}
|
||||
type ChartResponse = SupertestReturnType<'GET /internal/apm/services/{serviceName}/metrics/charts'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const supertest = getService('legacySupertestAsApmReadUser');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
|
||||
registry.when(
|
||||
'Metrics charts when data is loaded',
|
||||
{ config: 'basic', archives: ['metrics_8.0.0'] },
|
||||
() => {
|
||||
describe('for opbeans-node', () => {
|
||||
const start = encodeURIComponent('2020-09-08T14:50:00.000Z');
|
||||
const end = encodeURIComponent('2020-09-08T14:55:00.000Z');
|
||||
const agentName = 'nodejs';
|
||||
|
||||
describe('returns metrics data', () => {
|
||||
let chartsResponse: ChartResponse;
|
||||
before(async () => {
|
||||
chartsResponse = await supertest.get(
|
||||
`/internal/apm/services/opbeans-node/metrics/charts?start=${start}&end=${end}&agentName=${agentName}&kuery=&environment=ENVIRONMENT_ALL`
|
||||
);
|
||||
chartsResponse = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts',
|
||||
params: {
|
||||
path: { serviceName: 'opbeans-node' },
|
||||
query: {
|
||||
start: '2020-09-08T14:50:00.000Z',
|
||||
end: '2020-09-08T14:55:00.000Z',
|
||||
agentName: 'nodejs',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: ``,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('contains CPU usage and System memory usage chart data', async () => {
|
||||
expect(chartsResponse.status).to.be(200);
|
||||
expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(`
|
||||
|
@ -112,17 +116,22 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
|
||||
describe('for opbeans-java', () => {
|
||||
const agentName = 'java';
|
||||
|
||||
describe('returns metrics data', () => {
|
||||
const start = encodeURIComponent('2020-09-08T14:55:30.000Z');
|
||||
const end = encodeURIComponent('2020-09-08T15:00:00.000Z');
|
||||
|
||||
let chartsResponse: ChartResponse;
|
||||
before(async () => {
|
||||
chartsResponse = await supertest.get(
|
||||
`/internal/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&agentName=${agentName}&environment=ENVIRONMENT_ALL&kuery=`
|
||||
);
|
||||
chartsResponse = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts',
|
||||
params: {
|
||||
path: { serviceName: 'opbeans-java' },
|
||||
query: {
|
||||
start: '2020-09-08T14:55:30.000Z',
|
||||
end: '2020-09-08T15:00:00.000Z',
|
||||
agentName: 'java',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: ``,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('has correct chart data', async () => {
|
||||
|
@ -406,12 +415,19 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
// 9223372036854771712 = memory limit for a c-group when no memory limit is specified
|
||||
it('calculates system memory usage using system total field when cgroup limit is equal to 9223372036854771712', async () => {
|
||||
const start = encodeURIComponent('2020-09-08T15:00:30.000Z');
|
||||
const end = encodeURIComponent('2020-09-08T15:05:00.000Z');
|
||||
|
||||
const chartsResponse: ChartResponse = await supertest.get(
|
||||
`/internal/apm/services/opbeans-java/metrics/charts?start=${start}&end=${end}&agentName=${agentName}&environment=ENVIRONMENT_ALL&kuery=`
|
||||
);
|
||||
const chartsResponse = await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/services/{serviceName}/metrics/charts',
|
||||
params: {
|
||||
path: { serviceName: 'opbeans-java' },
|
||||
query: {
|
||||
start: '2020-09-08T15:00:30.000Z',
|
||||
end: '2020-09-08T15:05:00.000Z',
|
||||
agentName: 'java',
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: ``,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const systemMemoryUsageChart = chartsResponse.body.charts.find(
|
||||
({ key }) => key === 'memory_usage_chart'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue