Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,15 @@ describe('getAggregationTemplate', () => {
});
expect(result).toBe('PERCENTILE(??metricName, 95)');
});

it('should return PERCENTILE with to_tdigest casting for legacy histogram', () => {
const result = getAggregationTemplate({
type: ES_FIELD_TYPES.HISTOGRAM,
instrument: 'histogram',
placeholderName: 'metricName',
});
expect(result).toBe('PERCENTILE(TO_TDIGEST(??metricName), 95)');
});
});

describe('createTimeBucketAggregation', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from '@kbn/esql-language';
import { replaceParameters } from '@kbn/esql-composer';
import type { MetricField } from '../../../types';
import { isLegacyHistogram } from '../legacy_histogram';

type Params = Record<string, string | number | boolean | null>;
interface AggegationTemplateParams {
Expand Down Expand Up @@ -80,7 +81,7 @@ export function replaceFunctionParams(functionString: string, params: Params): s
* @param instrument - The metric instrument type (e.g., 'counter', 'histogram', 'gauge').
* @param placeholderName - The name of the placeholder to use in the template.
* @param customFunction - Optional custom aggregation function to use.
* @returns The ES|QL aggregation function template string.
* @returns The ES|QL aggregation function template string. Legacy histograms (type + instrument both histogram) use PERCENTILE(TO_TDIGEST(...), 95).
*/
export function getAggregationTemplate({
type,
Expand All @@ -92,6 +93,10 @@ export function getAggregationTemplate({
return `${customFunction}(??${placeholderName})`;
}

if (isLegacyHistogram({ type, instrument })) {
return `PERCENTILE(TO_TDIGEST(??${placeholderName}), 95)`;
}

if (type === 'exponential_histogram' || type === 'tdigest') {
return `PERCENTILE(??${placeholderName}, 95)`;
}
Expand All @@ -106,8 +111,8 @@ export function getAggregationTemplate({
/**
* Creates the metric aggregation part of an ES|QL query.
* It returns:
* - For `histogram` instrument:
* - `PERCENTILE(..., 95)` if type is `exponential_histogram or tdigest`
* - For legacy histogram (field type + instrument both histogram): `PERCENTILE(TO_TDIGEST(...), 95)`
* - For `histogram` instrument: `PERCENTILE(..., 95)` if type is `exponential_histogram` or `tdigest`
* - `SUM(RATE(...))` for counter instruments
* - `AVG(...)` for other metric types
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ const mockExponentialHistogramMetric: MetricField = {
instrument: 'histogram',
};

const mockLegacyHistogramMetric: MetricField = {
...mockMetric,
name: 'histogram.legacy',
type: ES_FIELD_TYPES.HISTOGRAM,
instrument: 'histogram',
};

describe('createESQLQuery', () => {
it('should generate a basic AVG query for a metric field', () => {
const query = createESQLQuery({ metric: mockMetric });
Expand Down Expand Up @@ -90,6 +97,31 @@ TS metrics-*
);
});

it('should generate a PERCENTILE query for legacy histogram', () => {
const query = createESQLQuery({
metric: mockLegacyHistogramMetric,
});
expect(query).toBe(
`
TS metrics-*
| STATS PERCENTILE(TO_TDIGEST(histogram.legacy), 95) BY BUCKET(@timestamp, 100, ?_tstart, ?_tend)
`.trim()
);
});

it('should generate a PERCENTILE query for legacy histogram with multiple dimensions', () => {
const query = createESQLQuery({
metric: mockLegacyHistogramMetric,
splitAccessors: ['service.name', 'host.name'],
});
expect(query).toBe(
`
TS metrics-*
| STATS PERCENTILE(TO_TDIGEST(histogram.legacy), 95) BY BUCKET(@timestamp, 100, ?_tstart, ?_tend), \`service.name\`, \`host.name\`
`.trim()
);
});

it('should generate exponential histogram query with single dimension', () => {
const query = createESQLQuery({
metric: mockExponentialHistogramMetric,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,4 @@ export * from './metric_unit/get_lens_metric_format';
export * from './metric_unit/get_unit_label';
export * from './metric_unit/normalize_unit';
export * from './fields';
export * from './user_messages';
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { ES_FIELD_TYPES } from '@kbn/field-types';
import { isLegacyHistogram } from './legacy_histogram';

describe('isLegacyHistogram', () => {
it('returns true when type and instrument are both histogram', () => {
expect(isLegacyHistogram({ type: ES_FIELD_TYPES.HISTOGRAM, instrument: 'histogram' })).toBe(
true
);
});

it('returns false when type is histogram but instrument is not', () => {
expect(isLegacyHistogram({ type: ES_FIELD_TYPES.HISTOGRAM, instrument: 'gauge' })).toBe(false);
expect(isLegacyHistogram({ type: ES_FIELD_TYPES.HISTOGRAM, instrument: 'counter' })).toBe(
false
);
});

it('returns false when type is histogram and instrument is undefined', () => {
expect(isLegacyHistogram({ type: ES_FIELD_TYPES.HISTOGRAM, instrument: undefined })).toBe(
false
);
});

it('returns false when type is not histogram', () => {
expect(isLegacyHistogram({ type: ES_FIELD_TYPES.LONG, instrument: 'histogram' })).toBe(false);
expect(
isLegacyHistogram({ type: ES_FIELD_TYPES.EXPONENTIAL_HISTOGRAM, instrument: 'histogram' })
).toBe(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { ES_FIELD_TYPES } from '@kbn/field-types';
import type { MappingTimeSeriesMetricType } from '@elastic/elasticsearch/lib/api/types';

/**
* A legacy histogram is a metric where both the ES field type and the
* metric instrument are histogram.
*/
export const isLegacyHistogram = (field: {
type: ES_FIELD_TYPES;
instrument?: MappingTimeSeriesMetricType;
}): boolean => field.type === 'histogram' && field.instrument === 'histogram';
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { i18n } from '@kbn/i18n';
import type { UserMessage } from '@kbn/lens-plugin/public';

export const LEGACY_HISTOGRAM_USER_MESSAGES: UserMessage[] = [
{
uniqueId: 'metrics-experience-histogram-warning',
severity: 'warning',
shortMessage: i18n.translate('metricsExperience.userMessage.histogram.short', {
defaultMessage: 'Histogram warning',
}),
longMessage: i18n.translate('metricsExperience.userMessage.histogram.long', {
defaultMessage:
'Calculated assuming T-Digest encoding. If the histogram was encoded differently, the data is approximate',
}),
fixableInEditor: false,
displayLocations: [{ id: 'embeddableBadge' }],
},
];
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ describe('useMetricFields', () => {
expect(cpuField?.dimensions).toEqual([{ name: 'host.name', type: ES_FIELD_TYPES.KEYWORD }]);
});

it('filters out legacy histogram metric types', () => {
it('returns legacy histogram metric types', () => {
const sampleRows = new Map([
['cpu.usage', { 'cpu.usage': 0.75 }],
['http.request.duration', { 'http.request.duration': 150 }],
Expand All @@ -189,10 +189,10 @@ describe('useMetricFields', () => {

const { result } = renderHook(() => useMetricFields());

expect(result.current.allMetricFields).toHaveLength(2);
expect(result.current.allMetricFields).toHaveLength(3);
expect(
result.current.allMetricFields.find((f) => f.name === 'http.request.duration')
).toBeUndefined();
).toBeDefined();
expect(result.current.allMetricFields.find((f) => f.name === 'cpu.usage')).toBeDefined();
expect(result.current.allMetricFields.find((f) => f.name === 'memory.used')).toBeDefined();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,6 @@ export const useMetricFields = (): UseMetricFieldsReturn => {
const fields: MetricField[] = [];

for (const metricField of metricFields) {
// Filter out legacy histogram metric types
if (metricField.type === 'histogram') {
continue;
}

const row = getSampleRow(metricField.name);
if (row) {
const enriched = enrichMetricField(metricField, dimensions, row);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import React, { useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import {
Expand All @@ -19,9 +19,10 @@ import {
useEuiTheme,
type EuiFlexGridProps,
} from '@elastic/eui';

import type { MetricField, UnifiedMetricsGridProps } from '../../../types';
import { PAGE_SIZE } from '../../../common/constants';
import { isLegacyHistogram } from '../../../common/utils/legacy_histogram';
import { LEGACY_HISTOGRAM_USER_MESSAGES } from '../../../common/utils/user_messages';
import { MetricsGrid } from './metrics_grid';
import { Pagination } from '../../pagination';
import { usePagination } from './hooks';
Expand Down Expand Up @@ -71,6 +72,12 @@ export const MetricsExperienceGridContent = ({
[filteredFieldsCount]
);

const getUserMessages = useCallback(
(metric: MetricField) =>
isLegacyHistogram(metric) ? LEGACY_HISTOGRAM_USER_MESSAGES : undefined,
[]
);

return (
<EuiFlexGroup
direction="column"
Expand Down Expand Up @@ -146,6 +153,7 @@ export const MetricsExperienceGridContent = ({
fetchParams={fetchParams}
searchTerm={searchTerm}
whereStatements={whereStatements}
getUserMessages={getUserMessages}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,40 @@ describe('MetricsGrid', () => {
expect(Chart).toHaveBeenCalledWith(expect.objectContaining({ size: 's' }), expect.anything());
});

it('passes getUserMessages(metric) result to each chart when getUserMessages is provided', () => {
const messagesForCpu = [
{
uniqueId: 'cpu-message',
severity: 'warning' as const,
shortMessage: 'CPU',
longMessage: 'CPU message',
fixableInEditor: false,
displayLocations: [{ id: 'embeddableBadge' as const }],
},
];

const getUserMessages = jest.fn((metric: (typeof fields)[0]) =>
metric.name === 'system.cpu.utilization' ? messagesForCpu : undefined
);

renderMetricsGrid({ getUserMessages });

expect(getUserMessages).toHaveBeenCalledTimes(fields.length);
expect(getUserMessages).toHaveBeenNthCalledWith(1, fields[0]);
expect(getUserMessages).toHaveBeenNthCalledWith(2, fields[1]);

expect(Chart).toHaveBeenNthCalledWith(
1,
expect.objectContaining({ userMessages: messagesForCpu }),
expect.anything()
);
expect(Chart).toHaveBeenNthCalledWith(
2,
expect.objectContaining({ userMessages: undefined }),
expect.anything()
);
});

it('handles multiple dimensions correctly in ESQL query and chart layers', () => {
const multipleDimensions = [
{ name: 'host.name', type: ES_FIELD_TYPES.KEYWORD },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type { EuiFlexGridProps } from '@elastic/eui';
import { EuiFlexGrid, EuiFlexItem, useEuiTheme } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { css } from '@emotion/react';
import type { EmbeddableComponentProps } from '@kbn/lens-plugin/public';
import type { MetricField, Dimension, UnifiedMetricsGridProps } from '../../../types';
import type { ChartSize } from '../../chart';
import { Chart } from '../../chart';
Expand All @@ -33,6 +34,7 @@ export type MetricsGridProps = Pick<
discoverFetch$: UnifiedMetricsGridProps['fetch$'];
fields: MetricField[];
whereStatements?: string[];
getUserMessages?: (metric: MetricField) => EmbeddableComponentProps['userMessages'];
};

const getItemKey = (metric: MetricField, index: number) => {
Expand All @@ -50,6 +52,7 @@ export const MetricsGrid = ({
fetchParams,
discoverFetch$,
searchTerm,
getUserMessages,
}: MetricsGridProps) => {
const gridRef = useRef<HTMLDivElement>(null);
const { euiTheme } = useEuiTheme();
Expand Down Expand Up @@ -149,6 +152,7 @@ export const MetricsGrid = ({
onViewDetails={handleViewDetails}
searchTerm={searchTerm}
whereStatements={whereStatements}
userMessages={getUserMessages ? getUserMessages(metric) : undefined}
/>
</EuiFlexItem>
);
Expand Down Expand Up @@ -184,6 +188,7 @@ interface ChartItemProps
onFocusCell: (rowIndex: number, colIndex: number) => void;
onViewDetails: (index: number, esqlQuery: string, metric: MetricField) => void;
whereStatements?: string[];
userMessages?: EmbeddableComponentProps['userMessages'];
}

const ChartItem = React.memo(
Expand All @@ -206,6 +211,7 @@ const ChartItem = React.memo(
whereStatements,
onFocusCell,
onViewDetails,
userMessages,
}: ChartItemProps) => {
const { euiTheme } = useEuiTheme();
const colorPalette = useMemo(
Expand Down Expand Up @@ -254,6 +260,7 @@ const ChartItem = React.memo(
chartLayers={chartLayers}
titleHighlight={searchTerm}
extraDisabledActions={[ACTION_OPEN_IN_DISCOVER]}
userMessages={userMessages}
/>
</A11yGridCell>
);
Expand Down
Loading