Skip to content

Commit

Permalink
[Metrics UI] Fix a bug in Metric Threshold query filter construction (e…
Browse files Browse the repository at this point in the history
…lastic#70672)

Co-authored-by: Elastic Machine <[email protected]>
# Conflicts:
#	x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts
  • Loading branch information
Zacqary committed Jul 6, 2020
1 parent fe07b03 commit 16722d9
Show file tree
Hide file tree
Showing 2 changed files with 198 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { MetricExpressionParams } from '../types';
import { getElasticsearchMetricQuery } from './metric_query';

describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
const expressionParams = {
metric: 'system.is.a.good.puppy.dog',
aggType: 'avg',
timeUnit: 'm',
timeSize: 1,
} as MetricExpressionParams;

const timefield = '@timestamp';
const groupBy = 'host.doggoname';

describe('when passed no filterQuery', () => {
const searchBody = getElasticsearchMetricQuery(expressionParams, timefield, groupBy);
test('includes a range filter', () => {
expect(
searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range'))
).toBeTruthy();
});

test('includes a metric field filter', () => {
expect(searchBody.query.bool.filter).toMatchObject(
expect.arrayContaining([{ exists: { field: 'system.is.a.good.puppy.dog' } }])
);
});
});

describe('when passed a filterQuery', () => {
const filterQuery =
// This is adapted from a real-world query that previously broke alerts
// We want to make sure it doesn't override any existing filters
'{"bool":{"filter":[{"bool":{"filter":[{"bool":{"must_not":[{"bool":{"should":[{"query_string":{"query":"bark*","fields":["host.name^1.0"],"type":"best_fields","default_operator":"or","max_determinized_states":10000,"enable_position_increments":true,"fuzziness":"AUTO","fuzzy_prefix_length":0,"fuzzy_max_expansions":50,"phrase_slop":0,"escape":false,"auto_generate_synonyms_phrase_query":true,"fuzzy_transpositions":true,"boost":1}}],"adjust_pure_negative":true,"minimum_should_match":"1","boost":1}}],"adjust_pure_negative":true,"boost":1}},{"bool":{"must_not":[{"bool":{"should":[{"query_string":{"query":"woof*","fields":["host.name^1.0"],"type":"best_fields","default_operator":"or","max_determinized_states":10000,"enable_position_increments":true,"fuzziness":"AUTO","fuzzy_prefix_length":0,"fuzzy_max_expansions":50,"phrase_slop":0,"escape":false,"auto_generate_synonyms_phrase_query":true,"fuzzy_transpositions":true,"boost":1}}],"adjust_pure_negative":true,"minimum_should_match":"1","boost":1}}],"adjust_pure_negative":true,"boost":1}}],"adjust_pure_negative":true,"boost":1}}],"adjust_pure_negative":true,"boost":1}}';

const searchBody = getElasticsearchMetricQuery(
expressionParams,
timefield,
groupBy,
filterQuery
);
test('includes a range filter', () => {
expect(
searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range'))
).toBeTruthy();
});

test('includes a metric field filter', () => {
expect(searchBody.query.bool.filter).toMatchObject(
expect.arrayContaining([{ exists: { field: 'system.is.a.good.puppy.dog' } }])
);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
import { MetricExpressionParams, Aggregators } from '../types';
import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds';
import { getDateHistogramOffset } from '../../../snapshot/query_helpers';
import { createPercentileAggregation } from './create_percentile_aggregation';

const MINIMUM_BUCKETS = 5;

const getParsedFilterQuery: (filterQuery: string | undefined) => Record<string, any> | null = (
filterQuery
) => {
if (!filterQuery) return null;
return JSON.parse(filterQuery);
};

export const getElasticsearchMetricQuery = (
{ metric, aggType, timeUnit, timeSize }: MetricExpressionParams,
timefield: string,
groupBy?: string | string[],
filterQuery?: string,
timeframe?: { start: number; end: number }
) => {
if (aggType === Aggregators.COUNT && metric) {
throw new Error('Cannot aggregate document count with a metric');
}
if (aggType !== Aggregators.COUNT && !metric) {
throw new Error('Can only aggregate without a metric if using the document count aggregator');
}
const interval = `${timeSize}${timeUnit}`;
const intervalAsSeconds = getIntervalInSeconds(interval);

const to = timeframe ? timeframe.end : Date.now();
// We need enough data for 5 buckets worth of data. We also need
// to convert the intervalAsSeconds to milliseconds.
const minimumFrom = to - intervalAsSeconds * 1000 * MINIMUM_BUCKETS;

const from = timeframe && timeframe.start <= minimumFrom ? timeframe.start : minimumFrom;

const offset = getDateHistogramOffset(from, interval);

const aggregations =
aggType === Aggregators.COUNT
? {}
: aggType === Aggregators.RATE
? networkTraffic('aggregatedValue', metric)
: aggType === Aggregators.P95 || aggType === Aggregators.P99
? createPercentileAggregation(aggType, metric)
: {
aggregatedValue: {
[aggType]: {
field: metric,
},
},
};

const baseAggs = {
aggregatedIntervals: {
date_histogram: {
field: timefield,
fixed_interval: interval,
offset,
extended_bounds: {
min: from,
max: to,
},
},
aggregations,
},
};

const aggs = groupBy
? {
groupings: {
composite: {
size: 10,
sources: Array.isArray(groupBy)
? groupBy.map((field, index) => ({
[`groupBy${index}`]: {
terms: { field },
},
}))
: [
{
groupBy0: {
terms: {
field: groupBy,
},
},
},
],
},
aggs: baseAggs,
},
}
: baseAggs;

const rangeFilters = [
{
range: {
'@timestamp': {
gte: from,
lte: to,
format: 'epoch_millis',
},
},
},
];

const metricFieldFilters = metric
? [
{
exists: {
field: metric,
},
},
]
: [];

const parsedFilterQuery = getParsedFilterQuery(filterQuery);

return {
query: {
bool: {
filter: [
...rangeFilters,
...metricFieldFilters,
...(parsedFilterQuery ? [parsedFilterQuery] : []),
],
},
},
size: 0,
aggs,
};
};

0 comments on commit 16722d9

Please sign in to comment.