Skip to content

Commit

Permalink
[8.x] [ResponseOps][Rules] Move metric rule params schema to package (#…
Browse files Browse the repository at this point in the history
…205492) (#205912)

# Backport

This will backport the following commits from `main` to `8.x`:
- [[ResponseOps][Rules] Move metric rule params schema to package
(#205492)](#205492)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT
[{"author":{"name":"Antonio","email":"[email protected]"},"sourceCommit":{"committedDate":"2025-01-08T09:50:26Z","message":"[ResponseOps][Rules]
Move metric rule params schema to package (#205492)\n\nConnected with
#195189\r\n\r\n## Summary\r\n\r\n- Moved params of duration metric
inventory threshold rule type
to\r\n`/response-ops/rule_params/metric_inventory_threshold/`\r\n- Moved
params of metric threshold rule type
to\r\n`/response-ops/rule_params/metric_threshold/`\r\n\r\n**I did NOT
move the corresponding type to the rule_params package due\r\nto the
recursive imports it would
create.**\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"4873fa18d70142c5c2e6633d51648c45f6f481cb","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-minor"],"number":205492,"url":"https://github.com/elastic/kibana/pull/205492","mergeCommit":{"message":"[ResponseOps][Rules]
Move metric rule params schema to package (#205492)\n\nConnected with
#195189\r\n\r\n## Summary\r\n\r\n- Moved params of duration metric
inventory threshold rule type
to\r\n`/response-ops/rule_params/metric_inventory_threshold/`\r\n- Moved
params of metric threshold rule type
to\r\n`/response-ops/rule_params/metric_threshold/`\r\n\r\n**I did NOT
move the corresponding type to the rule_params package due\r\nto the
recursive imports it would
create.**\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"4873fa18d70142c5c2e6633d51648c45f6f481cb"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/205492","number":205492,"mergeCommit":{"message":"[ResponseOps][Rules]
Move metric rule params schema to package (#205492)\n\nConnected with
#195189\r\n\r\n## Summary\r\n\r\n- Moved params of duration metric
inventory threshold rule type
to\r\n`/response-ops/rule_params/metric_inventory_threshold/`\r\n- Moved
params of metric threshold rule type
to\r\n`/response-ops/rule_params/metric_threshold/`\r\n\r\n**I did NOT
move the corresponding type to the rule_params package due\r\nto the
recursive imports it would
create.**\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine
<[email protected]>","sha":"4873fa18d70142c5c2e6633d51648c45f6f481cb"}}]}]
BACKPORT-->
  • Loading branch information
adcoelho authored Jan 9, 2025
1 parent c00cf83 commit cfe68b0
Show file tree
Hide file tree
Showing 12 changed files with 293 additions and 151 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* 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 { isEmpty } from 'lodash';

export const oneOfLiterals = (arrayOfLiterals: Readonly<string[]>) =>
schema.string({
validate: (value) =>
arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`,
});

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

const errorMessage = 'filterQuery must be a valid Elasticsearch filter expressed in JSON';
try {
const parsedValue = JSON.parse(value);
if (!isEmpty(parsedValue.bool)) {
return undefined;
}
return errorMessage;
} catch (e) {
return errorMessage;
}
};

export type TimeUnitChar = 's' | 'm' | 'h' | 'd';

export enum LEGACY_COMPARATORS {
OUTSIDE_RANGE = 'outside',
}
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 { metricInventoryThresholdRuleParamsSchema } from './latest';
export { metricInventoryThresholdRuleParamsSchema as metricInventoryThresholdRuleParamsSchemaV1 } 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,91 @@
/*
* 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, Type } from '@kbn/config-schema';
import { COMPARATORS } from '@kbn/alerting-comparators';

import {
LEGACY_COMPARATORS,
TimeUnitChar,
oneOfLiterals,
validateIsStringElasticsearchJSONFilter,
} from '../common/utils';

const SNAPSHOT_CUSTOM_AGGREGATIONS = ['avg', 'max', 'min', 'rate'] as const;
type SnapshotCustomAggregation = (typeof SNAPSHOT_CUSTOM_AGGREGATIONS)[number];

const SnapshotMetricTypeKeysArray = [
'count',
'cpuV2',
'cpu',
'diskLatency',
'diskSpaceUsage',
'load',
'memory',
'memoryFree',
'memoryTotal',
'normalizedLoad1m',
'tx',
'rx',
'txV2',
'rxV2',
'logRate',
'diskIOReadBytes',
'diskIOWriteBytes',
's3TotalRequests',
's3NumberOfObjects',
's3BucketSize',
's3DownloadBytes',
's3UploadBytes',
'rdsConnections',
'rdsQueriesExecuted',
'rdsActiveTransactions',
'rdsLatency',
'sqsMessagesVisible',
'sqsMessagesDelayed',
'sqsMessagesSent',
'sqsMessagesEmpty',
'sqsOldestMessage',
'custom',
];
type SnapshotMetricTypeKeys = (typeof SNAPSHOT_CUSTOM_AGGREGATIONS)[number];

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

export const metricInventoryThresholdRuleParamsSchema = schema.object(
{
criteria: schema.arrayOf(
schema.object({
threshold: schema.arrayOf(schema.number()),
comparator: oneOfLiterals(comparators) as Type<COMPARATORS>,
timeUnit: schema.string() as Type<TimeUnitChar>,
timeSize: schema.number(),
metric: oneOfLiterals(SnapshotMetricTypeKeysArray) as Type<SnapshotMetricTypeKeys>,
warningThreshold: schema.maybe(schema.arrayOf(schema.number())),
warningComparator: schema.maybe(oneOfLiterals(comparators)),
customMetric: schema.maybe(
schema.object({
type: schema.literal('custom'),
id: schema.string(),
field: schema.string(),
aggregation: oneOfLiterals(
SNAPSHOT_CUSTOM_AGGREGATIONS
) as Type<SnapshotCustomAggregation>,
label: schema.maybe(schema.string()),
})
),
})
),
nodeType: schema.string(),
filterQuery: schema.maybe(schema.string({ validate: validateIsStringElasticsearchJSONFilter })),
sourceId: schema.string(),
alertOnNoData: schema.maybe(schema.boolean()),
},
{ unknowns: 'allow' }
);
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 { metricThresholdRuleParamsSchema } from './latest';
export { metricThresholdRuleParamsSchema as metricThresholdRuleParamsSchemaV1 } 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,99 @@
/*
* 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 {
LEGACY_COMPARATORS,
oneOfLiterals,
validateIsStringElasticsearchJSONFilter,
} from '../common/utils';

const METRIC_EXPLORER_AGGREGATIONS = [
'avg',
'max',
'min',
'cardinality',
'rate',
'count',
'sum',
'p95',
'p99',
'custom',
] as const;

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

const baseCriterion = {
threshold: schema.arrayOf(schema.number()),
comparator: oneOfLiterals(comparator),
timeUnit: schema.string(),
timeSize: schema.number(),
warningThreshold: schema.maybe(schema.arrayOf(schema.number())),
warningComparator: schema.maybe(oneOfLiterals(comparator)),
};

const nonCountCriterion = schema.object({
...baseCriterion,
metric: schema.string(),
aggType: oneOfLiterals(METRIC_EXPLORER_AGGREGATIONS),
customMetrics: schema.never(),
equation: schema.never(),
label: schema.never(),
});

const countCriterion = schema.object({
...baseCriterion,
aggType: schema.literal('count'),
metric: schema.never(),
customMetrics: schema.never(),
equation: schema.never(),
label: schema.never(),
});

const customCriterion = schema.object({
...baseCriterion,
aggType: schema.literal('custom'),
metric: schema.never(),
customMetrics: schema.arrayOf(
schema.oneOf([
schema.object({
name: schema.string(),
aggType: oneOfLiterals(['avg', 'sum', 'max', 'min', 'cardinality']),
field: schema.string(),
filter: schema.never(),
}),
schema.object({
name: schema.string(),
aggType: schema.literal('count'),
filter: schema.maybe(schema.string()),
field: schema.never(),
}),
])
),
equation: schema.maybe(schema.string()),
label: schema.maybe(schema.string()),
});

export const metricThresholdRuleParamsSchema = schema.object(
{
criteria: schema.arrayOf(schema.oneOf([countCriterion, nonCountCriterion, customCriterion])),
groupBy: schema.maybe(schema.oneOf([schema.string(), schema.arrayOf(schema.string())])),
filterQuery: schema.maybe(
schema.string({
validate: validateIsStringElasticsearchJSONFilter,
})
),
sourceId: schema.string(),
alertOnNoData: schema.maybe(schema.boolean()),
alertOnGroupDisappear: schema.maybe(schema.boolean()),
},
{ unknowns: 'allow' }
);
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@
},
"include": ["**/*.ts"],
"exclude": ["target/**/*"],
"kbn_references": ["@kbn/config-schema", "@kbn/ml-anomaly-utils"]
"kbn_references": [
"@kbn/config-schema",
"@kbn/ml-anomaly-utils",
"@kbn/alerting-comparators",
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
* 2.0.
*/

import { isEmpty, isError } from 'lodash';
import { schema } from '@kbn/config-schema';
import { Logger, LogMeta } from '@kbn/logging';
import { isError } from 'lodash';
import type { Logger, LogMeta } from '@kbn/logging';
import type { ElasticsearchClient } from '@kbn/core/server';
import { ObservabilityConfig } from '@kbn/observability-plugin/server';
import { ALERT_RULE_PARAMETERS, TIMESTAMP } from '@kbn/rule-data-utils';
Expand Down Expand Up @@ -54,30 +53,6 @@ const SUPPORTED_ES_FIELD_TYPES = [
ES_FIELD_TYPES.BOOLEAN,
];

export const oneOfLiterals = (arrayOfLiterals: Readonly<string[]>) =>
schema.string({
validate: (value) =>
arrayOfLiterals.includes(value) ? undefined : `must be one of ${arrayOfLiterals.join(' | ')}`,
});

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

const errorMessage = 'filterQuery must be a valid Elasticsearch filter expressed in JSON';
try {
const parsedValue = JSON.parse(value);
if (!isEmpty(parsedValue.bool)) {
return undefined;
}
return errorMessage;
} catch (e) {
return errorMessage;
}
};

export const UNGROUPED_FACTORY_KEY = '*';

export const createScopedLogger = (
Expand Down
Loading

0 comments on commit cfe68b0

Please sign in to comment.