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
@@ -0,0 +1,63 @@
/*
* 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 { convertMetricValue } from './convert_metric_value';

describe('convertMetricValue', () => {
describe('cpu metrics', () => {
it('should convert cpu percentage from 0-100 to 0-1 scale', () => {
expect(convertMetricValue('cpu', 50)).toBe(0.5);
expect(convertMetricValue('cpu', 100)).toBe(1);
expect(convertMetricValue('cpu', 0)).toBe(0);
});

it('should convert cpuV2 percentage from 0-100 to 0-1 scale', () => {
expect(convertMetricValue('cpuV2', 50)).toBe(0.5);
expect(convertMetricValue('cpuV2', 100)).toBe(1);
expect(convertMetricValue('cpuV2', 0)).toBe(0);
});
});

describe('memory metrics', () => {
it('should convert memory percentage from 0-100 to 0-1 scale', () => {
expect(convertMetricValue('memory', 75)).toBe(0.75);
expect(convertMetricValue('memory', 100)).toBe(1);
expect(convertMetricValue('memory', 0)).toBe(0);
});
});

describe('network metrics (bits to bytes)', () => {
it('should convert tx threshold from bits to bytes', () => {
expect(convertMetricValue('tx', 8)).toBe(1);
});

it('should convert rx threshold from bits to bytes', () => {
expect(convertMetricValue('rx', 8)).toBe(1);
expect(convertMetricValue('rx', 800)).toBe(100);
});

it('should convert txV2 threshold from bits to bytes', () => {
expect(convertMetricValue('txV2', 8)).toBe(1);
expect(convertMetricValue('txV2', 800)).toBe(100);
expect(convertMetricValue('txV2', 450000)).toBe(56250);
});

it('should convert rxV2 threshold from bits to bytes', () => {
expect(convertMetricValue('rxV2', 8)).toBe(1);
expect(convertMetricValue('rxV2', 800)).toBe(100);
expect(convertMetricValue('rxV2', 450000)).toBe(56250);
});
});

describe('metrics without conversion', () => {
it('should return the value unchanged for metrics without converters', () => {
expect(convertMetricValue('diskIOReadBytes', 1000)).toBe(1000);
expect(convertMetricValue('diskIOWriteBytes', 2000)).toBe(2000);
expect(convertMetricValue('logRate', 500)).toBe(500);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,6 @@ const converters: Record<string, (n: number) => number> = {
memory: (n) => Number(n) / 100,
tx: (n) => Number(n) / 8,
rx: (n) => Number(n) / 8,
txV2: (n) => Number(n) / 8,
rxV2: (n) => Number(n) / 8,
Comment on lines +24 to +25
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ECS, rxV2 and txV2 used the same fields as rx and tx, and we missed this conversion.
For Semconv, system.network.io is in bytes, but users set up these alerts in bits/s, so this conversion is also needed.

};
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type {
InfraTimerangeInput,
SnapshotCustomMetricInput,
} from '../../../../../common/http_api';
import { isMetricRate, isCustomMetricRate, isInterfaceRateAgg } from './is_rate';
import { isMetricRate, isCustomMetricRate, getInterfaceRateFields } from './is_rate';
import { createRateAggs } from './create_rate_aggs';
import { createLogRateAggs } from './create_log_rate_aggs';
import { createRateAggsWithInterface } from './create_rate_agg_with_interface';
Expand Down Expand Up @@ -46,17 +46,12 @@ export const createMetricAggregations = async (
const aggregations = await inventoryModel.metrics.getAggregations({ schema });
const metricAgg = aggregations.get(metric);

if (isInterfaceRateAgg(metricAgg)) {
const field = get(
metricAgg,
`${metric}_interfaces.aggregations.${metric}_interface_max.max.field`
) as unknown as string;
const interfaceField = get(
metricAgg,
`${metric}_interfaces.terms.field`
) as unknown as string;
return createRateAggsWithInterface(timerange, metric, field, interfaceField);
const interfaceRateConfig = getInterfaceRateFields(metricAgg, metric);
if (interfaceRateConfig) {
const { field, interfaceField, filter } = interfaceRateConfig;
return createRateAggsWithInterface(timerange, metric, field, interfaceField, filter);
}

if (isMetricRate(metricAgg)) {
const field = get(metricAgg, `${metric}_max.max.field`) as unknown as string;
return createRateAggs(timerange, metric, field);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,17 @@
* 2.0.
*/

import type { estypes } from '@elastic/elasticsearch';
import type { InfraTimerangeInput } from '../../../../../common/http_api';
import { calculateRateTimeranges } from './calculate_rate_timeranges';

export const createRateAggsWithInterface = (
timerange: InfraTimerangeInput,
id: string,
field: string,
interfaceField: string
) => {
interfaceField: string,
filter?: estypes.QueryDslQueryContainer
): Record<string, estypes.AggregationsAggregationContainer> => {
const { firstBucketRange, secondBucketRange, intervalInSeconds } =
calculateRateTimeranges(timerange);

Expand All @@ -31,29 +33,35 @@ export const createRateAggsWithInterface = (
},
};

return {
[`${id}_first_bucket`]: {
filter: {
range: {
'@timestamp': {
gte: firstBucketRange.from,
lt: firstBucketRange.to,
format: 'epoch_millis',
},
const createBucketFilter = (range: { from: number; to: number }) => {
const rangeFilter: estypes.QueryDslQueryContainer = {
range: {
'@timestamp': {
gte: range.from,
lt: range.to,
format: 'epoch_millis',
},
},
};

if (!filter) {
return rangeFilter;
}

return {
bool: {
must: [filter, rangeFilter],
},
};
};

return {
[`${id}_first_bucket`]: {
filter: createBucketFilter(firstBucketRange),
aggs: interfaceAggs,
},
[`${id}_second_bucket`]: {
filter: {
range: {
'@timestamp': {
gte: secondBucketRange.from,
lt: secondBucketRange.to,
format: 'epoch_millis',
},
},
},
filter: createBucketFilter(secondBucketRange),
aggs: interfaceAggs,
},
[id]: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
*/

import type { MetricsUIAggregation } from '@kbn/metrics-data-access-plugin/common';
import { isCustomMetricRate, isInterfaceRateAgg, isMetricRate, isRate } from './is_rate';
import {
isCustomMetricRate,
isInterfaceRateAgg,
isFilteredInterfaceRateAgg,
isMetricRate,
isRate,
getInterfaceRateFields,
} from './is_rate';
import type { SnapshotCustomMetricInput } from '../../../../../common/http_api';

const customMaxMetricMock: SnapshotCustomMetricInput = {
Expand All @@ -33,6 +40,79 @@ const metricMock: MetricsUIAggregation = {
},
};

// Mock for networkTrafficWithInterfaces pattern (direct interface rate)
const directInterfaceRateMock: MetricsUIAggregation = {
rx_interfaces: {
terms: { field: 'system.network.name' },
aggregations: {
rx_interface_max: { max: { field: 'system.network.in.bytes' } },
},
},
rx_sum_of_interfaces: {
sum_bucket: {
buckets_path: 'rx_interfaces>rx_interface_max',
},
},
rx_deriv: {
derivative: {
buckets_path: 'rx_sum_of_interfaces',
gap_policy: 'skip',
unit: '1s',
},
},
rx: {
bucket_script: {
buckets_path: { value: 'rx_deriv[normalized_value]' },
script: {
source: 'params.value > 0.0 ? params.value : 0.0',
lang: 'painless',
},
gap_policy: 'skip',
},
},
};

// Mock for networkTrafficWithInterfacesWithFilter pattern (filter-wrapped interface rate)
const filteredInterfaceRateMock: MetricsUIAggregation = {
rxv2_dimension: {
filter: {
term: {
direction: 'receive',
},
},
aggs: {
rxv2_interfaces: {
terms: { field: 'device' },
aggregations: {
rxv2_interface_max: { max: { field: 'system.network.io' } },
},
},
rxv2_sum_of_interfaces: {
sum_bucket: {
buckets_path: 'rxV2_interfaces>rxv2_interface_max',
},
},
},
},
rxv2_deriv: {
derivative: {
buckets_path: 'rxV2_dimension>rxv2_sum_of_interfaces',
gap_policy: 'skip',
unit: '1s',
},
},
rxV2: {
bucket_script: {
buckets_path: { value: 'rxV2_deriv[normalized_value]' },
script: {
source: 'params.value > 0.0 ? params.value : 0.0',
lang: 'painless',
},
gap_policy: 'skip',
},
},
};

describe('isRate', () => {
describe('isMetricRate', () => {
it('should return false when metric is undefined', () => {
Expand All @@ -50,24 +130,87 @@ describe('isRate', () => {
it("should return true when aggregation is equal to 'rate'", () => {
expect(isCustomMetricRate(customRateMetricMock)).toEqual(true);
});
});

describe('isInterfaceRateAgg', () => {
it('should return false if metric is undefined', () => {
expect(isInterfaceRateAgg(undefined)).toEqual(false);
});
it('should return true when correct metric is passed', () => {
expect(isInterfaceRateAgg(metricMock)).toEqual(true);
});
describe('isInterfaceRateAgg', () => {
it('should return false if metric is undefined', () => {
expect(isInterfaceRateAgg(undefined)).toEqual(false);
});
it('should return true for direct interface rate pattern', () => {
expect(isInterfaceRateAgg(directInterfaceRateMock)).toEqual(true);
});
it('should return true for filter-wrapped interface rate pattern', () => {
expect(isInterfaceRateAgg(filteredInterfaceRateMock)).toEqual(true);
});
it('should return false for non-interface rate metrics', () => {
expect(isInterfaceRateAgg({ simple_max: { max: { field: 'test' } } })).toEqual(false);
});
});

describe('isFilteredInterfaceRateAgg', () => {
it('should return false if metric is undefined', () => {
expect(isFilteredInterfaceRateAgg(undefined)).toEqual(false);
});
it('should return false for direct interface rate pattern', () => {
expect(isFilteredInterfaceRateAgg(directInterfaceRateMock)).toEqual(false);
});
it('should return true for filter-wrapped interface rate pattern', () => {
expect(isFilteredInterfaceRateAgg(filteredInterfaceRateMock)).toEqual(true);
});
it('should return false for non-interface rate metrics', () => {
expect(isFilteredInterfaceRateAgg({ simple_max: { max: { field: 'test' } } })).toEqual(false);
});
});

describe('getInterfaceRateFields', () => {
it('should return null if metric is undefined', () => {
expect(getInterfaceRateFields(undefined, 'rx')).toEqual(null);
});

describe('isRate', () => {
it('should return false when incorrect metrics are provided', () => {
expect(isRate({} as MetricsUIAggregation, {} as SnapshotCustomMetricInput)).toEqual(false);
it('should return null for non-interface rate metrics', () => {
expect(getInterfaceRateFields({ simple_max: { max: { field: 'test' } } }, 'test')).toEqual(
null
);
});

it('should extract fields from direct interface rate pattern', () => {
const result = getInterfaceRateFields(directInterfaceRateMock, 'rx');
expect(result).toEqual({
field: 'system.network.in.bytes',
interfaceField: 'system.network.name',
});
expect(result?.filter).toBeUndefined();
});

it('should return true when proper metric are provided', () => {
expect(isRate(metricMock, customRateMetricMock)).toEqual(true);
it('should extract fields and filter from filter-wrapped interface rate pattern', () => {
const result = getInterfaceRateFields(filteredInterfaceRateMock, 'rxv2');
expect(result).toEqual({
field: 'system.network.io',
interfaceField: 'device',
filter: {
term: {
direction: 'receive',
},
},
});
});
});

describe('isRate', () => {
it('should return false when incorrect metrics are provided', () => {
expect(isRate({} as MetricsUIAggregation, {} as SnapshotCustomMetricInput)).toEqual(false);
});

it('should return true when proper metric are provided', () => {
expect(isRate(metricMock, customRateMetricMock)).toEqual(true);
});

it('should return true for direct interface rate pattern', () => {
expect(isRate(directInterfaceRateMock)).toEqual(true);
});

it('should return true for filter-wrapped interface rate pattern', () => {
expect(isRate(filteredInterfaceRateMock)).toEqual(true);
});
});
});
Loading
Loading