[Custom threshold] Add group by filters to the custom threshold view in app URL (#177016)

Resolves #173713

## Summary

This PR adds group by filters to the view in the app URL for the custom
threshold rule:


![image](c84eecfc-b196-4268-acdd-57bae86ac4c4)

I also moved some types to the common folder and adjusted them to match
the reality.

## 🧪 How to test
- Create a custom threshold rule with group by
   - One with persisted data view
   - One with an ad-hoc data view
- Check the view in app link in the alerts table, you should also see
the group filters there.
- Check the view in app URL from the actions, it should also include the
group filters.
This commit is contained in:
Maryam Saeidi 2024-02-22 17:24:43 +01:00 committed by GitHub
parent bf4b70ceb4
commit 637de1dff5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 303 additions and 144 deletions

View file

@ -37,12 +37,12 @@ export const legacyExperimentalFieldMap = {
},
[ALERT_GROUP_FIELD]: {
type: 'keyword',
array: false,
array: true,
required: false,
},
[ALERT_GROUP_VALUE]: {
type: 'keyword',
array: false,
array: true,
required: false,
},
} as const;

View file

@ -80,8 +80,8 @@ const ObservabilityApmAlertOptional = rt.partial({
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
'kibana.alert.group': rt.array(
rt.partial({
field: schemaString,
value: schemaString,
field: schemaStringArray,
value: schemaStringArray,
})
),
labels: schemaUnknown,

View file

@ -78,8 +78,8 @@ const ObservabilityLogsAlertOptional = rt.partial({
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
'kibana.alert.group': rt.array(
rt.partial({
field: schemaString,
value: schemaString,
field: schemaStringArray,
value: schemaStringArray,
})
),
});

View file

@ -78,8 +78,8 @@ const ObservabilityMetricsAlertOptional = rt.partial({
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
'kibana.alert.group': rt.array(
rt.partial({
field: schemaString,
value: schemaString,
field: schemaStringArray,
value: schemaStringArray,
})
),
});

View file

@ -77,8 +77,8 @@ const ObservabilitySloAlertOptional = rt.partial({
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
'kibana.alert.group': rt.array(
rt.partial({
field: schemaString,
value: schemaString,
field: schemaStringArray,
value: schemaStringArray,
})
),
'slo.id': schemaString,

View file

@ -81,8 +81,8 @@ const ObservabilityUptimeAlertOptional = rt.partial({
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
'kibana.alert.group': rt.array(
rt.partial({
field: schemaString,
value: schemaString,
field: schemaStringArray,
value: schemaStringArray,
})
),
'monitor.id': schemaString,

View file

@ -123,12 +123,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -207,12 +207,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -291,12 +291,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -375,12 +375,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -447,12 +447,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -490,12 +490,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -533,12 +533,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -660,12 +660,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -9085,12 +9085,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -9394,12 +9394,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -9493,12 +9493,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -9592,12 +9592,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -9691,12 +9691,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
@ -9796,12 +9796,12 @@ Object {
"type": "object",
},
"kibana.alert.group.field": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},
"kibana.alert.group.value": Object {
"array": false,
"array": true,
"required": false,
"type": "keyword",
},

View file

@ -48,7 +48,13 @@ describe('getViewInAppUrl', () => {
logsExplorerLocator,
startedAt,
endedAt,
filter: 'mockedFilter',
searchConfiguration: {
index: {},
query: {
language: '',
query: 'mockedFilter',
},
},
dataViewId: 'mockedDataViewId',
};
@ -56,6 +62,7 @@ describe('getViewInAppUrl', () => {
expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
dataset: args.dataViewId,
timeRange: returnedTimeRange,
filters: [],
query: {
query: 'mockedFilter and mockedCountFilter',
language: 'kuery',
@ -81,6 +88,7 @@ describe('getViewInAppUrl', () => {
expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
dataset: undefined,
timeRange: returnedTimeRange,
filters: [],
query: {
query: 'mockedCountFilter',
language: 'kuery',
@ -93,13 +101,20 @@ describe('getViewInAppUrl', () => {
logsExplorerLocator,
startedAt,
endedAt,
filter: 'mockedFilter',
searchConfiguration: {
index: {},
query: {
language: '',
query: 'mockedFilter',
},
},
};
expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl');
expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
dataset: undefined,
timeRange: returnedTimeRange,
filters: [],
query: {
query: 'mockedFilter',
language: 'kuery',
@ -118,6 +133,7 @@ describe('getViewInAppUrl', () => {
expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
dataset: undefined,
timeRange: returnedTimeRange,
filters: [],
query: {
query: '',
language: 'kuery',
@ -148,10 +164,86 @@ describe('getViewInAppUrl', () => {
expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
dataset: undefined,
timeRange: returnedTimeRange,
filters: [],
query: {
query: '',
language: 'kuery',
},
});
});
it('should call getRedirectUrl with filters if group and searchConfiguration filter are provided', () => {
const args: GetViewInAppUrlArgs = {
metrics: [
{
name: 'A',
aggType: Aggregators.COUNT,
filter: 'mockedCountFilter',
},
{
name: 'A',
aggType: Aggregators.AVERAGE,
field: 'mockedAvgField',
},
],
logsExplorerLocator,
startedAt,
endedAt,
searchConfiguration: {
index: {},
query: {
language: '',
query: 'mockedFilter',
},
filter: [
{
meta: {},
query: {
term: {
field: {
value: 'justTesting',
},
},
},
},
],
},
groups: [
{
field: 'host.name',
value: 'host-1',
},
],
};
expect(getViewInAppUrl(args)).toBe('mockedGetRedirectUrl');
expect(logsExplorerLocator.getRedirectUrl).toHaveBeenCalledWith({
dataset: undefined,
timeRange: returnedTimeRange,
filters: [
{
meta: {},
query: {
term: {
field: {
value: 'justTesting',
},
},
},
},
{
meta: {},
query: {
match_phrase: {
'host.name': 'host-1',
},
},
},
],
query: {
query: 'mockedFilter',
language: 'kuery',
},
});
});
});

View file

@ -5,31 +5,39 @@
* 2.0.
*/
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import type { TimeRange } from '@kbn/es-query';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability';
import type { TimeRange } from '@kbn/es-query';
import { getPaddedAlertTimeRange } from '@kbn/observability-get-padded-alert-time-range-util';
import type { LocatorPublic } from '@kbn/share-plugin/common';
import { getGroupFilters } from './helpers/get_group';
import { Group, SearchConfigurationWithExtractedReferenceType } from './types';
import type { CustomThresholdExpressionMetric } from './types';
export interface GetViewInAppUrlArgs {
searchConfiguration?: SearchConfigurationWithExtractedReferenceType;
dataViewId?: string;
endedAt?: string;
startedAt?: string;
filter?: string;
groups?: Group[];
logsExplorerLocator?: LocatorPublic<LogsExplorerLocatorParams>;
metrics?: CustomThresholdExpressionMetric[];
startedAt?: string;
}
export const getViewInAppUrl = ({
dataViewId,
endedAt,
startedAt = new Date().toISOString(),
filter,
groups,
logsExplorerLocator,
metrics = [],
searchConfiguration,
startedAt = new Date().toISOString(),
}: GetViewInAppUrlArgs) => {
if (!logsExplorerLocator) return '';
const dataset = searchConfiguration?.index.title ?? dataViewId;
const searchConfigurationQuery = searchConfiguration?.query.query;
const searchConfigurationFilters = searchConfiguration?.filter || [];
const groupFilters = getGroupFilters(groups);
const timeRange: TimeRange | undefined = getPaddedAlertTimeRange(startedAt, endedAt);
timeRange.to = endedAt ? timeRange.to : 'now';
@ -39,17 +47,18 @@ export const getViewInAppUrl = ({
};
const isOneCountConditionWithFilter =
metrics.length === 1 && metrics[0].aggType === 'count' && metrics[0].filter;
if (filter && isOneCountConditionWithFilter) {
query.query = `${filter} and ${metrics[0].filter}`;
if (searchConfigurationQuery && isOneCountConditionWithFilter) {
query.query = `${searchConfigurationQuery} and ${metrics[0].filter}`;
} else if (isOneCountConditionWithFilter) {
query.query = metrics[0].filter!;
} else if (filter) {
query.query = filter;
} else if (searchConfigurationQuery) {
query.query = searchConfigurationQuery;
}
return logsExplorerLocator?.getRedirectUrl({
dataset: dataViewId,
dataset,
timeRange,
query,
filters: [...searchConfigurationFilters, ...groupFilters],
});
};

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { getGroupQueries, getGroupFilters } from './get_group';
import { getGroupQueries, getGroupFilters, getGroups } from './get_group';
describe('getGroup', () => {
describe('getGroupQueries', () => {
@ -16,8 +16,8 @@ describe('getGroup', () => {
];
expect(getGroupQueries(groups)).toEqual([
{ term: { 'container.id': { value: 'container-0' } } },
{ term: { 'host.name': { value: 'host-0' } } },
{ match_phrase: { 'container.id': 'container-0' } },
{ match_phrase: { 'host.name': 'host-0' } },
]);
});
@ -29,8 +29,8 @@ describe('getGroup', () => {
const fieldName = 'custom.field';
expect(getGroupQueries(groups, fieldName)).toEqual([
{ term: { 'custom.field': { value: 'container-0' } } },
{ term: { 'custom.field': { value: 'host-0' } } },
{ match_phrase: { 'custom.field': 'container-0' } },
{ match_phrase: { 'custom.field': 'host-0' } },
]);
});
@ -51,11 +51,11 @@ describe('getGroup', () => {
expect(getGroupFilters(groups)).toEqual([
{
meta: {},
query: { term: { 'container.id': { value: 'container-0' } } },
query: { match_phrase: { 'container.id': 'container-0' } },
},
{
meta: {},
query: { term: { 'host.name': { value: 'host-0' } } },
query: { match_phrase: { 'host.name': 'host-0' } },
},
]);
});
@ -70,11 +70,11 @@ describe('getGroup', () => {
expect(getGroupFilters(groups, fieldName)).toEqual([
{
meta: {},
query: { term: { 'custom.field': { value: 'container-0' } } },
query: { match_phrase: { 'custom.field': 'container-0' } },
},
{
meta: {},
query: { term: { 'custom.field': { value: 'host-0' } } },
query: { match_phrase: { 'custom.field': 'host-0' } },
},
]);
});
@ -85,4 +85,21 @@ describe('getGroup', () => {
expect(getGroupFilters(groups)).toEqual([]);
});
});
describe('getGroups', () => {
it('should generate correct filter with default field name', () => {
const fields = ['container.id', 'host.name'];
const values = ['container-0', 'host-0'];
const groups = [
{ field: 'container.id', value: 'container-0' },
{ field: 'host.name', value: 'host-0' },
];
expect(getGroups(fields, values)).toEqual(groups);
});
it('should return empty array if fields and values are empty', () => {
expect(getGroups([], [])).toEqual([]);
});
});
});

View file

@ -22,10 +22,8 @@ export const getGroupQueries = (
return (
(groups &&
groups.map((group) => ({
term: {
[groupFieldName || group.field]: {
value: group.value,
},
match_phrase: {
[groupFieldName || group.field]: group.value,
},
}))) ||
[]
@ -35,3 +33,10 @@ export const getGroupQueries = (
export const getGroupFilters = (groups?: Group[], groupFieldName?: string): Filter[] => {
return getGroupQueries(groups, groupFieldName).map((query) => ({ meta: {}, query }));
};
export const getGroups = (fields: string[], values: string[]): Group[] => {
return fields.map((_, index) => ({
field: fields[index],
value: values[index],
}));
};

View file

@ -6,7 +6,8 @@
*/
import * as rt from 'io-ts';
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { DataViewSpec, SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import { Filter } from '@kbn/es-query';
import { TimeUnitChar } from '../utils/formatters/duration';
export const ThresholdFormatterTypeRT = rt.keyof({
@ -110,6 +111,30 @@ export enum InfraFormatterType {
percent = 'percent',
}
export interface Group {
field: string;
value: string;
}
export interface SearchConfigurationType {
index: SerializedSearchSourceFields;
query: {
query: string;
language: string;
};
filter?: Filter[];
}
export interface SearchConfigurationWithExtractedReferenceType {
// Index will be a data view spec after extracting references
index: DataViewSpec;
query: {
query: string;
language: string;
};
filter?: Filter[];
}
// Custom threshold alert types
// Alert fields['kibana.alert.group] type

View file

@ -7,10 +7,8 @@ Array [
Object {
"meta": Object {},
"query": Object {
"term": Object {
"host.name": Object {
"value": "host-1",
},
"match_phrase": Object {
"host.name": "host-1",
},
},
},

View file

@ -40,12 +40,12 @@ import moment from 'moment';
import { AlertHistoryChart } from './alert_history';
import { useLicense } from '../../../../hooks/use_license';
import { useKibana } from '../../../../utils/kibana_react';
import { getGroupFilters } from '../../../../../common/custom_threshold_rule/helpers/get_group';
import { metricValueFormatter } from '../../../../../common/custom_threshold_rule/metric_value_formatter';
import { AlertSummaryField } from '../../../..';
import { AlertParams, MetricExpression } from '../../types';
import { TIME_LABELS } from '../criterion_preview_chart/criterion_preview_chart';
import { Threshold } from '../custom_threshold';
import { getGroupFilters } from '../helpers/get_group';
import { CustomThresholdRule, CustomThresholdAlert } from '../types';
import { LogRateAnalysis } from './log_rate_analysis';
import { Groups } from './groups';

View file

@ -25,9 +25,12 @@ import { i18n } from '@kbn/i18n';
import { ALERT_GROUP, ALERT_GROUP_VALUE, type AlertConsumers } from '@kbn/rule-data-utils';
import { useAlertsHistory } from '@kbn/observability-alert-details';
import { convertTo } from '../../../../../common/utils/formatters';
import {
getGroupFilters,
getGroupQueries,
} from '../../../../../common/custom_threshold_rule/helpers/get_group';
import { useKibana } from '../../../../utils/kibana_react';
import { AlertParams } from '../../types';
import { getGroupFilters, getGroupQueries } from '../helpers/get_group';
import { RuleConditionChart } from '../rule_condition_chart/rule_condition_chart';
import { CustomThresholdAlert, CustomThresholdRule } from '../types';

View file

@ -117,10 +117,8 @@ Object {
},
},
Object {
"term": Object {
"groupByField": Object {
"value": "groupByValue",
},
"match_phrase": Object {
"groupByField": "groupByValue",
},
},
],
@ -185,17 +183,13 @@ Object {
},
},
Object {
"term": Object {
"groupByField": Object {
"value": "groupByValue",
},
"match_phrase": Object {
"groupByField": "groupByValue",
},
},
Object {
"term": Object {
"secondGroupByField": Object {
"value": "secondGroupByValue",
},
"match_phrase": Object {
"secondGroupByField": "secondGroupByValue",
},
},
],
@ -211,10 +205,8 @@ Object {
"bool": Object {
"filter": Array [
Object {
"term": Object {
"groupByField": Object {
"value": "groupByValue",
},
"match_phrase": Object {
"groupByField": "groupByValue",
},
},
],

View file

@ -7,13 +7,15 @@
import { get } from 'lodash';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import { getGroupFilters } from '../../helpers/get_group';
import { getGroupFilters } from '../../../../../../common/custom_threshold_rule/helpers/get_group';
import { Aggregators } from '../../../../../../common/custom_threshold_rule/types';
import { buildEsQuery } from '../../../../../utils/build_es_query';
import type {
CustomThresholdExpressionMetric,
Group,
} from '../../../../../../common/custom_threshold_rule/types';
import type { TopAlert } from '../../../../../typings/alerts';
import type { CustomThresholdRuleTypeParams } from '../../../types';
import type { CustomThresholdExpressionMetric } from '../../../../../../common/custom_threshold_rule/types';
import type { Group } from '../../types';
const getKuery = (metrics: CustomThresholdExpressionMetric[], filter?: string) => {
let query = '';

View file

@ -12,8 +12,3 @@ import { CustomThresholdAlertFields, CustomThresholdRuleTypeParams } from '../ty
// TODO Use a generic props for app sections https://github.com/elastic/kibana/issues/152690
export type CustomThresholdRule = Rule<CustomThresholdRuleTypeParams>;
export type CustomThresholdAlert = TopAlert<CustomThresholdAlertFields>;
export interface Group {
field: string;
value: string;
}

View file

@ -9,6 +9,8 @@ import { lazy } from 'react';
import { i18n } from '@kbn/i18n';
import { SerializedSearchSourceFields } from '@kbn/data-plugin/common';
import {
ALERT_GROUP_FIELD,
ALERT_GROUP_VALUE,
ALERT_REASON,
ALERT_RULE_PARAMETERS,
ALERT_START,
@ -17,12 +19,14 @@ import {
import type { LocatorPublic } from '@kbn/share-plugin/common';
import { LogsExplorerLocatorParams } from '@kbn/deeplinks-observability';
import { IUiSettingsClient } from '@kbn/core-ui-settings-browser';
import type { MetricExpression } from '../components/custom_threshold/types';
import type {
CustomMetricExpressionParams,
CustomThresholdExpressionMetric,
SearchConfigurationWithExtractedReferenceType,
} from '../../common/custom_threshold_rule/types';
import type { MetricExpression } from '../components/custom_threshold/types';
import { getViewInAppUrl } from '../../common/custom_threshold_rule/get_view_in_app_url';
import { getGroups } from '../../common/custom_threshold_rule/helpers/get_group';
import { SLO_ID_FIELD, SLO_INSTANCE_ID_FIELD } from '../../common/field_names/slo';
import { ObservabilityRuleTypeRegistry } from './create_observability_rule_type_registry';
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../../common/constants';
@ -83,7 +87,7 @@ const thresholdDefaultRecoveryMessage = i18n.translate(
}
);
const getDataViewId = (searchConfiguration?: SerializedSearchSourceFields) =>
const getDataViewId = (searchConfiguration?: SearchConfigurationWithExtractedReferenceType) =>
typeof searchConfiguration?.index === 'string'
? searchConfiguration.index
: searchConfiguration?.index?.title;
@ -147,8 +151,9 @@ export const registerObservabilityRuleTypes = async (
defaultRecoveryMessage: thresholdDefaultRecoveryMessage,
requiresAppContext: false,
format: ({ fields }) => {
const groups = getGroups(fields[ALERT_GROUP_FIELD], fields[ALERT_GROUP_VALUE]);
const searchConfiguration = fields[ALERT_RULE_PARAMETERS]?.searchConfiguration as
| SerializedSearchSourceFields
| SearchConfigurationWithExtractedReferenceType
| undefined;
const criteria = fields[ALERT_RULE_PARAMETERS]?.criteria as MetricExpression[];
const metrics: CustomThresholdExpressionMetric[] =
@ -158,11 +163,12 @@ export const registerObservabilityRuleTypes = async (
return {
reason: fields[ALERT_REASON] ?? '-',
link: getViewInAppUrl({
metrics,
startedAt: fields[ALERT_START],
logsExplorerLocator,
filter: (searchConfiguration?.query as { query: string }).query,
dataViewId,
groups,
logsExplorerLocator,
metrics,
searchConfiguration,
startedAt: fields[ALERT_START],
}),
hasBasePath: true,
};

View file

@ -1251,10 +1251,16 @@ describe('The custom threshold alert type', () => {
const ISO_DATE_REGEX = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
expect(getViewInAppUrl).toBeCalledWith({
dataViewId: 'c34a7c79-a88b-4b4a-ad19-72f6d24104e4',
filter: mockQuery,
logsExplorerLocator: undefined,
metrics: customThresholdCountCriterion.metrics,
startedAt: expect.stringMatching(ISO_DATE_REGEX),
searchConfiguration: {
index: {},
query: {
query: mockQuery,
language: 'kuery',
},
},
});
});
});

View file

@ -18,6 +18,7 @@ import { LocatorPublic } from '@kbn/share-plugin/common';
import { RecoveredActionGroup } from '@kbn/alerting-plugin/common';
import { IBasePath, Logger } from '@kbn/core/server';
import { LifecycleRuleExecutor } from '@kbn/rule-registry-plugin/server';
import { Group } from '../../../../common/custom_threshold_rule/types';
import { getEvaluationValues, getThreshold } from './lib/get_values';
import { AlertsLocatorParams, getAlertDetailsUrl } from '../../../../common';
import { getViewInAppUrl } from '../../../../common/custom_threshold_rule/get_view_in_app_url';
@ -177,16 +178,16 @@ export const createCustomThresholdExecutor = ({
}
const groupByKeysObjectMapping = getFormattedGroupBy(params.groupBy, resultGroupSet);
const groups = [...resultGroupSet];
const groupArray = [...resultGroupSet];
const nextMissingGroups = new Set<MissingGroupsRecord>();
const hasGroups = !isEqual(groups, [UNGROUPED_FACTORY_KEY]);
const hasGroups = !isEqual(groupArray, [UNGROUPED_FACTORY_KEY]);
let scheduledActionsCount = 0;
const alertLimit = baseAlertFactory.alertLimit.getValue();
let hasReachedLimit = false;
// The key of `groups` is the alert instance ID.
for (const group of groups) {
// The key of `groupArray` is the alert instance ID.
for (const group of groupArray) {
if (scheduledActionsCount >= alertLimit) {
// need to set this so that warning is displayed in the UI and in the logs
hasReachedLimit = true;
@ -264,6 +265,7 @@ export const createCustomThresholdExecutor = ({
new Set([...(additionalContext.tags ?? []), ...options.rule.tags])
);
const groups: Group[] = groupByKeysObjectMapping[group];
const alert = alertFactory(
`${group}`,
reason,
@ -271,7 +273,7 @@ export const createCustomThresholdExecutor = ({
additionalContext,
evaluationValues,
threshold,
groupByKeysObjectMapping[group]
groups
);
const alertUuid = getAlertUuid(group);
const indexedStartedAt = getAlertStartedDate(group) ?? startedAt.toISOString();
@ -291,9 +293,10 @@ export const createCustomThresholdExecutor = ({
}),
viewInAppUrl: getViewInAppUrl({
dataViewId: params.searchConfiguration?.index?.title ?? dataViewId,
filter: params.searchConfiguration.query.query,
groups,
logsExplorerLocator,
metrics: alertResults.length === 1 ? alertResults[0][group].metrics : [],
searchConfiguration: params.searchConfiguration,
startedAt: indexedStartedAt,
}),
...additionalContext,
@ -325,10 +328,11 @@ export const createCustomThresholdExecutor = ({
group,
timestamp: startedAt.toISOString(),
viewInAppUrl: getViewInAppUrl({
dataViewId: params.searchConfiguration?.index?.title ?? dataViewId,
filter: params.searchConfiguration.query.query,
dataViewId,
groups: group,
logsExplorerLocator,
metrics: params.criteria[0]?.metrics,
searchConfiguration: params.searchConfiguration,
startedAt: indexedStartedAt,
}),
...additionalContext,

View file

@ -9,8 +9,10 @@ import { ElasticsearchClient } from '@kbn/core/server';
import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
import type { Logger } from '@kbn/logging';
import { isString, get, identity } from 'lodash';
import { SearchConfigurationType } from '../types';
import { CustomMetricExpressionParams } from '../../../../../common/custom_threshold_rule/types';
import {
CustomMetricExpressionParams,
SearchConfigurationType,
} from '../../../../../common/custom_threshold_rule/types';
import type { BucketKey } from './get_data';
import { calculateCurrentTimeFrame, createBoolQuery } from './metric_query';

View file

@ -12,9 +12,9 @@ import { getIntervalInSeconds } from '../../../../../common/utils/get_interval_i
import {
Aggregators,
CustomMetricExpressionParams,
SearchConfigurationType,
} from '../../../../../common/custom_threshold_rule/types';
import { AdditionalContext } from '../utils';
import { SearchConfigurationType } from '../types';
import { createTimerange } from './create_timerange';
import { getData } from './get_data';
import { checkMissingGroups, MissingGroupsRecord } from './check_missing_group';

View file

@ -9,8 +9,10 @@ import { SearchResponse, AggregationsAggregate } from '@elastic/elasticsearch/li
import { ElasticsearchClient } from '@kbn/core/server';
import type { Logger } from '@kbn/logging';
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
import { SearchConfigurationType } from '../types';
import { CustomMetricExpressionParams } from '../../../../../common/custom_threshold_rule/types';
import {
CustomMetricExpressionParams,
SearchConfigurationType,
} from '../../../../../common/custom_threshold_rule/types';
import { UNGROUPED_FACTORY_KEY } from '../constants';
import { CONTAINER_ID, AdditionalContext, doFieldsExist, KUBERNETES_POD_UID } from '../utils';

View file

@ -10,8 +10,8 @@ import {
Comparator,
Aggregators,
CustomMetricExpressionParams,
SearchConfigurationType,
} from '../../../../../common/custom_threshold_rule/types';
import { SearchConfigurationType } from '../types';
import { getElasticsearchMetricQuery } from './metric_query';
describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
@ -30,10 +30,12 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => {
};
const searchConfiguration: SearchConfigurationType = {
index: {
id: 'dataset-logs-*-*',
name: 'All logs',
timeFieldName: '@timestamp',
title: 'logs-*-*',
index: {
id: 'dataset-logs-*-*',
name: 'All logs',
timeFieldName: '@timestamp',
title: 'logs-*-*',
},
},
query: {
language: 'kuery',

View file

@ -11,9 +11,9 @@ import { Filter } from '@kbn/es-query';
import {
Aggregators,
CustomMetricExpressionParams,
SearchConfigurationType,
} from '../../../../../common/custom_threshold_rule/types';
import { getSearchConfigurationBoolQuery } from '../../../../utils/get_parsed_filtered_query';
import { SearchConfigurationType } from '../types';
import { createCustomMetricsAggregations } from './create_custom_metrics_aggregations';
import {
CONTAINER_ID,

View file

@ -13,13 +13,14 @@ import {
RuleTypeState,
} from '@kbn/alerting-plugin/common';
import { Alert } from '@kbn/alerting-plugin/server';
import { TypeOf } from '@kbn/config-schema';
import { DataViewSpec } from '@kbn/data-views-plugin/common';
import { CustomMetricExpressionParams } from '../../../../common/custom_threshold_rule/types';
import {
CustomMetricExpressionParams,
Group,
SearchConfigurationWithExtractedReferenceType,
} from '../../../../common/custom_threshold_rule/types';
import { FIRED_ACTIONS_ID, NO_DATA_ACTIONS_ID, FIRED_ACTION, NO_DATA_ACTION } from './constants';
import { MissingGroupsRecord } from './lib/check_missing_group';
import { AdditionalContext } from './utils';
import { searchConfigurationSchema } from './register_custom_threshold_rule_type';
export enum AlertStates {
OK,
@ -29,13 +30,11 @@ export enum AlertStates {
}
// Executor types
export type SearchConfigurationType = TypeOf<typeof searchConfigurationSchema>;
export type RuleTypeParams = Record<string, unknown>;
export interface CustomThresholdRuleTypeParams extends RuleTypeParams {
criteria: CustomMetricExpressionParams[];
// Index will be a data view spec after extracting references
searchConfiguration: Omit<SearchConfigurationType, 'index'> & { index: DataViewSpec };
searchConfiguration: SearchConfigurationWithExtractedReferenceType;
groupBy?: string | string[];
alertOnNoData: boolean;
alertOnGroupDisappear?: boolean;
@ -45,7 +44,7 @@ export type CustomThresholdRuleTypeState = RuleTypeState & {
lastRunTimestamp?: number;
missingGroups?: Array<string | MissingGroupsRecord>;
groupBy?: string | string[];
searchConfiguration?: Omit<SearchConfigurationType, 'index'> & { index: DataViewSpec };
searchConfiguration?: SearchConfigurationWithExtractedReferenceType;
};
export type CustomThresholdAlertState = AlertState; // no specific instance state used
export type CustomThresholdAlertContext = AlertContext & {
@ -64,11 +63,6 @@ export type CustomThresholdActionGroup =
| typeof NO_DATA_ACTIONS_ID
| typeof RecoveredActionGroup.id;
export type Group = Array<{
field: string;
value: string;
}>;
export type CustomThresholdAlertFactory = (
id: string,
reason: string,
@ -76,7 +70,7 @@ export type CustomThresholdAlertFactory = (
additionalContext?: AdditionalContext | null,
evaluationValues?: Array<number | null>,
threshold?: Array<number | null>,
group?: Group
group?: Group[]
) => CustomThresholdAlert;
type CustomThresholdAlert = Alert<

View file

@ -16,8 +16,9 @@ import { ES_FIELD_TYPES } from '@kbn/field-types';
import { set } from '@kbn/safer-lodash-set';
import { ParsedExperimentalFields } from '@kbn/rule-registry-plugin/common/parse_experimental_fields';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import type { Group } from '../../../../common/custom_threshold_rule/types';
import { ObservabilityConfig } from '../../..';
import { AlertExecutionDetails, Group } from './types';
import { AlertExecutionDetails } from './types';
const ALERT_CONTEXT_CONTAINER = 'container';
const ALERT_CONTEXT_ORCHESTRATOR = 'orchestrator';
@ -228,13 +229,13 @@ export const flattenObject = (obj: AdditionalContext, prefix: string = ''): Addi
export const getFormattedGroupBy = (
groupBy: string | string[] | undefined,
groupSet: Set<string>
): Record<string, Group> => {
const groupByKeysObjectMapping: Record<string, Group> = {};
): Record<string, Group[]> => {
const groupByKeysObjectMapping: Record<string, Group[]> = {};
if (groupBy) {
groupSet.forEach((group) => {
const groupSetKeys = group.split(',');
groupByKeysObjectMapping[group] = Array.isArray(groupBy)
? groupBy.reduce((result: Group, groupByItem, index) => {
? groupBy.reduce((result: Group[], groupByItem, index) => {
result.push({ field: groupByItem, value: groupSetKeys[index]?.trim() });
return result;
}, [])

View file

@ -12,7 +12,7 @@ import {
fromKueryExpression,
toElasticsearchQuery,
} from '@kbn/es-query';
import { SearchConfigurationType } from '../lib/rules/custom_threshold/types';
import { SearchConfigurationType } from '../../common/custom_threshold_rule/types';
export const getParsedFilterQuery: (filter: string | undefined) => Array<Record<string, any>> = (
filter

View file

@ -252,6 +252,7 @@ export default function ({ getService }: FtrProviderContext) {
dataset: DATA_VIEW_TITLE,
timeRange: { to: 'now' },
query: { query: '', language: 'kuery' },
filters: [],
});
expect(parsedViewInAppUrl.params.timeRange.from).match(ISO_DATE_REGEX);
});

View file

@ -220,6 +220,7 @@ export default function ({ getService }: FtrProviderContext) {
dataset: DATA_VIEW_ID,
timeRange: { to: 'now' },
query: { query: '', language: 'kuery' },
filters: [],
});
expect(parsedViewInAppUrl.params.timeRange.from).match(ISO_DATE_REGEX);
});

View file

@ -252,6 +252,7 @@ export default function ({ getService }: FtrProviderContext) {
dataset: DATA_VIEW_ID,
timeRange: { to: 'now' },
query: { query: 'host.name:* and container.id:*', language: 'kuery' },
filters: [],
});
expect(parsedViewInAppUrl.params.timeRange.from).match(ISO_DATE_REGEX);
});

View file

@ -247,6 +247,7 @@ export default function ({ getService }: FtrProviderContext) {
dataset: DATE_VIEW_TITLE,
timeRange: { to: 'now' },
query: { query: '', language: 'kuery' },
filters: [],
});
expect(parsedViewInAppUrl.params.timeRange.from).match(ISO_DATE_REGEX);
});