mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[RAM] Modify rules client aggregate method to allow for custom aggregations (#149966)
## Summary Resolves: https://github.com/elastic/kibana/issues/131682 Taking inspiration from this PR: https://github.com/elastic/kibana/pull/148382 This PR changes the rules client aggregation method to accept arbitrary aggregations. This allows the users of the method to better customize the response when calling rules client aggregate. We have added validation to the aggregate method to prevent aggregations on certain rule fields. ```ts const ALLOW_FIELDS = [ 'alert.attributes.executionStatus.status', 'alert.attributes.lastRun.outcome', 'alert.attributes.muteAll', 'alert.attributes.tags', 'alert.attributes.snoozeSchedule', 'alert.attributes.snoozeSchedule.duration', 'alert.attributes.alertTypeId', 'alert.attributes.enabled', 'alert.attributes.params.*', ]; const ALLOW_AGG_TYPES = ['terms', 'composite', 'nested', 'filter']; ``` This PR also adds a new endpoint `rules/_tags` to allow for pagination of rule tags. This new endpoint takes advantage of the arbitrary aggregation of the new aggregate method. I tested the security solution rules page to ensure tags are still loading as expected. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Maxim Palenov <maxim.palenov@elastic.co>
This commit is contained in:
parent
7176d8c757
commit
50e2723268
31 changed files with 1737 additions and 343 deletions
|
@ -162,6 +162,8 @@ const histogramSchema = s.object({
|
|||
});
|
||||
|
||||
const compositeSchema = s.object({
|
||||
size: s.maybe(s.number()),
|
||||
after: s.maybe(s.recordOf(s.string(), s.nullable(s.oneOf([s.string(), s.number()])))),
|
||||
sources: s.arrayOf(
|
||||
s.recordOf(
|
||||
s.string(),
|
||||
|
|
114
x-pack/plugins/alerting/common/default_rule_aggregation.test.ts
Normal file
114
x-pack/plugins/alerting/common/default_rule_aggregation.test.ts
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
getDefaultRuleAggregation,
|
||||
formatDefaultAggregationResult,
|
||||
} from './default_rule_aggregation';
|
||||
|
||||
describe('getDefaultRuleAggregation', () => {
|
||||
it('should return aggregation with default maxTags', () => {
|
||||
const result = getDefaultRuleAggregation();
|
||||
expect(result.tags).toEqual({
|
||||
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 50 },
|
||||
});
|
||||
});
|
||||
|
||||
it('should return aggregation with custom maxTags', () => {
|
||||
const result = getDefaultRuleAggregation({ maxTags: 100 });
|
||||
expect(result.tags).toEqual({
|
||||
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: 100 },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDefaultAggregationResult', () => {
|
||||
it('should format aggregation result', () => {
|
||||
const result = formatDefaultAggregationResult({
|
||||
status: {
|
||||
buckets: [
|
||||
{ key: 'active', doc_count: 8 },
|
||||
{ key: 'error', doc_count: 6 },
|
||||
{ key: 'ok', doc_count: 10 },
|
||||
{ key: 'pending', doc_count: 4 },
|
||||
{ key: 'unknown', doc_count: 2 },
|
||||
{ key: 'warning', doc_count: 1 },
|
||||
],
|
||||
},
|
||||
outcome: {
|
||||
buckets: [
|
||||
{ key: 'succeeded', doc_count: 2 },
|
||||
{ key: 'failed', doc_count: 4 },
|
||||
{ key: 'warning', doc_count: 6 },
|
||||
],
|
||||
},
|
||||
enabled: {
|
||||
buckets: [
|
||||
{ key: 0, key_as_string: '0', doc_count: 2 },
|
||||
{ key: 1, key_as_string: '1', doc_count: 28 },
|
||||
],
|
||||
},
|
||||
muted: {
|
||||
buckets: [
|
||||
{ key: 0, key_as_string: '0', doc_count: 27 },
|
||||
{ key: 1, key_as_string: '1', doc_count: 3 },
|
||||
],
|
||||
},
|
||||
snoozed: {
|
||||
count: {
|
||||
doc_count: 5,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'a',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'b',
|
||||
doc_count: 20,
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
doc_count: 30,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual(
|
||||
expect.objectContaining({
|
||||
ruleExecutionStatus: {
|
||||
active: 8,
|
||||
error: 6,
|
||||
ok: 10,
|
||||
pending: 4,
|
||||
unknown: 2,
|
||||
warning: 1,
|
||||
},
|
||||
ruleLastRunOutcome: {
|
||||
succeeded: 2,
|
||||
failed: 4,
|
||||
warning: 6,
|
||||
},
|
||||
ruleEnabledStatus: {
|
||||
enabled: 28,
|
||||
disabled: 2,
|
||||
},
|
||||
ruleMutedStatus: {
|
||||
muted: 3,
|
||||
unmuted: 27,
|
||||
},
|
||||
ruleSnoozedStatus: {
|
||||
snoozed: 5,
|
||||
},
|
||||
ruleTags: ['a', 'b', 'c'],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
172
x-pack/plugins/alerting/common/default_rule_aggregation.ts
Normal file
172
x-pack/plugins/alerting/common/default_rule_aggregation.ts
Normal file
|
@ -0,0 +1,172 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
RuleExecutionStatusValues,
|
||||
RuleLastRunOutcomeValues,
|
||||
RuleAggregationFormattedResult,
|
||||
} from './rule';
|
||||
|
||||
export interface DefaultRuleAggregationResult {
|
||||
status: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
outcome: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
muted: {
|
||||
buckets: Array<{
|
||||
key: number;
|
||||
key_as_string: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
enabled: {
|
||||
buckets: Array<{
|
||||
key: number;
|
||||
key_as_string: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
snoozed: {
|
||||
count: {
|
||||
doc_count: number;
|
||||
};
|
||||
};
|
||||
tags: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetDefaultRuleAggregationParams {
|
||||
maxTags?: number;
|
||||
}
|
||||
|
||||
export const getDefaultRuleAggregation = (
|
||||
params?: GetDefaultRuleAggregationParams
|
||||
): Record<string, AggregationsAggregationContainer> => {
|
||||
const { maxTags = 50 } = params || {};
|
||||
return {
|
||||
status: {
|
||||
terms: { field: 'alert.attributes.executionStatus.status' },
|
||||
},
|
||||
outcome: {
|
||||
terms: { field: 'alert.attributes.lastRun.outcome' },
|
||||
},
|
||||
enabled: {
|
||||
terms: { field: 'alert.attributes.enabled' },
|
||||
},
|
||||
muted: {
|
||||
terms: { field: 'alert.attributes.muteAll' },
|
||||
},
|
||||
tags: {
|
||||
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: maxTags },
|
||||
},
|
||||
snoozed: {
|
||||
nested: {
|
||||
path: 'alert.attributes.snoozeSchedule',
|
||||
},
|
||||
aggs: {
|
||||
count: {
|
||||
filter: {
|
||||
exists: {
|
||||
field: 'alert.attributes.snoozeSchedule.duration',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const formatDefaultAggregationResult = (
|
||||
aggregations: DefaultRuleAggregationResult
|
||||
): RuleAggregationFormattedResult => {
|
||||
if (!aggregations) {
|
||||
// Return a placeholder with all zeroes
|
||||
const placeholder: RuleAggregationFormattedResult = {
|
||||
ruleExecutionStatus: {},
|
||||
ruleLastRunOutcome: {},
|
||||
ruleEnabledStatus: {
|
||||
enabled: 0,
|
||||
disabled: 0,
|
||||
},
|
||||
ruleMutedStatus: {
|
||||
muted: 0,
|
||||
unmuted: 0,
|
||||
},
|
||||
ruleSnoozedStatus: { snoozed: 0 },
|
||||
ruleTags: [],
|
||||
};
|
||||
|
||||
for (const key of RuleExecutionStatusValues) {
|
||||
placeholder.ruleExecutionStatus[key] = 0;
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
const ruleExecutionStatus = aggregations.status.buckets.map(({ key, doc_count: docCount }) => ({
|
||||
[key]: docCount,
|
||||
}));
|
||||
|
||||
const ruleLastRunOutcome = aggregations.outcome.buckets.map(({ key, doc_count: docCount }) => ({
|
||||
[key]: docCount,
|
||||
}));
|
||||
|
||||
const enabledBuckets = aggregations.enabled.buckets;
|
||||
const mutedBuckets = aggregations.muted.buckets;
|
||||
|
||||
const result: RuleAggregationFormattedResult = {
|
||||
ruleExecutionStatus: ruleExecutionStatus.reduce(
|
||||
(acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
|
||||
{}
|
||||
),
|
||||
ruleLastRunOutcome: ruleLastRunOutcome.reduce(
|
||||
(acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
|
||||
{}
|
||||
),
|
||||
ruleEnabledStatus: {
|
||||
enabled: enabledBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
|
||||
disabled: enabledBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
|
||||
},
|
||||
ruleMutedStatus: {
|
||||
muted: mutedBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
|
||||
unmuted: mutedBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
|
||||
},
|
||||
ruleSnoozedStatus: {
|
||||
snoozed: aggregations.snoozed?.count?.doc_count ?? 0,
|
||||
},
|
||||
ruleTags: [],
|
||||
};
|
||||
|
||||
// Fill missing keys with zeroes
|
||||
for (const key of RuleExecutionStatusValues) {
|
||||
if (!result.ruleExecutionStatus.hasOwnProperty(key)) {
|
||||
result.ruleExecutionStatus[key] = 0;
|
||||
}
|
||||
}
|
||||
for (const key of RuleLastRunOutcomeValues) {
|
||||
if (!result.ruleLastRunOutcome.hasOwnProperty(key)) {
|
||||
result.ruleLastRunOutcome[key] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const tagsBuckets = aggregations.tags?.buckets || [];
|
||||
result.ruleTags = tagsBuckets.map((bucket) => bucket.key);
|
||||
|
||||
return result;
|
||||
};
|
|
@ -23,6 +23,8 @@ export * from './rule_notify_when_type';
|
|||
export * from './parse_duration';
|
||||
export * from './execution_log_types';
|
||||
export * from './rule_snooze_type';
|
||||
export * from './default_rule_aggregation';
|
||||
export * from './rule_tags_aggregation';
|
||||
|
||||
export { mappingFromFieldMap, getComponentTemplateFromFieldMap } from './alert_schema';
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import type {
|
|||
SavedObjectAttributes,
|
||||
SavedObjectsResolveResponse,
|
||||
} from '@kbn/core/server';
|
||||
import type { KueryNode } from '@kbn/es-query';
|
||||
import { RuleNotifyWhenType } from './rule_notify_when_type';
|
||||
import { RuleSnooze } from './rule_snooze_type';
|
||||
|
||||
|
@ -89,8 +90,21 @@ export interface RuleAction {
|
|||
};
|
||||
}
|
||||
|
||||
export interface RuleAggregations {
|
||||
alertExecutionStatus: { [status: string]: number };
|
||||
export interface AggregateOptions {
|
||||
search?: string;
|
||||
defaultSearchOperator?: 'AND' | 'OR';
|
||||
searchFields?: string[];
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
filter?: string | KueryNode;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
}
|
||||
|
||||
export interface RuleAggregationFormattedResult {
|
||||
ruleExecutionStatus: { [status: string]: number };
|
||||
ruleLastRunOutcome: { [status: string]: number };
|
||||
ruleEnabledStatus: { enabled: number; disabled: number };
|
||||
ruleMutedStatus: { muted: number; unmuted: number };
|
||||
|
|
93
x-pack/plugins/alerting/common/rule_tags_aggregation.test.ts
Normal file
93
x-pack/plugins/alerting/common/rule_tags_aggregation.test.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getRuleTagsAggregation, formatRuleTagsAggregationResult } from './rule_tags_aggregation';
|
||||
|
||||
describe('getRuleTagsAggregation', () => {
|
||||
it('should return aggregation with default params', () => {
|
||||
const result = getRuleTagsAggregation();
|
||||
expect(result.tags).toEqual({
|
||||
composite: {
|
||||
size: 50,
|
||||
sources: [
|
||||
{
|
||||
tags: {
|
||||
terms: {
|
||||
field: 'alert.attributes.tags',
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return aggregation with custom maxTags', () => {
|
||||
const result = getRuleTagsAggregation({
|
||||
maxTags: 100,
|
||||
after: {
|
||||
tags: 'e',
|
||||
},
|
||||
});
|
||||
expect(result.tags).toEqual({
|
||||
composite: {
|
||||
size: 100,
|
||||
after: {
|
||||
tags: 'e',
|
||||
},
|
||||
sources: [
|
||||
{
|
||||
tags: {
|
||||
terms: {
|
||||
field: 'alert.attributes.tags',
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatRuleTagsAggregationResult', () => {
|
||||
it('should format aggregation result', () => {
|
||||
const result = formatRuleTagsAggregationResult({
|
||||
tags: {
|
||||
buckets: [
|
||||
{
|
||||
key: {
|
||||
tags: 'a',
|
||||
},
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
tags: 'b',
|
||||
},
|
||||
doc_count: 2,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
tags: 'c',
|
||||
},
|
||||
doc_count: 3,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
tags: 'd',
|
||||
},
|
||||
doc_count: 4,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ruleTags).toEqual(['a', 'b', 'c', 'd']);
|
||||
});
|
||||
});
|
76
x-pack/plugins/alerting/common/rule_tags_aggregation.ts
Normal file
76
x-pack/plugins/alerting/common/rule_tags_aggregation.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type {
|
||||
AggregationsAggregationContainer,
|
||||
AggregationsCompositeAggregation,
|
||||
AggregationsAggregateOrder,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { AggregateOptions } from './rule';
|
||||
|
||||
export type RuleTagsAggregationOptions = Pick<AggregateOptions, 'filter' | 'search'> & {
|
||||
after?: AggregationsCompositeAggregation['after'];
|
||||
maxTags?: number;
|
||||
};
|
||||
|
||||
export interface RuleTagsAggregationFormattedResult {
|
||||
ruleTags: string[];
|
||||
}
|
||||
|
||||
export interface RuleTagsAggregationResult {
|
||||
tags: {
|
||||
buckets: Array<{
|
||||
key: {
|
||||
tags: string;
|
||||
};
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetRuleTagsAggregationParams {
|
||||
maxTags?: number;
|
||||
after?: AggregationsCompositeAggregation['after'];
|
||||
}
|
||||
|
||||
export const getRuleTagsAggregation = (
|
||||
params?: GetRuleTagsAggregationParams
|
||||
): Record<string, AggregationsAggregationContainer> => {
|
||||
const { maxTags = 50, after } = params || {};
|
||||
return {
|
||||
tags: {
|
||||
composite: {
|
||||
...(after ? { after } : {}),
|
||||
size: maxTags,
|
||||
sources: [
|
||||
{
|
||||
tags: {
|
||||
terms: {
|
||||
field: 'alert.attributes.tags',
|
||||
order: 'asc' as unknown as AggregationsAggregateOrder,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const formatRuleTagsAggregationResult = (
|
||||
aggregations: RuleTagsAggregationResult
|
||||
): RuleTagsAggregationFormattedResult => {
|
||||
if (!aggregations) {
|
||||
return {
|
||||
ruleTags: [],
|
||||
};
|
||||
}
|
||||
const tagsBuckets = aggregations.tags.buckets || [];
|
||||
return {
|
||||
ruleTags: tagsBuckets.map((bucket) => bucket.key.tags),
|
||||
};
|
||||
};
|
|
@ -30,6 +30,103 @@ beforeEach(() => {
|
|||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const aggregateResult = {
|
||||
status: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'ok',
|
||||
doc_count: 15,
|
||||
},
|
||||
{
|
||||
key: 'error',
|
||||
doc_count: 2,
|
||||
},
|
||||
{
|
||||
key: 'active',
|
||||
doc_count: 23,
|
||||
},
|
||||
{
|
||||
key: 'pending',
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: 'unknown',
|
||||
doc_count: 0,
|
||||
},
|
||||
{
|
||||
key: 'warning',
|
||||
doc_count: 10,
|
||||
},
|
||||
],
|
||||
},
|
||||
outcome: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'succeeded',
|
||||
doc_count: 2,
|
||||
},
|
||||
{
|
||||
key: 'failed',
|
||||
doc_count: 4,
|
||||
},
|
||||
{
|
||||
key: 'warning',
|
||||
doc_count: 6,
|
||||
},
|
||||
],
|
||||
},
|
||||
enabled: {
|
||||
buckets: [
|
||||
{
|
||||
key: 0,
|
||||
key_as_string: '0',
|
||||
doc_count: 2,
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
key_as_string: '1',
|
||||
doc_count: 28,
|
||||
},
|
||||
],
|
||||
},
|
||||
muted: {
|
||||
buckets: [
|
||||
{
|
||||
key: 0,
|
||||
key_as_string: '0',
|
||||
doc_count: 27,
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
key_as_string: '1',
|
||||
doc_count: 3,
|
||||
},
|
||||
],
|
||||
},
|
||||
snoozed: {
|
||||
doc_count: 0,
|
||||
count: {
|
||||
doc_count: 0,
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
buckets: [
|
||||
{
|
||||
key: 'a',
|
||||
doc_count: 10,
|
||||
},
|
||||
{
|
||||
key: 'b',
|
||||
doc_count: 20,
|
||||
},
|
||||
{
|
||||
key: 'c',
|
||||
doc_count: 30,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
describe('aggregateRulesRoute', () => {
|
||||
it('aggregate rules with proper parameters', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
|
@ -41,32 +138,6 @@ describe('aggregateRulesRoute', () => {
|
|||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rules/_aggregate"`);
|
||||
|
||||
const aggregateResult = {
|
||||
alertExecutionStatus: {
|
||||
ok: 15,
|
||||
error: 2,
|
||||
active: 23,
|
||||
pending: 1,
|
||||
unknown: 0,
|
||||
},
|
||||
ruleLastRunOutcome: {
|
||||
succeeded: 1,
|
||||
failed: 2,
|
||||
warning: 3,
|
||||
},
|
||||
ruleEnabledStatus: {
|
||||
disabled: 1,
|
||||
enabled: 40,
|
||||
},
|
||||
ruleMutedStatus: {
|
||||
muted: 2,
|
||||
unmuted: 39,
|
||||
},
|
||||
ruleSnoozedStatus: {
|
||||
snoozed: 4,
|
||||
},
|
||||
ruleTags: ['a', 'b', 'c'],
|
||||
};
|
||||
rulesClient.aggregate.mockResolvedValueOnce(aggregateResult);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
|
@ -83,8 +154,8 @@ describe('aggregateRulesRoute', () => {
|
|||
Object {
|
||||
"body": Object {
|
||||
"rule_enabled_status": Object {
|
||||
"disabled": 1,
|
||||
"enabled": 40,
|
||||
"disabled": 2,
|
||||
"enabled": 28,
|
||||
},
|
||||
"rule_execution_status": Object {
|
||||
"active": 23,
|
||||
|
@ -92,18 +163,19 @@ describe('aggregateRulesRoute', () => {
|
|||
"ok": 15,
|
||||
"pending": 1,
|
||||
"unknown": 0,
|
||||
"warning": 10,
|
||||
},
|
||||
"rule_last_run_outcome": Object {
|
||||
"failed": 2,
|
||||
"succeeded": 1,
|
||||
"warning": 3,
|
||||
"failed": 4,
|
||||
"succeeded": 2,
|
||||
"warning": 6,
|
||||
},
|
||||
"rule_muted_status": Object {
|
||||
"muted": 2,
|
||||
"unmuted": 39,
|
||||
"muted": 3,
|
||||
"unmuted": 27,
|
||||
},
|
||||
"rule_snoozed_status": Object {
|
||||
"snoozed": 4,
|
||||
"snoozed": 0,
|
||||
},
|
||||
"rule_tags": Array [
|
||||
"a",
|
||||
|
@ -118,6 +190,51 @@ describe('aggregateRulesRoute', () => {
|
|||
expect(rulesClient.aggregate.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"aggs": Object {
|
||||
"enabled": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.enabled",
|
||||
},
|
||||
},
|
||||
"muted": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.muteAll",
|
||||
},
|
||||
},
|
||||
"outcome": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.lastRun.outcome",
|
||||
},
|
||||
},
|
||||
"snoozed": Object {
|
||||
"aggs": Object {
|
||||
"count": Object {
|
||||
"filter": Object {
|
||||
"exists": Object {
|
||||
"field": "alert.attributes.snoozeSchedule.duration",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"nested": Object {
|
||||
"path": "alert.attributes.snoozeSchedule",
|
||||
},
|
||||
},
|
||||
"status": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.executionStatus.status",
|
||||
},
|
||||
},
|
||||
"tags": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.tags",
|
||||
"order": Object {
|
||||
"_key": "asc",
|
||||
},
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
"options": Object {
|
||||
"defaultSearchOperator": "AND",
|
||||
},
|
||||
|
@ -128,8 +245,8 @@ describe('aggregateRulesRoute', () => {
|
|||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: {
|
||||
rule_enabled_status: {
|
||||
disabled: 1,
|
||||
enabled: 40,
|
||||
disabled: 2,
|
||||
enabled: 28,
|
||||
},
|
||||
rule_execution_status: {
|
||||
ok: 15,
|
||||
|
@ -137,18 +254,19 @@ describe('aggregateRulesRoute', () => {
|
|||
active: 23,
|
||||
pending: 1,
|
||||
unknown: 0,
|
||||
warning: 10,
|
||||
},
|
||||
rule_last_run_outcome: {
|
||||
succeeded: 1,
|
||||
failed: 2,
|
||||
warning: 3,
|
||||
failed: 4,
|
||||
succeeded: 2,
|
||||
warning: 6,
|
||||
},
|
||||
rule_muted_status: {
|
||||
muted: 2,
|
||||
unmuted: 39,
|
||||
muted: 3,
|
||||
unmuted: 27,
|
||||
},
|
||||
rule_snoozed_status: {
|
||||
snoozed: 4,
|
||||
snoozed: 0,
|
||||
},
|
||||
rule_tags: ['a', 'b', 'c'],
|
||||
},
|
||||
|
@ -163,20 +281,7 @@ describe('aggregateRulesRoute', () => {
|
|||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
rulesClient.aggregate.mockResolvedValueOnce({
|
||||
alertExecutionStatus: {
|
||||
ok: 15,
|
||||
error: 2,
|
||||
active: 23,
|
||||
pending: 1,
|
||||
unknown: 0,
|
||||
},
|
||||
ruleLastRunOutcome: {
|
||||
succeeded: 2,
|
||||
failed: 4,
|
||||
warning: 6,
|
||||
},
|
||||
});
|
||||
rulesClient.aggregate.mockResolvedValueOnce(aggregateResult);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
|
@ -221,22 +326,10 @@ describe('aggregateRulesRoute', () => {
|
|||
const router = httpServiceMock.createRouter();
|
||||
|
||||
aggregateRulesRoute(router, licenseState, mockUsageCounter);
|
||||
const aggregateResult = {
|
||||
alertExecutionStatus: {
|
||||
ok: 15,
|
||||
error: 2,
|
||||
active: 23,
|
||||
pending: 1,
|
||||
unknown: 0,
|
||||
},
|
||||
ruleLastRunOutcome: {
|
||||
succeeded: 2,
|
||||
failed: 4,
|
||||
warning: 6,
|
||||
},
|
||||
};
|
||||
rulesClient.aggregate.mockResolvedValueOnce(aggregateResult);
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
rulesClient.aggregate.mockResolvedValueOnce(aggregateResult);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
|
|
|
@ -8,8 +8,14 @@
|
|||
import { IRouter } from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
||||
import {
|
||||
AggregateOptions,
|
||||
DefaultRuleAggregationResult,
|
||||
formatDefaultAggregationResult,
|
||||
getDefaultRuleAggregation,
|
||||
RuleAggregationFormattedResult,
|
||||
} from '../../common';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { AggregateResult, AggregateOptions } from '../rules_client';
|
||||
import { RewriteResponseCase, RewriteRequestCase, verifyAccessAndContext } from './lib';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types';
|
||||
import { trackLegacyTerminology } from './lib/track_legacy_terminology';
|
||||
|
@ -45,8 +51,8 @@ const rewriteQueryReq: RewriteRequestCase<AggregateOptions> = ({
|
|||
...(hasReference ? { hasReference } : {}),
|
||||
...(searchFields ? { searchFields } : {}),
|
||||
});
|
||||
const rewriteBodyRes: RewriteResponseCase<AggregateResult> = ({
|
||||
alertExecutionStatus,
|
||||
const rewriteBodyRes: RewriteResponseCase<RuleAggregationFormattedResult> = ({
|
||||
ruleExecutionStatus,
|
||||
ruleLastRunOutcome,
|
||||
ruleEnabledStatus,
|
||||
ruleMutedStatus,
|
||||
|
@ -55,7 +61,7 @@ const rewriteBodyRes: RewriteResponseCase<AggregateResult> = ({
|
|||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rule_execution_status: alertExecutionStatus,
|
||||
rule_execution_status: ruleExecutionStatus,
|
||||
rule_last_run_outcome: ruleLastRunOutcome,
|
||||
rule_enabled_status: ruleEnabledStatus,
|
||||
rule_muted_status: ruleMutedStatus,
|
||||
|
@ -86,9 +92,12 @@ export const aggregateRulesRoute = (
|
|||
[req.query.search, req.query.search_fields].filter(Boolean) as string[],
|
||||
usageCounter
|
||||
);
|
||||
const aggregateResult = await rulesClient.aggregate({ options });
|
||||
const aggregateResult = await rulesClient.aggregate<DefaultRuleAggregationResult>({
|
||||
aggs: getDefaultRuleAggregation(),
|
||||
options,
|
||||
});
|
||||
return res.ok({
|
||||
body: rewriteBodyRes(aggregateResult),
|
||||
body: rewriteBodyRes(formatDefaultAggregationResult(aggregateResult)),
|
||||
});
|
||||
})
|
||||
)
|
||||
|
@ -111,9 +120,12 @@ export const aggregateRulesRoute = (
|
|||
[req.body.search, req.body.search_fields].filter(Boolean) as string[],
|
||||
usageCounter
|
||||
);
|
||||
const aggregateResult = await rulesClient.aggregate({ options });
|
||||
const aggregateResult = await rulesClient.aggregate<DefaultRuleAggregationResult>({
|
||||
aggs: getDefaultRuleAggregation(),
|
||||
options,
|
||||
});
|
||||
return res.ok({
|
||||
body: rewriteBodyRes(aggregateResult),
|
||||
body: rewriteBodyRes(formatDefaultAggregationResult(aggregateResult)),
|
||||
});
|
||||
})
|
||||
)
|
||||
|
|
170
x-pack/plugins/alerting/server/routes/get_rule_tags.test.ts
Normal file
170
x-pack/plugins/alerting/server/routes/get_rule_tags.test.ts
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { httpServiceMock } from '@kbn/core/server/mocks';
|
||||
import { licenseStateMock } from '../lib/license_state.mock';
|
||||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { mockHandlerArguments } from './_mock_handler_arguments';
|
||||
import { rulesClientMock } from '../rules_client.mock';
|
||||
import { getRuleTagsRoute } from './get_rule_tags';
|
||||
|
||||
import {} from '../../common/rule_tags_aggregation';
|
||||
|
||||
const rulesClient = rulesClientMock.create();
|
||||
|
||||
jest.mock('../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../common/rule_tags_aggregation', () => ({
|
||||
...jest.requireActual('../../common/rule_tags_aggregation'),
|
||||
formatRuleTagsAggregationResult: jest.fn(),
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
const { formatRuleTagsAggregationResult } = jest.requireMock('../../common/rule_tags_aggregation');
|
||||
|
||||
describe('getRuleTagsRoute', () => {
|
||||
it('aggregates rule tags with proper parameters', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getRuleTagsRoute(router, licenseState);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/rules/_tags"`);
|
||||
|
||||
const aggregateResult = { ruleTags: ['a', 'b', 'c'] };
|
||||
|
||||
formatRuleTagsAggregationResult.mockReturnValueOnce(aggregateResult);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
query: {
|
||||
filter: 'test',
|
||||
search: 'search text',
|
||||
after: {
|
||||
tags: 'c',
|
||||
},
|
||||
},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"rule_tags": Array [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
],
|
||||
},
|
||||
}
|
||||
`);
|
||||
expect(rulesClient.aggregate).toHaveBeenCalledTimes(1);
|
||||
expect(rulesClient.aggregate.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"aggs": Object {
|
||||
"tags": Object {
|
||||
"composite": Object {
|
||||
"after": Object {
|
||||
"tags": "c",
|
||||
},
|
||||
"size": 50,
|
||||
"sources": Array [
|
||||
Object {
|
||||
"tags": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.tags",
|
||||
"order": "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
"options": Object {
|
||||
"after": Object {
|
||||
"tags": "c",
|
||||
},
|
||||
"defaultSearchOperator": "AND",
|
||||
"filter": "test",
|
||||
"search": "search text",
|
||||
"searchFields": Array [
|
||||
"tags",
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
`);
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: {
|
||||
rule_tags: ['a', 'b', 'c'],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('ensures the license allows aggregating rule tags', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
getRuleTagsRoute(router, licenseState);
|
||||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
formatRuleTagsAggregationResult.mockReturnValueOnce({ ruleTags: ['a', 'b', 'c', 'd'] });
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
{
|
||||
query: {
|
||||
filter: 'test',
|
||||
search: 'search text',
|
||||
after: {
|
||||
tags: 'c',
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
|
||||
it('ensures the license check prevents aggregating rule tags', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
(verifyApiAccess as jest.Mock).mockImplementation(() => {
|
||||
throw new Error('OMG');
|
||||
});
|
||||
|
||||
getRuleTagsRoute(router, licenseState);
|
||||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{},
|
||||
{
|
||||
query: {},
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
expect(handler(context, req, res)).rejects.toMatchInlineSnapshot(`[Error: OMG]`);
|
||||
|
||||
expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);
|
||||
});
|
||||
});
|
83
x-pack/plugins/alerting/server/routes/get_rule_tags.ts
Normal file
83
x-pack/plugins/alerting/server/routes/get_rule_tags.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
RuleTagsAggregationResult,
|
||||
RuleTagsAggregationFormattedResult,
|
||||
RuleTagsAggregationOptions,
|
||||
getRuleTagsAggregation,
|
||||
formatRuleTagsAggregationResult,
|
||||
} from '../../common';
|
||||
import { AlertingRequestHandlerContext, INTERNAL_BASE_ALERTING_API_PATH } from '../types';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { RewriteResponseCase, RewriteRequestCase, verifyAccessAndContext } from './lib';
|
||||
|
||||
const querySchema = schema.object({
|
||||
filter: schema.maybe(schema.string()),
|
||||
search: schema.maybe(schema.string()),
|
||||
after: schema.maybe(
|
||||
schema.recordOf(
|
||||
schema.string(),
|
||||
schema.nullable(schema.oneOf([schema.string(), schema.number()]))
|
||||
)
|
||||
),
|
||||
max_tags: schema.maybe(schema.number()),
|
||||
});
|
||||
|
||||
const rewriteQueryReq: RewriteRequestCase<RuleTagsAggregationOptions> = ({
|
||||
max_tags: maxTags,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
...(maxTags ? { maxTags } : {}),
|
||||
});
|
||||
|
||||
const rewriteBodyRes: RewriteResponseCase<RuleTagsAggregationFormattedResult> = ({
|
||||
ruleTags,
|
||||
...rest
|
||||
}) => ({
|
||||
...rest,
|
||||
rule_tags: ruleTags,
|
||||
});
|
||||
|
||||
export const getRuleTagsRoute = (
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState
|
||||
) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${INTERNAL_BASE_ALERTING_API_PATH}/rules/_tags`,
|
||||
validate: {
|
||||
query: querySchema,
|
||||
},
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, req, res) {
|
||||
const rulesClient = (await context.alerting).getRulesClient();
|
||||
const options = rewriteQueryReq(req.query);
|
||||
|
||||
const aggregateResult = await rulesClient.aggregate<RuleTagsAggregationResult>({
|
||||
options: {
|
||||
...options,
|
||||
defaultSearchOperator: 'AND',
|
||||
searchFields: ['tags'],
|
||||
},
|
||||
aggs: getRuleTagsAggregation({
|
||||
maxTags: options.maxTags,
|
||||
after: options.after,
|
||||
}),
|
||||
});
|
||||
|
||||
return res.ok({
|
||||
body: rewriteBodyRes(formatRuleTagsAggregationResult(aggregateResult)),
|
||||
});
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -44,6 +44,7 @@ import { bulkDisableRulesRoute } from './bulk_disable_rules';
|
|||
import { cloneRuleRoute } from './clone_rule';
|
||||
import { getFlappingSettingsRoute } from './get_flapping_settings';
|
||||
import { updateFlappingSettingsRoute } from './update_flapping_settings';
|
||||
import { getRuleTagsRoute } from './get_rule_tags';
|
||||
|
||||
export interface RouteOptions {
|
||||
router: IRouter<AlertingRequestHandlerContext>;
|
||||
|
@ -91,4 +92,5 @@ export function defineRoutes(opts: RouteOptions) {
|
|||
cloneRuleRoute(router, licenseState);
|
||||
getFlappingSettingsRoute(router, licenseState);
|
||||
updateFlappingSettingsRoute(router, licenseState);
|
||||
getRuleTagsRoute(router, licenseState);
|
||||
}
|
||||
|
|
|
@ -31,6 +31,13 @@ jest.mock('../lib/track_legacy_terminology', () => ({
|
|||
trackLegacyTerminology: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../../common', () => ({
|
||||
...jest.requireActual('../../../common'),
|
||||
formatDefaultAggregationResult: jest.fn(),
|
||||
}));
|
||||
|
||||
const { formatDefaultAggregationResult } = jest.requireMock('../../../common');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
@ -47,7 +54,7 @@ describe('aggregateAlertRoute', () => {
|
|||
expect(config.path).toMatchInlineSnapshot(`"/api/alerts/_aggregate"`);
|
||||
|
||||
const aggregateResult = {
|
||||
alertExecutionStatus: {
|
||||
ruleExecutionStatus: {
|
||||
ok: 15,
|
||||
error: 2,
|
||||
active: 23,
|
||||
|
@ -60,7 +67,7 @@ describe('aggregateAlertRoute', () => {
|
|||
warning: 3,
|
||||
},
|
||||
};
|
||||
rulesClient.aggregate.mockResolvedValueOnce(aggregateResult);
|
||||
formatDefaultAggregationResult.mockReturnValueOnce(aggregateResult);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
|
@ -95,6 +102,51 @@ describe('aggregateAlertRoute', () => {
|
|||
expect(rulesClient.aggregate.mock.calls[0]).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Object {
|
||||
"aggs": Object {
|
||||
"enabled": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.enabled",
|
||||
},
|
||||
},
|
||||
"muted": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.muteAll",
|
||||
},
|
||||
},
|
||||
"outcome": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.lastRun.outcome",
|
||||
},
|
||||
},
|
||||
"snoozed": Object {
|
||||
"aggs": Object {
|
||||
"count": Object {
|
||||
"filter": Object {
|
||||
"exists": Object {
|
||||
"field": "alert.attributes.snoozeSchedule.duration",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"nested": Object {
|
||||
"path": "alert.attributes.snoozeSchedule",
|
||||
},
|
||||
},
|
||||
"status": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.executionStatus.status",
|
||||
},
|
||||
},
|
||||
"tags": Object {
|
||||
"terms": Object {
|
||||
"field": "alert.attributes.tags",
|
||||
"order": Object {
|
||||
"_key": "asc",
|
||||
},
|
||||
"size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
"options": Object {
|
||||
"defaultSearchOperator": "AND",
|
||||
},
|
||||
|
@ -103,7 +155,10 @@ describe('aggregateAlertRoute', () => {
|
|||
`);
|
||||
|
||||
expect(res.ok).toHaveBeenCalledWith({
|
||||
body: aggregateResult,
|
||||
body: {
|
||||
ruleLastRunOutcome: aggregateResult.ruleLastRunOutcome,
|
||||
alertExecutionStatus: aggregateResult.ruleExecutionStatus,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -115,8 +170,8 @@ describe('aggregateAlertRoute', () => {
|
|||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
|
||||
rulesClient.aggregate.mockResolvedValueOnce({
|
||||
alertExecutionStatus: {
|
||||
formatDefaultAggregationResult.mockReturnValueOnce({
|
||||
ruleExecutionStatus: {
|
||||
ok: 15,
|
||||
error: 2,
|
||||
active: 23,
|
||||
|
@ -173,6 +228,22 @@ describe('aggregateAlertRoute', () => {
|
|||
const router = httpServiceMock.createRouter();
|
||||
|
||||
aggregateAlertRoute(router, licenseState, mockUsageCounter);
|
||||
|
||||
formatDefaultAggregationResult.mockReturnValueOnce({
|
||||
ruleExecutionStatus: {
|
||||
ok: 15,
|
||||
error: 2,
|
||||
active: 23,
|
||||
pending: 1,
|
||||
unknown: 0,
|
||||
},
|
||||
ruleLastRunOutcome: {
|
||||
succeeded: 1,
|
||||
failed: 2,
|
||||
warning: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
|
@ -192,6 +263,22 @@ describe('aggregateAlertRoute', () => {
|
|||
const router = httpServiceMock.createRouter();
|
||||
|
||||
aggregateAlertRoute(router, licenseState, mockUsageCounter);
|
||||
|
||||
formatDefaultAggregationResult.mockReturnValueOnce({
|
||||
ruleExecutionStatus: {
|
||||
ok: 15,
|
||||
error: 2,
|
||||
active: 23,
|
||||
pending: 1,
|
||||
unknown: 0,
|
||||
},
|
||||
ruleLastRunOutcome: {
|
||||
succeeded: 1,
|
||||
failed: 2,
|
||||
warning: 3,
|
||||
},
|
||||
});
|
||||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ rulesClient },
|
||||
|
|
|
@ -10,7 +10,12 @@ import { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
|||
import type { AlertingRouter } from '../../types';
|
||||
import { ILicenseState } from '../../lib/license_state';
|
||||
import { verifyApiAccess } from '../../lib/license_api_access';
|
||||
import { LEGACY_BASE_ALERT_API_PATH } from '../../../common';
|
||||
import {
|
||||
LEGACY_BASE_ALERT_API_PATH,
|
||||
DefaultRuleAggregationResult,
|
||||
getDefaultRuleAggregation,
|
||||
formatDefaultAggregationResult,
|
||||
} from '../../../common';
|
||||
import { renameKeys } from '../lib/rename_keys';
|
||||
import { FindOptions } from '../../rules_client';
|
||||
import { trackLegacyRouteUsage } from '../../lib/track_legacy_route_usage';
|
||||
|
@ -77,9 +82,16 @@ export const aggregateAlertRoute = (
|
|||
: [query.search_fields];
|
||||
}
|
||||
|
||||
const aggregateResult = await rulesClient.aggregate({ options });
|
||||
const aggregateResult = await rulesClient.aggregate<DefaultRuleAggregationResult>({
|
||||
options,
|
||||
aggs: getDefaultRuleAggregation(),
|
||||
});
|
||||
const { ruleExecutionStatus, ...rest } = formatDefaultAggregationResult(aggregateResult);
|
||||
return res.ok({
|
||||
body: aggregateResult,
|
||||
body: {
|
||||
...rest,
|
||||
alertExecutionStatus: ruleExecutionStatus,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
|
|
|
@ -10,8 +10,6 @@ type RenameAlertToRule<K extends string> = K extends `alertTypeId`
|
|||
? `ruleTypeId`
|
||||
: K extends `alertId`
|
||||
? `ruleId`
|
||||
: K extends `alertExecutionStatus`
|
||||
? `ruleExecutionStatus`
|
||||
: K extends `actionTypeId`
|
||||
? `connectorTypeId`
|
||||
: K extends `alertInstanceId`
|
||||
|
|
|
@ -12,7 +12,7 @@ export type RulesClientMock = jest.Mocked<Schema>;
|
|||
|
||||
const createRulesClientMock = () => {
|
||||
const mocked: RulesClientMock = {
|
||||
aggregate: jest.fn().mockReturnValue({ alertExecutionStatus: {}, ruleLastRunOutcome: {} }),
|
||||
aggregate: jest.fn().mockReturnValue({ ruleExecutionStatus: {}, ruleLastRunOutcome: {} }),
|
||||
create: jest.fn(),
|
||||
get: jest.fn(),
|
||||
resolve: jest.fn(),
|
||||
|
|
|
@ -0,0 +1,304 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getRuleTagsAggregation, getDefaultRuleAggregation } from '../../../common';
|
||||
import type { AggregationsAggregateOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { validateRuleAggregationFields } from './validate_rule_aggregation_fields';
|
||||
|
||||
describe('validateAggregationTerms', () => {
|
||||
it('should allow for simple valid aggregations', () => {
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.lastRun.outcome',
|
||||
},
|
||||
},
|
||||
name2: {
|
||||
terms: {
|
||||
field: 'alert.attributes.tags',
|
||||
},
|
||||
},
|
||||
});
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
it('should allow for nested valid aggregations', () => {
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.executionStatus.status',
|
||||
},
|
||||
aggs: {
|
||||
nestedAggs: {
|
||||
terms: {
|
||||
field: 'alert.attributes.snoozeSchedule',
|
||||
},
|
||||
aggs: {
|
||||
anotherNestedAgg: {
|
||||
terms: {
|
||||
field: 'alert.attributes.alertTypeId',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
it('should allow for nested valid aggregations with root level aggs', () => {
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
composite: {
|
||||
sources: [
|
||||
{
|
||||
tags: {
|
||||
terms: {
|
||||
field: 'alert.attributes.tags',
|
||||
order: 'asc' as unknown as AggregationsAggregateOrder,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
terms: {
|
||||
field: 'alert.attributes.muteAll',
|
||||
},
|
||||
aggs: {
|
||||
nestedAggs: {
|
||||
nested: {
|
||||
path: 'alert.attributes.snoozeSchedule',
|
||||
},
|
||||
terms: {
|
||||
field: 'alert.attributes.enabled',
|
||||
},
|
||||
aggs: {
|
||||
nestedAggsAgain: {
|
||||
terms: {
|
||||
field: 'alert.attributes.executionStatus.status',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
it('should allow for default and tags aggregations', () => {
|
||||
expect(() => validateRuleAggregationFields(getDefaultRuleAggregation())).not.toThrowError();
|
||||
expect(() => validateRuleAggregationFields(getRuleTagsAggregation())).not.toThrowError();
|
||||
});
|
||||
|
||||
it('should throw for simple aggregation with invalid fields', () => {
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.apiKey',
|
||||
},
|
||||
},
|
||||
name2: {
|
||||
terms: {
|
||||
field: 'foo.attributes.bar1',
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation term: alert.attributes.apiKey"`);
|
||||
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.bar',
|
||||
},
|
||||
},
|
||||
name2: {
|
||||
terms: {
|
||||
field: 'alert.attributes.consumer',
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation term: alert.attributes.bar"`);
|
||||
});
|
||||
|
||||
it('should throw for nested aggregations with invalid fields', () => {
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.apiKey',
|
||||
},
|
||||
aggs: {
|
||||
nestedAggs: {
|
||||
terms: {
|
||||
field: 'foo.attributes.bar1',
|
||||
},
|
||||
aggs: {
|
||||
anotherNestedAgg: {
|
||||
terms: {
|
||||
field: 'foo.attributes.bar2',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation term: alert.attributes.apiKey"`);
|
||||
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.snoozeSchedule',
|
||||
},
|
||||
aggs: {
|
||||
nestedAggs: {
|
||||
terms: {
|
||||
field: 'alert.attributes.enabled',
|
||||
},
|
||||
aggs: {
|
||||
anotherNestedAgg: {
|
||||
terms: {
|
||||
field: 'alert.attributes.consumer',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation term: alert.attributes.consumer"`);
|
||||
});
|
||||
|
||||
it('should throw for both aggs and aggregations at the same nesting level with invalid fields', () => {
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.snoozeSchedule',
|
||||
},
|
||||
aggs: {
|
||||
nestedAggs1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.enabled',
|
||||
},
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
nestedAggs2: {
|
||||
terms: {
|
||||
field: 'alert.attributes.consumer',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation term: alert.attributes.consumer"`);
|
||||
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.snoozeSchedule',
|
||||
},
|
||||
aggs: {
|
||||
nestedAggs1: {
|
||||
terms: {
|
||||
field: 'alert.attributes.consumer',
|
||||
},
|
||||
},
|
||||
},
|
||||
aggregations: {
|
||||
nestedAggs2: {
|
||||
terms: {
|
||||
field: 'alert.attributes.enabled',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation term: alert.attributes.consumer"`);
|
||||
});
|
||||
|
||||
it('should throw for nested aggregations with invalid root level aggs types', () => {
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
cardinality: {
|
||||
field: 'alert.attributes.muteAll',
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation type: cardinality"`);
|
||||
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
aggs: {
|
||||
nestedAggs: {
|
||||
terms: {
|
||||
field: 'alert.attributes.executionStatus.status',
|
||||
},
|
||||
avg: {
|
||||
field: 'alert.attributes.executionStatus.status',
|
||||
},
|
||||
},
|
||||
},
|
||||
max: {
|
||||
field: 'alert.attributes.executionStatus.status',
|
||||
},
|
||||
cardinality: {
|
||||
field: 'alert.attributes.executionStatus.status',
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation type: max"`);
|
||||
});
|
||||
|
||||
it('should throw for invalid multi_terms aggregations', () => {
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
multi_terms: {
|
||||
terms: [{ field: 'foo.attributes.bar' }, { field: 'alert.attributes.apiKey' }],
|
||||
},
|
||||
aggs: {
|
||||
nestedAggs: {
|
||||
multi_terms: {
|
||||
terms: [{ field: 'foo.attributes.bar2' }, { field: 'foo.attributes.bar3' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation type: multi_terms"`);
|
||||
|
||||
expect(() => {
|
||||
validateRuleAggregationFields({
|
||||
name1: {
|
||||
multi_terms: {
|
||||
terms: [{ field: 'foo.attributes.bar' }, { field: 'foo.attributes.bar1' }],
|
||||
},
|
||||
aggs: {
|
||||
nestedAggs: {
|
||||
multi_terms: {
|
||||
terms: [{ field: 'alert.attributes.consumer' }, { field: 'foo.attributes.bar3' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}).toThrowErrorMatchingInlineSnapshot(`"Invalid aggregation type: multi_terms"`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
|
||||
const ALLOW_FIELDS = [
|
||||
'alert.attributes.executionStatus.status',
|
||||
'alert.attributes.lastRun.outcome',
|
||||
'alert.attributes.muteAll',
|
||||
'alert.attributes.tags',
|
||||
'alert.attributes.snoozeSchedule',
|
||||
'alert.attributes.snoozeSchedule.duration',
|
||||
'alert.attributes.alertTypeId',
|
||||
'alert.attributes.enabled',
|
||||
'alert.attributes.params.*',
|
||||
];
|
||||
|
||||
const ALLOW_AGG_TYPES = ['terms', 'composite', 'nested', 'filter'];
|
||||
|
||||
const AGG_TYPES_TO_VERIFY = ['field', 'path'];
|
||||
|
||||
const AGG_KEYS = ['aggs', 'aggregations'];
|
||||
|
||||
export const validateRuleAggregationFields = (
|
||||
aggs: Record<string, AggregationsAggregationContainer>
|
||||
) => {
|
||||
Object.values(aggs).forEach((aggContainer) => {
|
||||
// validate root level aggregation types (non aggs/aggregations)
|
||||
validateTypes(aggContainer);
|
||||
|
||||
// Recursively go through aggs to validate terms
|
||||
if (aggContainer.aggs) {
|
||||
validateRuleAggregationFields(aggContainer.aggs);
|
||||
}
|
||||
if (aggContainer.aggregations) {
|
||||
validateRuleAggregationFields(aggContainer.aggregations);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const validateTypes = (container: AggregationsAggregationContainer) => {
|
||||
Object.entries(container).forEach(([aggType, aggContainer]) => {
|
||||
// Do not try to validate aggs/aggregations, as the above function is already doing that
|
||||
if (AGG_KEYS.includes(aggType)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ALLOW_AGG_TYPES.includes(aggType)) {
|
||||
throw Boom.badRequest(`Invalid aggregation type: ${aggType}`);
|
||||
}
|
||||
|
||||
validateFields(aggContainer);
|
||||
});
|
||||
};
|
||||
|
||||
const validateFields = (container: AggregationsAggregationContainer) => {
|
||||
Object.entries(container).forEach(([aggType, aggContainer]) => {
|
||||
// Found field, check field against blocklist
|
||||
if (AGG_TYPES_TO_VERIFY.includes(aggType) && !ALLOW_FIELDS.includes(aggContainer)) {
|
||||
throw Boom.badRequest(`Invalid aggregation term: ${aggContainer}`);
|
||||
}
|
||||
|
||||
// Did not find anything, keep recursing if possible
|
||||
if (typeof aggContainer === 'object' && aggContainer !== null && !Array.isArray(aggContainer)) {
|
||||
validateFields(aggContainer);
|
||||
}
|
||||
});
|
||||
};
|
|
@ -6,90 +6,34 @@
|
|||
*/
|
||||
|
||||
import { KueryNode, nodeBuilder } from '@kbn/es-query';
|
||||
import { RawRule, RuleExecutionStatusValues, RuleLastRunOutcomeValues } from '../../types';
|
||||
import type { AggregationsAggregationContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { AlertingAuthorizationEntity } from '../../authorization';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
|
||||
import { buildKueryNodeFilter } from '../common';
|
||||
import { alertingAuthorizationFilterOpts } from '../common/constants';
|
||||
import { RulesClientContext } from '../types';
|
||||
import { RawRule, AggregateOptions } from '../../types';
|
||||
import { validateRuleAggregationFields } from '../lib/validate_rule_aggregation_fields';
|
||||
|
||||
export interface AggregateOptions extends IndexType {
|
||||
search?: string;
|
||||
defaultSearchOperator?: 'AND' | 'OR';
|
||||
searchFields?: string[];
|
||||
hasReference?: {
|
||||
type: string;
|
||||
id: string;
|
||||
};
|
||||
filter?: string | KueryNode;
|
||||
maxTags?: number;
|
||||
export interface AggregateParams<AggregationResult> {
|
||||
options?: AggregateOptions;
|
||||
aggs: Record<keyof AggregationResult, AggregationsAggregationContainer>;
|
||||
}
|
||||
|
||||
interface IndexType {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface AggregateResult {
|
||||
alertExecutionStatus: { [status: string]: number };
|
||||
ruleLastRunOutcome: { [status: string]: number };
|
||||
ruleEnabledStatus?: { enabled: number; disabled: number };
|
||||
ruleMutedStatus?: { muted: number; unmuted: number };
|
||||
ruleSnoozedStatus?: { snoozed: number };
|
||||
ruleTags?: string[];
|
||||
}
|
||||
|
||||
export interface RuleAggregation {
|
||||
status: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
outcome: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
muted: {
|
||||
buckets: Array<{
|
||||
key: number;
|
||||
key_as_string: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
enabled: {
|
||||
buckets: Array<{
|
||||
key: number;
|
||||
key_as_string: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
snoozed: {
|
||||
count: {
|
||||
doc_count: number;
|
||||
};
|
||||
};
|
||||
tags: {
|
||||
buckets: Array<{
|
||||
key: string;
|
||||
doc_count: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
export async function aggregate(
|
||||
export async function aggregate<T = Record<string, unknown>>(
|
||||
context: RulesClientContext,
|
||||
{
|
||||
options: { fields, filter, maxTags = 50, ...options } = {},
|
||||
}: { options?: AggregateOptions } = {}
|
||||
): Promise<AggregateResult> {
|
||||
params: AggregateParams<T>
|
||||
): Promise<T> {
|
||||
const { options = {}, aggs } = params;
|
||||
const { filter, page = 1, perPage = 0, ...restOptions } = options;
|
||||
|
||||
let authorizationTuple;
|
||||
try {
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter(
|
||||
AlertingAuthorizationEntity.Rule,
|
||||
alertingAuthorizationFilterOpts
|
||||
);
|
||||
validateRuleAggregationFields(aggs);
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
|
@ -103,124 +47,18 @@ export async function aggregate(
|
|||
const { filter: authorizationFilter } = authorizationTuple;
|
||||
const filterKueryNode = buildKueryNodeFilter(filter);
|
||||
|
||||
const resp = await context.unsecuredSavedObjectsClient.find<RawRule, RuleAggregation>({
|
||||
...options,
|
||||
const result = await context.unsecuredSavedObjectsClient.find<RawRule, T>({
|
||||
...restOptions,
|
||||
filter:
|
||||
authorizationFilter && filterKueryNode
|
||||
? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode])
|
||||
: authorizationFilter,
|
||||
page: 1,
|
||||
perPage: 0,
|
||||
page,
|
||||
perPage,
|
||||
type: 'alert',
|
||||
aggs: {
|
||||
status: {
|
||||
terms: { field: 'alert.attributes.executionStatus.status' },
|
||||
},
|
||||
outcome: {
|
||||
terms: { field: 'alert.attributes.lastRun.outcome' },
|
||||
},
|
||||
enabled: {
|
||||
terms: { field: 'alert.attributes.enabled' },
|
||||
},
|
||||
muted: {
|
||||
terms: { field: 'alert.attributes.muteAll' },
|
||||
},
|
||||
tags: {
|
||||
terms: { field: 'alert.attributes.tags', order: { _key: 'asc' }, size: maxTags },
|
||||
},
|
||||
snoozed: {
|
||||
nested: {
|
||||
path: 'alert.attributes.snoozeSchedule',
|
||||
},
|
||||
aggs: {
|
||||
count: {
|
||||
filter: {
|
||||
exists: {
|
||||
field: 'alert.attributes.snoozeSchedule.duration',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
aggs,
|
||||
});
|
||||
|
||||
if (!resp.aggregations) {
|
||||
// Return a placeholder with all zeroes
|
||||
const placeholder: AggregateResult = {
|
||||
alertExecutionStatus: {},
|
||||
ruleLastRunOutcome: {},
|
||||
ruleEnabledStatus: {
|
||||
enabled: 0,
|
||||
disabled: 0,
|
||||
},
|
||||
ruleMutedStatus: {
|
||||
muted: 0,
|
||||
unmuted: 0,
|
||||
},
|
||||
ruleSnoozedStatus: { snoozed: 0 },
|
||||
};
|
||||
|
||||
for (const key of RuleExecutionStatusValues) {
|
||||
placeholder.alertExecutionStatus[key] = 0;
|
||||
}
|
||||
|
||||
return placeholder;
|
||||
}
|
||||
|
||||
const alertExecutionStatus = resp.aggregations.status.buckets.map(
|
||||
({ key, doc_count: docCount }) => ({
|
||||
[key]: docCount,
|
||||
})
|
||||
);
|
||||
|
||||
const ruleLastRunOutcome = resp.aggregations.outcome.buckets.map(
|
||||
({ key, doc_count: docCount }) => ({
|
||||
[key]: docCount,
|
||||
})
|
||||
);
|
||||
|
||||
const ret: AggregateResult = {
|
||||
alertExecutionStatus: alertExecutionStatus.reduce(
|
||||
(acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
|
||||
{}
|
||||
),
|
||||
ruleLastRunOutcome: ruleLastRunOutcome.reduce(
|
||||
(acc, curr: { [status: string]: number }) => Object.assign(acc, curr),
|
||||
{}
|
||||
),
|
||||
};
|
||||
|
||||
// Fill missing keys with zeroes
|
||||
for (const key of RuleExecutionStatusValues) {
|
||||
if (!ret.alertExecutionStatus.hasOwnProperty(key)) {
|
||||
ret.alertExecutionStatus[key] = 0;
|
||||
}
|
||||
}
|
||||
for (const key of RuleLastRunOutcomeValues) {
|
||||
if (!ret.ruleLastRunOutcome.hasOwnProperty(key)) {
|
||||
ret.ruleLastRunOutcome[key] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const enabledBuckets = resp.aggregations.enabled.buckets;
|
||||
ret.ruleEnabledStatus = {
|
||||
enabled: enabledBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
|
||||
disabled: enabledBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
|
||||
};
|
||||
|
||||
const mutedBuckets = resp.aggregations.muted.buckets;
|
||||
ret.ruleMutedStatus = {
|
||||
muted: mutedBuckets.find((bucket) => bucket.key === 1)?.doc_count ?? 0,
|
||||
unmuted: mutedBuckets.find((bucket) => bucket.key === 0)?.doc_count ?? 0,
|
||||
};
|
||||
|
||||
ret.ruleSnoozedStatus = {
|
||||
snoozed: resp.aggregations.snoozed?.count?.doc_count ?? 0,
|
||||
};
|
||||
|
||||
const tagsBuckets = resp.aggregations.tags?.buckets || [];
|
||||
ret.ruleTags = tagsBuckets.map((bucket) => bucket.key);
|
||||
|
||||
return ret;
|
||||
// params.
|
||||
return result.aggregations!;
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ import {
|
|||
GetRuleExecutionKPIParams,
|
||||
} from './methods/get_execution_kpi';
|
||||
import { find, FindParams } from './methods/find';
|
||||
import { aggregate, AggregateOptions } from './methods/aggregate';
|
||||
import { aggregate, AggregateParams } from './methods/aggregate';
|
||||
import { deleteRule } from './methods/delete';
|
||||
import { update, UpdateOptions } from './methods/update';
|
||||
import { bulkDeleteRules } from './methods/bulk_delete';
|
||||
|
@ -77,7 +77,8 @@ export class RulesClient {
|
|||
};
|
||||
}
|
||||
|
||||
public aggregate = (params?: { options?: AggregateOptions }) => aggregate(this.context, params);
|
||||
public aggregate = <T = Record<string, unknown>>(params: AggregateParams<T>): Promise<T> =>
|
||||
aggregate<T>(this.context, params);
|
||||
public clone = <Params extends RuleTypeParams = never>(...args: CloneArguments) =>
|
||||
clone<Params>(this.context, ...args);
|
||||
public create = <Params extends RuleTypeParams = never>(params: CreateOptions<Params>) =>
|
||||
|
|
|
@ -16,7 +16,11 @@ import { AlertingAuthorization } from '../../authorization/alerting_authorizatio
|
|||
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { RecoveredActionGroup } from '../../../common';
|
||||
import {
|
||||
RecoveredActionGroup,
|
||||
getDefaultRuleAggregation,
|
||||
DefaultRuleAggregationResult,
|
||||
} from '../../../common';
|
||||
import { RegistryRuleType } from '../../rule_type_registry';
|
||||
import { fromKueryExpression, nodeTypes } from '@kbn/es-query';
|
||||
|
||||
|
@ -157,38 +161,107 @@ describe('aggregate()', () => {
|
|||
|
||||
test('calls saved objects client with given params to perform aggregation', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
const result = await rulesClient.aggregate({ options: {} });
|
||||
const result = await rulesClient.aggregate<DefaultRuleAggregationResult>({
|
||||
options: {},
|
||||
aggs: getDefaultRuleAggregation(),
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"alertExecutionStatus": Object {
|
||||
"active": 8,
|
||||
"error": 6,
|
||||
"ok": 10,
|
||||
"pending": 4,
|
||||
"unknown": 2,
|
||||
"warning": 1,
|
||||
"enabled": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 2,
|
||||
"key": 0,
|
||||
"key_as_string": "0",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 28,
|
||||
"key": 1,
|
||||
"key_as_string": "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
"ruleEnabledStatus": Object {
|
||||
"disabled": 2,
|
||||
"enabled": 28,
|
||||
"muted": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 27,
|
||||
"key": 0,
|
||||
"key_as_string": "0",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 3,
|
||||
"key": 1,
|
||||
"key_as_string": "1",
|
||||
},
|
||||
],
|
||||
},
|
||||
"ruleLastRunOutcome": Object {
|
||||
"failed": 4,
|
||||
"succeeded": 2,
|
||||
"warning": 6,
|
||||
"outcome": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 2,
|
||||
"key": "succeeded",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 4,
|
||||
"key": "failed",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 6,
|
||||
"key": "warning",
|
||||
},
|
||||
],
|
||||
},
|
||||
"ruleMutedStatus": Object {
|
||||
"muted": 3,
|
||||
"unmuted": 27,
|
||||
"snoozed": Object {
|
||||
"count": Object {
|
||||
"doc_count": 0,
|
||||
},
|
||||
"doc_count": 0,
|
||||
},
|
||||
"ruleSnoozedStatus": Object {
|
||||
"snoozed": 0,
|
||||
"status": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 8,
|
||||
"key": "active",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 6,
|
||||
"key": "error",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 10,
|
||||
"key": "ok",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 4,
|
||||
"key": "pending",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 2,
|
||||
"key": "unknown",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 1,
|
||||
"key": "warning",
|
||||
},
|
||||
],
|
||||
},
|
||||
"tags": Object {
|
||||
"buckets": Array [
|
||||
Object {
|
||||
"doc_count": 10,
|
||||
"key": "a",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 20,
|
||||
"key": "b",
|
||||
},
|
||||
Object {
|
||||
"doc_count": 30,
|
||||
"key": "c",
|
||||
},
|
||||
],
|
||||
},
|
||||
"ruleTags": Array [
|
||||
"a",
|
||||
"b",
|
||||
"c",
|
||||
],
|
||||
}
|
||||
`);
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
|
@ -244,7 +317,10 @@ describe('aggregate()', () => {
|
|||
});
|
||||
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
await rulesClient.aggregate({ options: { filter: 'foo: someTerm' } });
|
||||
await rulesClient.aggregate({
|
||||
options: { filter: 'foo: someTerm' },
|
||||
aggs: getDefaultRuleAggregation(),
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toEqual([
|
||||
|
@ -296,7 +372,7 @@ describe('aggregate()', () => {
|
|||
const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger });
|
||||
authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized'));
|
||||
|
||||
await expect(rulesClient.aggregate()).rejects.toThrow();
|
||||
await expect(rulesClient.aggregate({ aggs: getDefaultRuleAggregation() })).rejects.toThrow();
|
||||
expect(auditLogger.log).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
event: expect.objectContaining({
|
||||
|
@ -315,7 +391,7 @@ describe('aggregate()', () => {
|
|||
test('sets to default (50) if it is not provided', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
|
||||
await rulesClient.aggregate();
|
||||
await rulesClient.aggregate({ aggs: getDefaultRuleAggregation() });
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([
|
||||
{
|
||||
|
@ -331,7 +407,9 @@ describe('aggregate()', () => {
|
|||
test('sets to the provided value', async () => {
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
|
||||
await rulesClient.aggregate({ options: { maxTags: 1000 } });
|
||||
await rulesClient.aggregate({
|
||||
aggs: getDefaultRuleAggregation({ maxTags: 1000 }),
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find.mock.calls[0]).toMatchObject([
|
||||
{
|
||||
|
|
|
@ -37,7 +37,6 @@ export type {
|
|||
export type { CreateOptions } from './methods/create';
|
||||
export type { FindOptions, FindResult } from './methods/find';
|
||||
export type { UpdateOptions } from './methods/update';
|
||||
export type { AggregateOptions, AggregateResult } from './methods/aggregate';
|
||||
export type { GetAlertSummaryParams } from './methods/get_alert_summary';
|
||||
export type {
|
||||
GetExecutionLogByIdParams,
|
||||
|
|
|
@ -14,6 +14,12 @@ import {
|
|||
} from '../../../../routes/__mocks__/request_responses';
|
||||
import { requestContextMock, serverMock } from '../../../../routes/__mocks__';
|
||||
|
||||
const emptyTagAggregationResult = {
|
||||
tags: {
|
||||
buckets: [],
|
||||
},
|
||||
};
|
||||
|
||||
describe('Rule management filters route', () => {
|
||||
let server: ReturnType<typeof serverMock.create>;
|
||||
let { clients, context } = requestContextMock.createTools();
|
||||
|
@ -30,6 +36,7 @@ describe('Rule management filters route', () => {
|
|||
|
||||
describe('status codes', () => {
|
||||
test('returns 200', async () => {
|
||||
clients.rulesClient.aggregate.mockResolvedValue(emptyTagAggregationResult);
|
||||
const response = await server.inject(
|
||||
getRuleManagementFiltersRequest(),
|
||||
requestContextMock.convertContext(context)
|
||||
|
@ -41,6 +48,7 @@ describe('Rule management filters route', () => {
|
|||
clients.rulesClient.find.mockImplementation(async () => {
|
||||
throw new Error('Test error');
|
||||
});
|
||||
clients.rulesClient.aggregate.mockResolvedValue(emptyTagAggregationResult);
|
||||
const response = await server.inject(
|
||||
getRuleManagementFiltersRequest(),
|
||||
requestContextMock.convertContext(context)
|
||||
|
@ -57,9 +65,34 @@ describe('Rule management filters route', () => {
|
|||
test('1 rule installed, 1 custom rule and 3 tags', async () => {
|
||||
clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit());
|
||||
clients.rulesClient.aggregate.mockResolvedValue({
|
||||
alertExecutionStatus: {},
|
||||
ruleLastRunOutcome: {},
|
||||
ruleTags: ['a', 'b', 'c'],
|
||||
status: {
|
||||
buckets: [],
|
||||
},
|
||||
outcome: {
|
||||
buckets: [],
|
||||
},
|
||||
tags: {
|
||||
buckets: [
|
||||
{
|
||||
key: {
|
||||
tags: 'a',
|
||||
},
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
tags: 'b',
|
||||
},
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
tags: 'c',
|
||||
},
|
||||
doc_count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const request = getRuleManagementFiltersRequest();
|
||||
const response = await server.inject(request, requestContextMock.convertContext(context));
|
||||
|
|
|
@ -17,9 +17,34 @@ describe('read_tags', () => {
|
|||
test('it should return tags from the aggregation', async () => {
|
||||
const rulesClient = rulesClientMock.create();
|
||||
rulesClient.aggregate.mockResolvedValue({
|
||||
alertExecutionStatus: {},
|
||||
ruleLastRunOutcome: {},
|
||||
ruleTags: ['tag 1', 'tag 2', 'tag 3', 'tag 4'],
|
||||
tags: {
|
||||
buckets: [
|
||||
{
|
||||
key: {
|
||||
tags: 'tag 1',
|
||||
},
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
tags: 'tag 2',
|
||||
},
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
tags: 'tag 3',
|
||||
},
|
||||
doc_count: 1,
|
||||
},
|
||||
{
|
||||
key: {
|
||||
tags: 'tag 4',
|
||||
},
|
||||
doc_count: 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const tags = await readTags({ rulesClient });
|
||||
|
|
|
@ -6,6 +6,11 @@
|
|||
*/
|
||||
|
||||
import type { RulesClient } from '@kbn/alerting-plugin/server';
|
||||
import type { RuleTagsAggregationResult } from '@kbn/alerting-plugin/common';
|
||||
import {
|
||||
getRuleTagsAggregation,
|
||||
formatRuleTagsAggregationResult,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { enrichFilterWithRuleTypeMapping } from '../../../logic/search/enrich_filter_with_rule_type_mappings';
|
||||
|
||||
// This is a contrived max limit on the number of tags. In fact it can exceed this number and will be truncated to the hardcoded number.
|
||||
|
@ -17,13 +22,14 @@ export const readTags = async ({
|
|||
rulesClient: RulesClient;
|
||||
perPage?: number;
|
||||
}): Promise<string[]> => {
|
||||
const res = await rulesClient.aggregate({
|
||||
const res = await rulesClient.aggregate<RuleTagsAggregationResult>({
|
||||
options: {
|
||||
fields: ['tags'],
|
||||
filter: enrichFilterWithRuleTypeMapping(undefined),
|
||||
maxTags: EXPECTED_MAX_TAGS,
|
||||
},
|
||||
aggs: getRuleTagsAggregation({
|
||||
maxTags: EXPECTED_MAX_TAGS,
|
||||
}),
|
||||
});
|
||||
|
||||
return res.ruleTags ?? [];
|
||||
return formatRuleTagsAggregationResult(res).ruleTags;
|
||||
};
|
||||
|
|
|
@ -6,19 +6,21 @@
|
|||
*/
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { AsApiContract } from '@kbn/actions-plugin/common';
|
||||
import { RuleAggregations } from '../../../types';
|
||||
import {
|
||||
RuleAggregationFormattedResult,
|
||||
RuleTagsAggregationFormattedResult,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
|
||||
import { mapFiltersToKql } from './map_filters_to_kql';
|
||||
import {
|
||||
LoadRuleAggregationsProps,
|
||||
rewriteBodyRes,
|
||||
rewriteTagsBodyRes,
|
||||
RuleTagsAggregations,
|
||||
} from './aggregate_helpers';
|
||||
import { LoadRuleAggregationsProps, rewriteBodyRes, rewriteTagsBodyRes } from './aggregate_helpers';
|
||||
|
||||
// TODO: https://github.com/elastic/kibana/issues/131682
|
||||
export async function loadRuleTags({ http }: { http: HttpSetup }): Promise<RuleTagsAggregations> {
|
||||
const res = await http.get<AsApiContract<RuleAggregations>>(
|
||||
export async function loadRuleTags({
|
||||
http,
|
||||
}: {
|
||||
http: HttpSetup;
|
||||
}): Promise<RuleTagsAggregationFormattedResult> {
|
||||
const res = await http.get<AsApiContract<RuleTagsAggregationFormattedResult>>(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`
|
||||
);
|
||||
return rewriteTagsBodyRes(res);
|
||||
|
@ -32,7 +34,7 @@ export async function loadRuleAggregations({
|
|||
ruleExecutionStatusesFilter,
|
||||
ruleStatusesFilter,
|
||||
tagsFilter,
|
||||
}: LoadRuleAggregationsProps): Promise<RuleAggregations> {
|
||||
}: LoadRuleAggregationsProps): Promise<RuleAggregationFormattedResult> {
|
||||
const filters = mapFiltersToKql({
|
||||
typesFilter,
|
||||
actionTypesFilter,
|
||||
|
@ -40,7 +42,7 @@ export async function loadRuleAggregations({
|
|||
ruleStatusesFilter,
|
||||
tagsFilter,
|
||||
});
|
||||
const res = await http.post<AsApiContract<RuleAggregations>>(
|
||||
const res = await http.post<AsApiContract<RuleAggregationFormattedResult>>(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
|
|
|
@ -7,13 +7,13 @@
|
|||
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { RewriteRequestCase } from '@kbn/actions-plugin/common';
|
||||
import { RuleAggregations, RuleStatus } from '../../../types';
|
||||
import {
|
||||
RuleAggregationFormattedResult,
|
||||
RuleTagsAggregationFormattedResult,
|
||||
} from '@kbn/alerting-plugin/common';
|
||||
import { RuleStatus } from '../../../types';
|
||||
|
||||
export interface RuleTagsAggregations {
|
||||
ruleTags: string[];
|
||||
}
|
||||
|
||||
export const rewriteBodyRes: RewriteRequestCase<RuleAggregations> = ({
|
||||
export const rewriteBodyRes: RewriteRequestCase<RuleAggregationFormattedResult> = ({
|
||||
rule_execution_status: ruleExecutionStatus,
|
||||
rule_last_run_outcome: ruleLastRunOutcome,
|
||||
rule_enabled_status: ruleEnabledStatus,
|
||||
|
@ -31,7 +31,7 @@ export const rewriteBodyRes: RewriteRequestCase<RuleAggregations> = ({
|
|||
ruleTags,
|
||||
});
|
||||
|
||||
export const rewriteTagsBodyRes: RewriteRequestCase<RuleTagsAggregations> = ({
|
||||
export const rewriteTagsBodyRes: RewriteRequestCase<RuleTagsAggregationFormattedResult> = ({
|
||||
rule_tags: ruleTags,
|
||||
}: any) => ({
|
||||
ruleTags,
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import { AsApiContract } from '@kbn/actions-plugin/common';
|
||||
import { RuleAggregations } from '../../../types';
|
||||
import { RuleAggregationFormattedResult } from '@kbn/alerting-plugin/common';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
|
||||
import { LoadRuleAggregationsProps, rewriteBodyRes } from './aggregate_helpers';
|
||||
import { mapFiltersToKueryNode } from './map_filters_to_kuery_node';
|
||||
|
@ -18,7 +18,7 @@ export async function loadRuleAggregationsWithKueryFilter({
|
|||
ruleExecutionStatusesFilter,
|
||||
ruleStatusesFilter,
|
||||
tagsFilter,
|
||||
}: LoadRuleAggregationsProps): Promise<RuleAggregations> {
|
||||
}: LoadRuleAggregationsProps): Promise<RuleAggregationFormattedResult> {
|
||||
const filtersKueryNode = mapFiltersToKueryNode({
|
||||
typesFilter,
|
||||
actionTypesFilter,
|
||||
|
@ -28,7 +28,7 @@ export async function loadRuleAggregationsWithKueryFilter({
|
|||
searchText,
|
||||
});
|
||||
|
||||
const res = await http.post<AsApiContract<RuleAggregations>>(
|
||||
const res = await http.post<AsApiContract<RuleAggregationFormattedResult>>(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/rules/_aggregate`,
|
||||
{
|
||||
body: JSON.stringify({
|
||||
|
|
|
@ -41,7 +41,6 @@ import {
|
|||
SanitizedRule as AlertingSanitizedRule,
|
||||
ResolvedSanitizedRule,
|
||||
RuleAction,
|
||||
RuleAggregations as AlertingRuleAggregations,
|
||||
RuleTaskState,
|
||||
AlertSummary as RuleSummary,
|
||||
ExecutionDuration,
|
||||
|
@ -103,14 +102,10 @@ type Rule<Params extends RuleTypeParams = RuleTypeParams> = SanitizedRule<Params
|
|||
type ResolvedRule = Omit<ResolvedSanitizedRule<RuleTypeParams>, 'alertTypeId'> & {
|
||||
ruleTypeId: ResolvedSanitizedRule['alertTypeId'];
|
||||
};
|
||||
type RuleAggregations = Omit<AlertingRuleAggregations, 'alertExecutionStatus'> & {
|
||||
ruleExecutionStatus: AlertingRuleAggregations['alertExecutionStatus'];
|
||||
};
|
||||
|
||||
export type {
|
||||
Rule,
|
||||
RuleAction,
|
||||
RuleAggregations,
|
||||
RuleTaskState,
|
||||
RuleSummary,
|
||||
ExecutionDuration,
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { Spaces } from '../../../scenarios';
|
||||
import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
const tags = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createAggregateTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
|
||||
const createRule = async (overrides = {}) => {
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(getTestRuleData(overrides))
|
||||
.expect(200);
|
||||
|
||||
return createdRule.id;
|
||||
};
|
||||
|
||||
describe('getRuleTags', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
afterEach(() => objectRemover.removeAll());
|
||||
|
||||
it('should get rule tags when there are no rules', async () => {
|
||||
const response = await supertest.get(
|
||||
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_tags`
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
expect(response.body.rule_tags.filter((tag: string) => tag !== 'foo')).to.eql([]);
|
||||
});
|
||||
|
||||
it('should get rule tags from all rules', async () => {
|
||||
await Promise.all(
|
||||
tags.map(async (tag) => {
|
||||
const ruleId = await createRule({ tags: [tag] });
|
||||
objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting');
|
||||
})
|
||||
);
|
||||
|
||||
const ruleId = await createRule({ tags: ['a', 'b', 'c'] });
|
||||
objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting');
|
||||
|
||||
const response = await supertest.get(
|
||||
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_tags`
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
expect(response.body.rule_tags.filter((tag: string) => tag !== 'foo').sort()).to.eql(
|
||||
tags.sort()
|
||||
);
|
||||
});
|
||||
|
||||
it('should paginate rule tags', async () => {
|
||||
await Promise.all(
|
||||
tags.map(async (tag) => {
|
||||
const ruleId = await createRule({ tags: [tag] });
|
||||
objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting');
|
||||
})
|
||||
);
|
||||
|
||||
const ruleId = await createRule({ tags: ['foo'] });
|
||||
objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting');
|
||||
|
||||
const response = await supertest.get(
|
||||
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_tags?max_tags=5`
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
expect(response.body.rule_tags).to.eql(tags.sort().slice(0, 5));
|
||||
|
||||
const paginatedResponse = await supertest.get(
|
||||
`${getUrlPrefix(
|
||||
Spaces.space1.id
|
||||
)}/internal/alerting/rules/_tags?max_tags=5&after=${JSON.stringify({
|
||||
tags: 'e',
|
||||
})}`
|
||||
);
|
||||
|
||||
expect(paginatedResponse.status).to.eql(200);
|
||||
expect(paginatedResponse.body.rule_tags).to.eql(['f', 'foo', 'g', 'h', 'i']);
|
||||
});
|
||||
|
||||
it('should search rule tags', async () => {
|
||||
await Promise.all(
|
||||
tags.map(async (tag) => {
|
||||
const ruleId = await createRule({ tags: [tag] });
|
||||
objectRemover.add(Spaces.space1.id, ruleId, 'rule', 'alerting');
|
||||
})
|
||||
);
|
||||
|
||||
const response = await supertest.get(
|
||||
`${getUrlPrefix(Spaces.space1.id)}/internal/alerting/rules/_tags?search=a`
|
||||
);
|
||||
|
||||
expect(response.body.rule_tags.filter((tag: string) => tag !== 'foo')).to.eql(['a']);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -26,6 +26,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
|
|||
loadTestFile(require.resolve('./get_alert_summary'));
|
||||
loadTestFile(require.resolve('./get_execution_log'));
|
||||
loadTestFile(require.resolve('./get_action_error_log'));
|
||||
loadTestFile(require.resolve('./get_rule_tags'));
|
||||
loadTestFile(require.resolve('./rule_types'));
|
||||
loadTestFile(require.resolve('./event_log'));
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue