[RAC][Observability] Use flattened type for rule params in Observability (#120758)

* add kibana.alert.rule.parameters as a flattened type

* temp

* rule_data_formatter

* fix bug in search strategy with flattend field type where prefix was wrong (kibana.alert.rule.parameters was ignored)

* fix inventory rule data formatters

* remove console log

* hack that prepends kibana.alerts.rule.parameters in the nested subfields

* import ALERT_RULE_PARAMETERS from kbn rule data utils

* remove console log

* format custom metric link

* remove ALERT_PARAMS from technical field names

* fix bug in timelines plugin to use dotField instead of prependField & fix failing tests

* remove console log and unused variable

* delete kibana.alert.rule.params from the mapping

* flatten kibana.alert.rule.parameters and add some unit tests

* fix rule_data_formatter

* handle scenario of having multiple items in an array (multiple conditions setup in the rule)

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
mgiota 2021-12-16 21:29:06 +01:00 committed by GitHub
parent ecf2265d56
commit cdd66ea0eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 149 additions and 35 deletions

View file

@ -53,7 +53,6 @@ const ALERT_RULE_LICENSE = `${ALERT_RULE_NAMESPACE}.license` as const;
const ALERT_RULE_CATEGORY = `${ALERT_RULE_NAMESPACE}.category` as const; const ALERT_RULE_CATEGORY = `${ALERT_RULE_NAMESPACE}.category` as const;
const ALERT_RULE_NAME = `${ALERT_RULE_NAMESPACE}.name` as const; const ALERT_RULE_NAME = `${ALERT_RULE_NAMESPACE}.name` as const;
const ALERT_RULE_NOTE = `${ALERT_RULE_NAMESPACE}.note` as const; const ALERT_RULE_NOTE = `${ALERT_RULE_NAMESPACE}.note` as const;
const ALERT_RULE_PARAMS = `${ALERT_RULE_NAMESPACE}.params` as const;
const ALERT_RULE_PARAMETERS = `${ALERT_RULE_NAMESPACE}.parameters` as const; const ALERT_RULE_PARAMETERS = `${ALERT_RULE_NAMESPACE}.parameters` as const;
const ALERT_RULE_REFERENCES = `${ALERT_RULE_NAMESPACE}.references` as const; const ALERT_RULE_REFERENCES = `${ALERT_RULE_NAMESPACE}.references` as const;
const ALERT_RULE_RISK_SCORE = `${ALERT_RULE_NAMESPACE}.risk_score` as const; const ALERT_RULE_RISK_SCORE = `${ALERT_RULE_NAMESPACE}.risk_score` as const;
@ -113,7 +112,6 @@ const fields = {
ALERT_RULE_LICENSE, ALERT_RULE_LICENSE,
ALERT_RULE_NAME, ALERT_RULE_NAME,
ALERT_RULE_NOTE, ALERT_RULE_NOTE,
ALERT_RULE_PARAMS,
ALERT_RULE_PARAMETERS, ALERT_RULE_PARAMETERS,
ALERT_RULE_REFERENCES, ALERT_RULE_REFERENCES,
ALERT_RULE_RISK_SCORE, ALERT_RULE_RISK_SCORE,
@ -171,7 +169,6 @@ export {
ALERT_RULE_LICENSE, ALERT_RULE_LICENSE,
ALERT_RULE_NAME, ALERT_RULE_NAME,
ALERT_RULE_NOTE, ALERT_RULE_NOTE,
ALERT_RULE_PARAMS,
ALERT_RULE_PARAMETERS, ALERT_RULE_PARAMETERS,
ALERT_RULE_REFERENCES, ALERT_RULE_REFERENCES,
ALERT_RULE_RISK_SCORE, ALERT_RULE_RISK_SCORE,

View file

@ -72,9 +72,7 @@ export const eventHit = {
], ],
astring: 'cool', astring: 'cool',
aNumber: 1, aNumber: 1,
anObject: { neat: true,
neat: true,
},
}, },
], ],
}, },

View file

@ -7,35 +7,45 @@
import { import {
ALERT_REASON, ALERT_REASON,
ALERT_RULE_PARAMS, ALERT_RULE_PARAMETERS,
TIMESTAMP, TIMESTAMP,
} from '@kbn/rule-data-utils/technical_field_names'; } from '@kbn/rule-data-utils/technical_field_names';
import { encode } from 'rison-node'; import { encode } from 'rison-node';
import { stringify } from 'query-string'; import { stringify } from 'query-string';
import { ObservabilityRuleTypeFormatter } from '../../../../observability/public'; import { ObservabilityRuleTypeFormatter } from '../../../../observability/public';
import { InventoryMetricThresholdParams } from '../../../common/alerting/metrics';
export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => { export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => {
const reason = fields[ALERT_REASON] ?? '-'; const reason = fields[ALERT_REASON] ?? '-';
const ruleParams = parseRuleParams(fields[ALERT_RULE_PARAMS]); const nodeTypeField = `${ALERT_RULE_PARAMETERS}.nodeType`;
const nodeType = fields[nodeTypeField];
let link = '/app/metrics/link-to/inventory?'; let link = '/app/metrics/link-to/inventory?';
if (ruleParams) { if (nodeType) {
const linkToParams: Record<string, any> = { const linkToParams: Record<string, any> = {
nodeType: ruleParams.nodeType, nodeType: fields[nodeTypeField][0],
timestamp: Date.parse(fields[TIMESTAMP]), timestamp: Date.parse(fields[TIMESTAMP]),
customMetric: '', customMetric: '',
}; };
// We always pick the first criteria metric for the URL // We always pick the first criteria metric for the URL
const criteria = ruleParams.criteria[0]; const criteriaMetric = fields[`${ALERT_RULE_PARAMETERS}.criteria.metric`][0];
if (criteria.customMetric && criteria.customMetric.id !== 'alert-custom-metric') { const criteriaCustomMetricId = fields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.id`][0];
const customMetric = encode(criteria.customMetric); if (criteriaCustomMetricId !== 'alert-custom-metric') {
const criteriaCustomMetricAggregation =
fields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.aggregation`][0];
const criteriaCustomMetricField =
fields[`${ALERT_RULE_PARAMETERS}.criteria.customMetric.field`][0];
const customMetric = encode({
id: criteriaCustomMetricId,
type: 'custom',
field: criteriaCustomMetricField,
aggregation: criteriaCustomMetricAggregation,
});
linkToParams.customMetric = customMetric; linkToParams.customMetric = customMetric;
linkToParams.metric = customMetric; linkToParams.metric = customMetric;
} else { } else {
linkToParams.metric = encode({ type: criteria.metric }); linkToParams.metric = encode({ type: criteriaMetric });
} }
link += stringify(linkToParams); link += stringify(linkToParams);
@ -46,11 +56,3 @@ export const formatReason: ObservabilityRuleTypeFormatter = ({ fields }) => {
link, link,
}; };
}; };
function parseRuleParams(params?: string): InventoryMetricThresholdParams | undefined {
try {
return typeof params === 'string' ? JSON.parse(params) : undefined;
} catch (_) {
return;
}
}

View file

@ -6,7 +6,7 @@
*/ */
import { i18n } from '@kbn/i18n'; import { i18n } from '@kbn/i18n';
import { ALERT_REASON, ALERT_RULE_PARAMS } from '@kbn/rule-data-utils'; import { ALERT_REASON, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils';
import moment from 'moment'; import moment from 'moment';
import { first, get, last } from 'lodash'; import { first, get, last } from 'lodash';
import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label'; import { getCustomMetricLabel } from '../../../../common/formatters/get_custom_metric_label';
@ -74,7 +74,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) =
id, id,
fields: { fields: {
[ALERT_REASON]: reason, [ALERT_REASON]: reason,
[ALERT_RULE_PARAMS]: JSON.stringify(params), [ALERT_RULE_PARAMETERS]: params as any, // the type assumes the object is already flattened when writing the same way as when reading https://github.com/elastic/kibana/blob/main/x-pack/plugins/rule_registry/common/field_map/runtime_type_from_fieldmap.ts#L60
}, },
}); });

View file

@ -16,7 +16,6 @@ export const technicalRuleFieldMap = {
Fields.EVENT_ACTION, Fields.EVENT_ACTION,
Fields.TAGS Fields.TAGS
), ),
[Fields.ALERT_RULE_PARAMS]: { type: 'keyword', index: false },
[Fields.ALERT_RULE_PARAMETERS]: { type: 'flattened', ignore_above: 4096 }, [Fields.ALERT_RULE_PARAMETERS]: { type: 'flattened', ignore_above: 4096 },
[Fields.ALERT_RULE_TYPE_ID]: { type: 'keyword', required: true }, [Fields.ALERT_RULE_TYPE_ID]: { type: 'keyword', required: true },
[Fields.ALERT_RULE_CONSUMER]: { type: 'keyword', required: true }, [Fields.ALERT_RULE_CONSUMER]: { type: 'keyword', required: true },

View file

@ -131,7 +131,115 @@ describe('Events Details Helpers', () => {
const result = getDataFromFieldsHits(whackFields); const result = getDataFromFieldsHits(whackFields);
expect(result).toEqual(whackResultFields); expect(result).toEqual(whackResultFields);
}); });
it('flattens alert parameters', () => {
const ruleParameterFields = {
'kibana.alert.rule.parameters': [
{
nodeType: 'host',
criteria: [
{
metric: 'cpu',
comparator: '>',
threshold: [3],
timeSize: 1,
timeUnit: 'm',
customMetric: {
type: 'custom',
id: 'alert-custom-metric',
field: '',
aggregation: 'avg',
},
},
],
sourceId: 'default',
},
],
};
const ruleParametersResultFields = [
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.nodeType',
values: ['host'],
originalValue: ['host'],
isObjectArray: false,
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.criteria.metric',
isObjectArray: false,
originalValue: ['cpu'],
values: ['cpu'],
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.criteria.comparator',
values: ['>'],
originalValue: ['>'],
isObjectArray: false,
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.criteria.threshold',
isObjectArray: false,
originalValue: ['3'],
values: ['3'],
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.criteria.timeSize',
isObjectArray: false,
originalValue: ['1'],
values: ['1'],
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.criteria.timeUnit',
values: ['m'],
originalValue: ['m'],
isObjectArray: false,
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.criteria.customMetric.type',
isObjectArray: false,
originalValue: ['custom'],
values: ['custom'],
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.criteria.customMetric.id',
isObjectArray: false,
originalValue: ['alert-custom-metric'],
values: ['alert-custom-metric'],
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.criteria.customMetric.field',
isObjectArray: false,
originalValue: [''],
values: [''],
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.criteria.customMetric.aggregation',
isObjectArray: false,
originalValue: ['avg'],
values: ['avg'],
},
{
category: 'kibana',
field: 'kibana.alert.rule.parameters.sourceId',
isObjectArray: false,
originalValue: ['default'],
values: ['default'],
},
];
const result = getDataFromFieldsHits(ruleParameterFields);
expect(result).toEqual(ruleParametersResultFields);
});
}); });
it('#getDataFromSourceHits', () => { it('#getDataFromSourceHits', () => {
const _source: EventSource = { const _source: EventSource = {
'@timestamp': '2021-02-24T00:41:06.527Z', '@timestamp': '2021-02-24T00:41:06.527Z',

View file

@ -7,9 +7,9 @@
import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp'; import { get, isEmpty, isNumber, isObject, isString } from 'lodash/fp';
import { ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils/technical_field_names';
import { EventHit, EventSource, TimelineEventsDetailsItem } from '../search_strategy'; import { EventHit, EventSource, TimelineEventsDetailsItem } from '../search_strategy';
import { toObjectArrayOfStrings, toStringArray } from './to_array'; import { toObjectArrayOfStrings, toStringArray } from './to_array';
export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags']; export const baseCategoryFields = ['@timestamp', 'labels', 'message', 'tags'];
export const getFieldCategory = (field: string): string => { export const getFieldCategory = (field: string): string => {
@ -38,6 +38,9 @@ export const formatGeoLocation = (item: unknown[]) => {
export const isGeoField = (field: string) => export const isGeoField = (field: string) =>
field.includes('geo.location') || field.includes('geoip.location'); field.includes('geo.location') || field.includes('geoip.location');
export const isRuleParametersFieldOrSubfield = (field: string, prependField?: string) =>
prependField?.includes(ALERT_RULE_PARAMETERS) || field === ALERT_RULE_PARAMETERS;
export const getDataFromSourceHits = ( export const getDataFromSourceHits = (
sources: EventSource, sources: EventSource,
category?: string, category?: string,
@ -79,7 +82,6 @@ export const getDataFromFieldsHits = (
): TimelineEventsDetailsItem[] => ): TimelineEventsDetailsItem[] =>
Object.keys(fields).reduce<TimelineEventsDetailsItem[]>((accumulator, field) => { Object.keys(fields).reduce<TimelineEventsDetailsItem[]>((accumulator, field) => {
const item: unknown[] = fields[field]; const item: unknown[] = fields[field];
const fieldCategory = const fieldCategory =
prependFieldCategory != null ? prependFieldCategory : getFieldCategory(field); prependFieldCategory != null ? prependFieldCategory : getFieldCategory(field);
if (isGeoField(field)) { if (isGeoField(field)) {
@ -112,13 +114,21 @@ export const getDataFromFieldsHits = (
}, },
]; ];
} }
// format nested fields // format nested fields
const nestedFields = Array.isArray(item) let nestedFields;
? item if (isRuleParametersFieldOrSubfield(field, prependField)) {
.reduce((acc, i) => [...acc, getDataFromFieldsHits(i, dotField, fieldCategory)], []) nestedFields = Array.isArray(item)
.flat() ? item
: getDataFromFieldsHits(item, prependField, fieldCategory); .reduce((acc, i) => [...acc, getDataFromFieldsHits(i, dotField, fieldCategory)], [])
.flat()
: getDataFromFieldsHits(item, dotField, fieldCategory);
} else {
nestedFields = Array.isArray(item)
? item
.reduce((acc, i) => [...acc, getDataFromFieldsHits(i, dotField, fieldCategory)], [])
.flat()
: getDataFromFieldsHits(item, prependField, fieldCategory);
}
// combine duplicate fields // combine duplicate fields
const flat: Record<string, TimelineEventsDetailsItem> = [ const flat: Record<string, TimelineEventsDetailsItem> = [