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 @@ -8,10 +8,11 @@
import {
ECS_CONTAINER_CPU_USAGE_LIMIT_PCT,
ECS_CONTAINER_MEMORY_USAGE_BYTES,
otelDatasetFilter,
SEMCONV_DOCKER_CONTAINER_CPU_UTILIZATION,
SEMCONV_DOCKER_CONTAINER_MEMORY_PERCENT,
SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION,
SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION,
SEMCONV_CONTAINER_CPU_USAGE,
SEMCONV_CONTAINER_MEMORY_WORKING_SET,
} from '../shared/constants';
import { getOptionsForSchema } from './container_metrics_configs';

Expand All @@ -35,7 +36,7 @@ describe('container_metrics_configs', () => {
const { options } = getOptionsForSchema(true, false);

expect(options.groupBy).toBe('container.id');
expect(options.kuery).toBe('event.dataset: "dockerstatsreceiver.otel"');
expect(options.kuery).toBe(otelDatasetFilter('dockerstatsreceiver.otel'));
expect(options.metrics).toEqual(
expect.arrayContaining([
{ field: SEMCONV_DOCKER_CONTAINER_CPU_UTILIZATION, aggregation: 'avg' },
Expand All @@ -49,7 +50,7 @@ describe('container_metrics_configs', () => {
const { options } = getOptionsForSchema(true);

expect(options.groupBy).toBe('container.id');
expect(options.kuery).toBe('event.dataset: "dockerstatsreceiver.otel"');
expect(options.kuery).toBe(otelDatasetFilter('dockerstatsreceiver.otel'));
expect(options.metrics).toEqual(
expect.arrayContaining([
{ field: SEMCONV_DOCKER_CONTAINER_CPU_UTILIZATION, aggregation: 'avg' },
Expand All @@ -63,11 +64,11 @@ describe('container_metrics_configs', () => {
const { options } = getOptionsForSchema(true, true);

expect(options.groupBy).toBe('container.id');
expect(options.kuery).toBe('event.dataset: "kubeletstatsreceiver.otel"');
expect(options.kuery).toBe(otelDatasetFilter('kubeletstatsreceiver.otel'));
expect(options.metrics).toEqual(
expect.arrayContaining([
{ field: SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION, aggregation: 'avg' },
{ field: SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION, aggregation: 'avg' },
{ field: SEMCONV_CONTAINER_CPU_USAGE, aggregation: 'avg' },
{ field: SEMCONV_CONTAINER_MEMORY_WORKING_SET, aggregation: 'avg' },
])
);
expect(options.metrics).toHaveLength(2);
Expand All @@ -84,14 +85,16 @@ describe('container_metrics_configs', () => {
const kuery = 'container.id: "abc-123"';
const { options } = getOptionsForSchema(true, false, kuery);

expect(options.kuery).toBe(`event.dataset: "dockerstatsreceiver.otel" AND (${kuery})`);
expect(options.kuery).toBe(`${otelDatasetFilter('dockerstatsreceiver.otel')} AND (${kuery})`);
});

it('combines kuery with SemConv K8s when isOtel is true, isK8sContainer true, and kuery is provided', () => {
const kuery = 'container.id: "abc-123"';
const { options } = getOptionsForSchema(true, true, kuery);

expect(options.kuery).toBe(`event.dataset: "kubeletstatsreceiver.otel" AND (${kuery})`);
expect(options.kuery).toBe(
`${otelDatasetFilter('kubeletstatsreceiver.otel')} AND (${kuery})`
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ import { createMetricByFieldLookup, makeUnpackMetric, metricsToApiOptions } from
import {
ECS_CONTAINER_CPU_USAGE_LIMIT_PCT,
ECS_CONTAINER_MEMORY_USAGE_BYTES,
otelDatasetFilter,
SEMCONV_DOCKER_CONTAINER_MEMORY_PERCENT,
SEMCONV_DOCKER_CONTAINER_CPU_UTILIZATION,
SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION,
SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION,
SEMCONV_CONTAINER_CPU_USAGE,
SEMCONV_CONTAINER_MEMORY_WORKING_SET,
} from '../shared/constants';

// --- ECS (Elastic Common Schema) ---
Expand Down Expand Up @@ -43,7 +44,7 @@ type ContainerMetricsFieldSemconvDocker =

const containerMetricsQueryConfigSemconvDocker: MetricsQueryOptions<ContainerMetricsFieldSemconvDocker> =
{
sourceFilter: 'event.dataset: "dockerstatsreceiver.otel"',
sourceFilter: otelDatasetFilter('dockerstatsreceiver.otel'),
groupByField: 'container.id',
metricsMap: {
[SEMCONV_DOCKER_CONTAINER_CPU_UTILIZATION]: {
Expand All @@ -58,22 +59,24 @@ const containerMetricsQueryConfigSemconvDocker: MetricsQueryOptions<ContainerMet
};

// --- SemConv K8s (Kubernetes container metrics) ---
// Uses generic container metrics always emitted by kubeletstats,
// regardless of whether k8s resource limits are configured.
type ContainerMetricsFieldSemconvK8s =
| typeof SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION
| typeof SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION;
| typeof SEMCONV_CONTAINER_CPU_USAGE
| typeof SEMCONV_CONTAINER_MEMORY_WORKING_SET;

const containerMetricsQueryConfigSemconvK8s: MetricsQueryOptions<ContainerMetricsFieldSemconvK8s> =
{
sourceFilter: 'event.dataset: "kubeletstatsreceiver.otel"',
sourceFilter: otelDatasetFilter('kubeletstatsreceiver.otel'),
groupByField: 'container.id',
metricsMap: {
[SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION]: {
[SEMCONV_CONTAINER_CPU_USAGE]: {
aggregation: 'avg',
field: SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION,
field: SEMCONV_CONTAINER_CPU_USAGE,
},
[SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION]: {
[SEMCONV_CONTAINER_MEMORY_WORKING_SET]: {
aggregation: 'avg',
field: SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION,
field: SEMCONV_CONTAINER_MEMORY_WORKING_SET,
},
},
};
Expand All @@ -88,7 +91,7 @@ const metricByFieldSemconvDocker = createMetricByFieldLookup(
);
export const unpackMetricSemconvDocker = makeUnpackMetric(metricByFieldSemconvDocker);

const metricByFieldSemconvK8s = createMetricByFieldLookup(
export const metricByFieldSemconvK8s = createMetricByFieldLookup(
containerMetricsQueryConfigSemconvK8s.metricsMap
);
export const unpackMetricSemconvK8s = makeUnpackMetric(metricByFieldSemconvK8s);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ function containerNodeColumns({
ContainerMetricsTableProps,
'timerange' | 'isOtel' | 'metricsIndices' | 'isK8sContainer'
>): Array<EuiBasicTableColumn<ContainerNodeMetricsRow>> {
const memoryUnit = isOtel ? '%' : ' MB';
const memoryUnit = isOtel && !isK8sContainer ? '%' : ' MB';
return [
{
name: i18n.translate('xpack.metricsData.metricsTable.container.idColumnHeader', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@
import {
ECS_CONTAINER_CPU_USAGE_LIMIT_PCT,
ECS_CONTAINER_MEMORY_USAGE_BYTES,
otelDatasetFilter,
SEMCONV_DOCKER_CONTAINER_CPU_UTILIZATION,
SEMCONV_DOCKER_CONTAINER_MEMORY_PERCENT,
SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION,
SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION,
SEMCONV_CONTAINER_CPU_USAGE,
SEMCONV_CONTAINER_MEMORY_WORKING_SET,
} from '../shared/constants';
import { useContainerMetricsTable } from './use_container_metrics_table';
import { useInfrastructureNodeMetrics } from '../shared';
Expand Down Expand Up @@ -78,7 +79,7 @@ describe('useContainerMetricsTable hook', () => {
expect(useInfrastructureNodeMetricsMock).toHaveBeenCalledWith(
expect.objectContaining({
metricsExplorerOptions: expect.objectContaining({
kuery: `event.dataset: "dockerstatsreceiver.otel" AND (${kuery})`,
kuery: `${otelDatasetFilter('dockerstatsreceiver.otel')} AND (${kuery})`,
metrics: expect.arrayContaining([
expect.objectContaining({ field: SEMCONV_DOCKER_CONTAINER_CPU_UTILIZATION }),
expect.objectContaining({ field: SEMCONV_DOCKER_CONTAINER_MEMORY_PERCENT }),
Expand Down Expand Up @@ -110,13 +111,13 @@ describe('useContainerMetricsTable hook', () => {
expect(useInfrastructureNodeMetricsMock).toHaveBeenCalledWith(
expect.objectContaining({
metricsExplorerOptions: expect.objectContaining({
kuery: `event.dataset: "kubeletstatsreceiver.otel" AND (${kuery})`,
kuery: `${otelDatasetFilter('kubeletstatsreceiver.otel')} AND (${kuery})`,
metrics: expect.arrayContaining([
expect.objectContaining({
field: SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION,
field: SEMCONV_CONTAINER_CPU_USAGE,
}),
expect.objectContaining({
field: SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION,
field: SEMCONV_CONTAINER_MEMORY_WORKING_SET,
}),
]),
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ import {
ECS_CONTAINER_MEMORY_USAGE_BYTES,
SEMCONV_DOCKER_CONTAINER_CPU_UTILIZATION,
SEMCONV_DOCKER_CONTAINER_MEMORY_PERCENT,
SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION,
SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION,
SEMCONV_CONTAINER_CPU_USAGE,
SEMCONV_CONTAINER_MEMORY_WORKING_SET,
} from '../shared/constants';

export { metricByFieldEcs, metricByField } from './container_metrics_configs';
Expand Down Expand Up @@ -52,12 +52,11 @@ const semconvDockerUnpackMetrics = (row: MetricsExplorerRow) => {
};

const semconvK8sUnpackMetrics = (row: MetricsExplorerRow) => {
// semconv k8s unpack metrics
const cpuUtilization = unpackMetricSemconvK8s(row, SEMCONV_K8S_CONTAINER_CPU_LIMIT_UTILIZATION);
const memoryUsage = unpackMetricSemconvK8s(row, SEMCONV_K8S_CONTAINER_MEMORY_LIMIT_UTILIZATION);
const cpuUsage = unpackMetricSemconvK8s(row, SEMCONV_CONTAINER_CPU_USAGE);
const memoryBytes = unpackMetricSemconvK8s(row, SEMCONV_CONTAINER_MEMORY_WORKING_SET);
return {
averageCpuUsage: cpuUtilization !== null ? scaleUpPercentage(cpuUtilization) : null,
averageMemoryUsage: memoryUsage !== null ? scaleUpPercentage(memoryUsage) : null,
averageCpuUsage: cpuUsage !== null ? scaleUpPercentage(cpuUsage) : null,
Comment thread
rmyz marked this conversation as resolved.
averageMemoryUsage: memoryBytes !== null ? Math.floor(memoryBytes / 1_000_000) : null,
};
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

import {
otelDatasetFilter,
SEMCONV_SYSTEM_CPU_LOGICAL_COUNT,
SEMCONV_SYSTEM_CPU_UTILIZATION,
SEMCONV_SYSTEM_MEMORY_LIMIT,
SEMCONV_SYSTEM_MEMORY_TOTAL,
SEMCONV_SYSTEM_MEMORY_UTILIZATION,
SYSTEM_CPU_CORES,
SYSTEM_CPU_TOTAL_NORM_PCT,
Expand Down Expand Up @@ -81,18 +82,115 @@ describe('useHostMetricsTable hook', () => {
expect(useInfrastructureNodeMetricsMock).toHaveBeenCalledWith(
expect.objectContaining({
metricsExplorerOptions: expect.objectContaining({
kuery: `event.dataset: "hostmetricsreceiver.otel" AND (${kuery})`,
kuery: `${otelDatasetFilter('hostmetricsreceiver.otel')} AND (${kuery})`,
metrics: expect.arrayContaining([
expect.objectContaining({ field: SEMCONV_SYSTEM_CPU_LOGICAL_COUNT }),
expect.objectContaining({ field: SEMCONV_SYSTEM_CPU_UTILIZATION }),
expect.objectContaining({ field: SEMCONV_SYSTEM_MEMORY_LIMIT }),
expect.objectContaining({ field: SEMCONV_SYSTEM_MEMORY_TOTAL }),
expect.objectContaining({ field: SEMCONV_SYSTEM_MEMORY_UTILIZATION }),
]),
}),
})
);
});

it('should transform OTel rows into populated host metrics', () => {
const kuery = `host.name: "gke-edge-oblt-pool-1-9a60016d-lgg9"`;

useInfrastructureNodeMetricsMock.mockClear();
useInfrastructureNodeMetricsMock.mockReturnValue({
isLoading: true,
data: { state: 'empty-indices' },
metricIndices: 'test-index',
});

renderHook(() =>
useHostMetricsTable({
timerange: { from: 'now-30d', to: 'now' },
kuery,
metricsClient: createMetricsClientMock({}),
isOtel: true,
})
);

const lastCallArgs = useInfrastructureNodeMetricsMock.mock.calls.at(-1);
const { transform, metricsExplorerOptions } = lastCallArgs?.[0] ?? {};
expect(transform).toBeDefined();

const metrics = metricsExplorerOptions?.metrics ?? [];
const metricIndexByField = new Map(metrics.map((metric, index) => [metric.field, index]));

const row = transform!({
id: 'otel-host-1',
columns: [],
rows: [
{
timestamp: Date.now(),
[`metric_${metricIndexByField.get(SEMCONV_SYSTEM_CPU_LOGICAL_COUNT)}`]: 8,
[`metric_${metricIndexByField.get(SEMCONV_SYSTEM_CPU_UTILIZATION)}`]: 0.42,
[`metric_${metricIndexByField.get(SEMCONV_SYSTEM_MEMORY_TOTAL)}`]: 16_000_000_000,
[`metric_${metricIndexByField.get(SEMCONV_SYSTEM_MEMORY_UTILIZATION)}`]: 0.25,
},
],
});

expect(row).toEqual({
name: 'otel-host-1',
cpuCount: 8,
averageCpuUsagePercent: 42,
totalMemoryMegabytes: 16000,
averageMemoryUsagePercent: 25,
});
});

it('should return null totalMemoryMegabytes when memory total metric is absent (B=0 in equation)', () => {
const kuery = `host.name: "gke-edge-oblt-pool-1-9a60016d-lgg9"`;

useInfrastructureNodeMetricsMock.mockClear();
useInfrastructureNodeMetricsMock.mockReturnValue({
isLoading: true,
data: { state: 'empty-indices' },
metricIndices: 'test-index',
});

renderHook(() =>
useHostMetricsTable({
timerange: { from: 'now-30d', to: 'now' },
kuery,
metricsClient: createMetricsClientMock({}),
isOtel: true,
})
);

const lastCallArgs = useInfrastructureNodeMetricsMock.mock.calls.at(-1);
const { transform, metricsExplorerOptions } = lastCallArgs?.[0] ?? {};
expect(transform).toBeDefined();

const metrics = metricsExplorerOptions?.metrics ?? [];
const metricIndexByField = new Map(metrics.map((metric, index) => [metric.field, index]));

const row = transform!({
id: 'otel-host-2',
columns: [],
rows: [
{
timestamp: Date.now(),
[`metric_${metricIndexByField.get(SEMCONV_SYSTEM_CPU_LOGICAL_COUNT)}`]: 4,
[`metric_${metricIndexByField.get(SEMCONV_SYSTEM_CPU_UTILIZATION)}`]: 0.1,
[`metric_${metricIndexByField.get(SEMCONV_SYSTEM_MEMORY_UTILIZATION)}`]: 0.5,
},
],
});

expect(row).toEqual({
name: 'otel-host-2',
cpuCount: 4,
averageCpuUsagePercent: 10,
totalMemoryMegabytes: null,
averageMemoryUsagePercent: 50,
});
});

it('should call useInfrastructureNodeMetrics with ECS metrics when isOtel is false', () => {
const kuery = `host.name: "gke-edge-oblt-pool-1-9a60016d-lgg9"`;

Expand Down
Loading
Loading