Skip to content

Commit

Permalink
[ResponseOps] Move custom threshold rule params to the package (elast…
Browse files Browse the repository at this point in the history
…ic#208686)

Fixes: elastic#195191

Move log threshold rule type params to the new package.

P.S.: I've moved function `validateKQLStringFilter` and test for it in
my previous PR: elastic#205507

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
  • Loading branch information
3 people authored Feb 4, 2025
1 parent 3d7ccc5 commit 07557b6
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 126 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export { customThresholdParamsSchema } from './latest';
export { customThresholdParamsSchema as customThresholdParamsSchemaV1 } from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

export * from './v1';
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import { schema } from '@kbn/config-schema';
import { COMPARATORS } from '@kbn/alerting-comparators';
import { dataViewSpecSchema } from '../common';
import { oneOfLiterals, validateKQLStringFilter, LEGACY_COMPARATORS } from '../common/utils';

const allowedAggregators = [
'avg',
'sum',
'min',
'max',
'cardinality',
'rate',
'p95',
'p99',
'last_value',
];

const comparators = Object.values({ ...COMPARATORS, ...LEGACY_COMPARATORS });

const searchConfigSchema = schema.object({
index: schema.oneOf([schema.string(), dataViewSpecSchema]),
query: schema.object({
language: schema.string(),
query: schema.string({
validate: validateKQLStringFilter,
}),
}),
filter: schema.maybe(
schema.arrayOf(
schema.object({
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
meta: schema.recordOf(schema.string(), schema.any()),
})
)
),
});

const customCriterion = schema.object({
threshold: schema.arrayOf(schema.number()),
comparator: oneOfLiterals(comparators),
timeUnit: schema.string(),
timeSize: schema.number(),
aggType: schema.maybe(schema.literal('custom')),
metric: schema.never(),
metrics: schema.arrayOf(
schema.oneOf([
schema.object({
name: schema.string(),
aggType: oneOfLiterals(allowedAggregators),
field: schema.string(),
filter: schema.never(),
}),
schema.object({
name: schema.string(),
aggType: schema.literal('count'),
filter: schema.maybe(
schema.string({
validate: validateKQLStringFilter,
})
),
field: schema.never(),
}),
])
),
equation: schema.maybe(schema.string()),
label: schema.maybe(schema.string()),
});

export const customThresholdParamsSchema = schema.object(
{
criteria: schema.arrayOf(customCriterion),
groupBy: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
alertOnNoData: schema.maybe(schema.boolean()),
alertOnGroupDisappear: schema.maybe(schema.boolean()),
searchConfiguration: searchConfigSchema,
},
{ unknowns: 'allow' }
);
Original file line number Diff line number Diff line change
Expand Up @@ -31100,7 +31100,6 @@
"xpack.observability.customThreshold.rule.reason.forTheLast": "durée : {duration}",
"xpack.observability.customThreshold.rule.reason.group": "groupe : {group}",
"xpack.observability.customThreshold.rule.reasonActionVariableDescription": "Une description concise de la raison du signalement",
"xpack.observability.customThreshold.rule.schema.invalidFilterQuery": "filterQuery doit être un filtre KQL valide (erreur : {errorMessage})",
"xpack.observability.customThreshold.rule.sourceConfiguration.missingHttp": "Échec de chargement de la source : Aucun client HTTP disponible.",
"xpack.observability.customThreshold.rule.sourceConfiguration.updateFailureBody": "Nous n'avons pas pu appliquer les modifications à la configuration des indicateurs. Réessayez plus tard.",
"xpack.observability.customThreshold.rule.sourceConfiguration.updateFailureTitle": "La mise à jour de la configuration a échoué",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30964,7 +30964,6 @@
"xpack.observability.customThreshold.rule.reason.forTheLast": "duration: {duration}",
"xpack.observability.customThreshold.rule.reason.group": "グループ:{group}",
"xpack.observability.customThreshold.rule.reasonActionVariableDescription": "アラートの理由の簡潔な説明",
"xpack.observability.customThreshold.rule.schema.invalidFilterQuery": "filterQueryは有効なKQLフィルターでなければなりません(エラー:{errorMessage})",
"xpack.observability.customThreshold.rule.sourceConfiguration.missingHttp": "ソースの読み込みに失敗しました:HTTPクライアントがありません。",
"xpack.observability.customThreshold.rule.sourceConfiguration.updateFailureBody": "変更をメトリック構成に適用できませんでした。しばらくたってから再試行してください。",
"xpack.observability.customThreshold.rule.sourceConfiguration.updateFailureTitle": "構成の更新が失敗しました",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30511,7 +30511,6 @@
"xpack.observability.customThreshold.rule.reason.forTheLast": "持续时间:{duration}",
"xpack.observability.customThreshold.rule.reason.group": "组:{group}",
"xpack.observability.customThreshold.rule.reasonActionVariableDescription": "告警原因的简洁描述",
"xpack.observability.customThreshold.rule.schema.invalidFilterQuery": "filterQuery 必须是有效的 KQL 筛选(错误:{errorMessage})",
"xpack.observability.customThreshold.rule.sourceConfiguration.missingHttp": "无法加载源:无 HTTP 客户端可用。",
"xpack.observability.customThreshold.rule.sourceConfiguration.updateFailureBody": "无法对指标配置应用更改。请稍后重试。",
"xpack.observability.customThreshold.rule.sourceConfiguration.updateFailureTitle": "配置更新失败",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,17 @@
*/

import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';
import { extractReferences, injectReferences } from '@kbn/data-plugin/common';
import { dataViewSpecSchema } from '@kbn/data-views-plugin/server/rest_api_routes/schema';
import { i18n } from '@kbn/i18n';
import { IRuleTypeAlerts, GetViewInAppRelativeUrlFnOpts } from '@kbn/alerting-plugin/server';
import { IBasePath, Logger } from '@kbn/core/server';
import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils';
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
import { LicenseType } from '@kbn/licensing-plugin/server';
import { COMPARATORS } from '@kbn/alerting-comparators';
import { EsQueryRuleParamsExtractedParams } from '@kbn/stack-alerts-plugin/server/rule_types/es_query/rule_type_params';
import { LEGACY_COMPARATORS } from '../../../../common/utils/convert_legacy_outside_comparator';
import { customThresholdParamsSchema } from '@kbn/response-ops-rule-params/custom_threshold';
import { observabilityFeatureId, observabilityPaths } from '../../../../common';
import { Aggregators } from '../../../../common/custom_threshold_rule/types';
import { THRESHOLD_RULE_REGISTRATION_CONTEXT } from '../../../common/constants';

import {
alertDetailUrlActionVariableDescription,
cloudActionVariableDescription,
Expand All @@ -36,7 +31,6 @@ import {
valueActionVariableDescription,
viewInAppUrlActionVariableDescription,
} from './translations';
import { oneOfLiterals, validateKQLStringFilter } from './utils';
import {
createCustomThresholdExecutor,
CustomThresholdLocators,
Expand All @@ -53,92 +47,25 @@ export const MetricsRulesTypeAlertDefinition: IRuleTypeAlerts<CustomThresholdAle
shouldWrite: true,
};

export const searchConfigurationSchema = schema.object({
index: schema.oneOf([schema.string(), dataViewSpecSchema]),
query: schema.object({
language: schema.string(),
query: schema.string({
validate: validateKQLStringFilter,
}),
}),
filter: schema.maybe(
schema.arrayOf(
schema.object({
query: schema.maybe(schema.recordOf(schema.string(), schema.any())),
meta: schema.recordOf(schema.string(), schema.any()),
})
)
),
});

export function thresholdRuleType(
basePath: IBasePath,
config: ObservabilityConfig,
logger: Logger,
locators: CustomThresholdLocators
) {
const comparators = Object.values({ ...COMPARATORS, ...LEGACY_COMPARATORS });
const baseCriterion = {
threshold: schema.arrayOf(schema.number()),
comparator: oneOfLiterals(comparators),
timeUnit: schema.string(),
timeSize: schema.number(),
};
const allowedAggregators = Object.values(Aggregators);
allowedAggregators.splice(Object.values(Aggregators).indexOf(Aggregators.COUNT), 1);

const customCriterion = schema.object({
...baseCriterion,
aggType: schema.maybe(schema.literal('custom')),
metric: schema.never(),
metrics: schema.arrayOf(
schema.oneOf([
schema.object({
name: schema.string(),
aggType: oneOfLiterals(allowedAggregators),
field: schema.string(),
filter: schema.never(),
}),
schema.object({
name: schema.string(),
aggType: schema.literal('count'),
filter: schema.maybe(
schema.string({
validate: validateKQLStringFilter,
})
),
field: schema.never(),
}),
])
),
equation: schema.maybe(schema.string()),
label: schema.maybe(schema.string()),
});

const paramsSchema = schema.object(
{
criteria: schema.arrayOf(customCriterion),
groupBy: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
alertOnNoData: schema.maybe(schema.boolean()),
alertOnGroupDisappear: schema.maybe(schema.boolean()),
searchConfiguration: searchConfigurationSchema,
},
{ unknowns: 'allow' }
);

return {
id: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
name: i18n.translate('xpack.observability.threshold.ruleName', {
defaultMessage: 'Custom threshold',
}),
fieldsForAAD: CUSTOM_THRESHOLD_AAD_FIELDS,
validate: {
params: paramsSchema,
params: customThresholdParamsSchema,
},
schemas: {
params: {
type: 'config-schema' as const,
schema: paramsSchema,
schema: customThresholdParamsSchema,
},
},
defaultActionGroupId: FIRED_ACTION.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { flattenObject, getFormattedGroupBy, validateKQLStringFilter } from './utils';
import { flattenObject, getFormattedGroupBy } from './utils';

describe('FlattenObject', () => {
it('flattens multi level item', () => {
Expand Down Expand Up @@ -57,29 +57,6 @@ describe('FlattenObject', () => {
});
});

describe('validateKQLStringFilter', () => {
const data = [
// input, output
['', undefined],
['host.name:host-0', undefined],
];
const dataWithError = [
// input, output
[
':*',
'filterQuery must be a valid KQL filter (error: Expected "(", NOT, end of input, field name, value, whitespace but ":" found.',
],
];

test.each(data)('validateKQLStringFilter(%s): %o', (input: any, output: any) => {
expect(validateKQLStringFilter(input)).toEqual(output);
});

test.each(dataWithError)('validateKQLStringFilter(%s): %o', (input: any, output: any) => {
expect(validateKQLStringFilter(input)).toContain(output);
});
});

describe('getFormattedGroupBy', () => {
it('should format groupBy correctly for empty input', () => {
expect(getFormattedGroupBy(undefined, new Set<string>())).toEqual({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
*/

import { isError } from 'lodash';
import { buildEsQuery as kbnBuildEsQuery } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { schema } from '@kbn/config-schema';
import { Logger, LogMeta } from '@kbn/logging';
import type { ElasticsearchClient, IBasePath } from '@kbn/core/server';
Expand Down Expand Up @@ -45,26 +43,6 @@ export const oneOfLiterals = (arrayOfLiterals: Readonly<string[]>) =>
arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`,
});

export const validateKQLStringFilter = (value: string) => {
if (value === '') {
// Allow clearing the filter.
return;
}

try {
kbnBuildEsQuery(undefined, [{ query: value, language: 'kuery' }], [], {
allowLeadingWildcards: true,
queryStringOptions: {},
ignoreFilterIfFieldNotInIndex: false,
});
} catch (e) {
return i18n.translate('xpack.observability.customThreshold.rule.schema.invalidFilterQuery', {
defaultMessage: 'filterQuery must be a valid KQL filter (error: {errorMessage})',
values: { errorMessage: e?.message },
});
}
};

export const createScopedLogger = (
logger: Logger,
scope: string,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@
"@kbn/response-ops-rule-form",
"@kbn/streams-plugin",
"@kbn/data-service",
"@kbn/ebt-tools"
"@kbn/ebt-tools",
"@kbn/response-ops-rule-params"
],
"exclude": ["target/**/*"]
}

0 comments on commit 07557b6

Please sign in to comment.