Add global execution summary endpoint to alerting API

This commit is contained in:
Edgar Santos 2025-03-28 19:03:50 +01:00
parent bf7389f515
commit 719374c501
20 changed files with 712 additions and 2 deletions

View file

@ -38,6 +38,18 @@ export const EMPTY_EXECUTION_KPI_RESULT = {
triggeredActions: 0,
};
export const EMPTY_EXECUTION_SUMMARY_RESULT = {
executions: {
total: 0,
success: 0,
},
latestExecutionSummary: {
success: 0,
failure: 0,
warning: 0,
},
};
export type ExecutionLogSortFields = (typeof executionLogSortableColumns)[number];
export type ActionErrorLogSortFields = (typeof actionErrorLogSortableColumns)[number];

View file

@ -149,6 +149,7 @@ export {
executionLogSortableColumns,
actionErrorLogSortableColumns,
EMPTY_EXECUTION_KPI_RESULT,
EMPTY_EXECUTION_SUMMARY_RESULT,
} from './execution_log_types';
export type { RuleSnoozeSchedule, RuleSnooze } from './rule_snooze_type';
export type { RRuleParams, RRuleRecord } from './rrule_type';
@ -245,6 +246,9 @@ export const INTERNAL_ALERTING_GAPS_GET_SUMMARY_BY_RULE_IDS_API_PATH =
export const INTERNAL_ALERTING_GAPS_FILL_BY_ID_API_PATH =
`${INTERNAL_ALERTING_GAPS_API_PATH}/_fill_by_id` as const;
export const INTERNAL_ALERTING_GET_GLOBAL_RULE_EXECUTION_SUMMARY_API_PATH =
`${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_summary` as const;
// External
export const ARCHIVE_MAINTENANCE_WINDOW_API_PATH = `${BASE_MAINTENANCE_WINDOW_API_PATH}/{id}/_archive`;
export const UNARCHIVE_MAINTENANCE_WINDOW_API_PATH = `${BASE_MAINTENANCE_WINDOW_API_PATH}/{id}/_unarchive`;

View file

@ -0,0 +1,27 @@
/*
* 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.
*/
export {
getGlobalExecutionSummarySchema,
getGlobalExecutionSummaryResponseBodySchema,
} from './schemas/latest';
export type {
GetGlobalExecutionSummary,
GetGlobalExecutionSummaryResponseBody,
GetGlobalExecutionSummaryResponse,
} from './types/latest';
export {
getGlobalExecutionSummarySchema as getGlobalExecutionSummarySchemaV1,
getGlobalExecutionSummaryResponseBodySchema as getGlobalExecutionSummaryResponseBodySchemaV1,
} from './schemas/v1';
export type {
GetGlobalExecutionSummary as GetGlobalExecutionSummaryV1,
GetGlobalExecutionSummaryResponse as GetGlobalExecutionSummaryResponseV1,
GetGlobalExecutionSummaryResponseBody as GetGlobalExecutionSummaryResponseBodyV1,
} from './types/v1';

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,45 @@
/*
* 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 { schema } from '@kbn/config-schema';
export const getGlobalExecutionSummarySchema = schema.object(
{
date_start: schema.string(),
date_end: schema.string(),
},
{
validate({ date_start: start, date_end: end }) {
const parsedStart = Date.parse(start);
if (isNaN(parsedStart)) {
return `[start]: query start must be valid date`;
}
const parsedEnd = Date.parse(end);
if (isNaN(parsedEnd)) {
return `[end]: query end must be valid date`;
}
if (parsedStart >= parsedEnd) {
return `[start]: query start must be before end`;
}
},
}
);
const positiveNumber = schema.number({ min: 0 });
export const getGlobalExecutionSummaryResponseBodySchema = schema.object({
executions: schema.object({
total: positiveNumber,
success: positiveNumber,
}),
latestExecutionSummary: schema.object({
success: positiveNumber,
failure: positiveNumber,
warning: positiveNumber,
}),
});

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './v1';

View file

@ -0,0 +1,21 @@
/*
* 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 { TypeOf } from '@kbn/config-schema';
import type {
getGlobalExecutionSummarySchema,
getGlobalExecutionSummaryResponseBodySchema,
} from '..';
export type GetGlobalExecutionSummary = TypeOf<typeof getGlobalExecutionSummarySchema>;
export type GetGlobalExecutionSummaryResponseBody = TypeOf<
typeof getGlobalExecutionSummaryResponseBodySchema
>;
export interface GetGlobalExecutionSummaryResponse {
body: GetGlobalExecutionSummaryResponseBody;
}

View file

@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getExecutionSummaryAggregation should correctly generate the aggregation 1`] = `
Object {
"executionsCount": Object {
"filter": Object {
"exists": Object {
"field": "event.outcome",
},
},
},
"latestExecutionOutcome": Object {
"aggs": Object {
"by_rule_id": Object {
"aggs": Object {
"latest_execution": Object {
"top_hits": Object {
"_source": Object {
"includes": Array [
"event.outcome",
],
},
"size": 1,
"sort": Array [
Object {
"@timestamp": Object {
"order": "desc",
},
},
],
},
},
},
"terms": Object {
"field": "rule.id",
"size": 10000,
},
},
},
"filter": Object {
"exists": Object {
"field": "event.outcome",
},
},
},
"successfulExecutionsCount": Object {
"filter": Object {
"term": Object {
"event.outcome": "success",
},
},
},
}
`;

View file

@ -16,6 +16,8 @@ import {
formatSortForTermSort,
getExecutionKPIAggregation,
formatExecutionKPIResult,
getExecutionSummaryAggregation,
formatExecutionSummaryResult,
} from './get_execution_log_aggregation';
describe('formatSortForBucketSort', () => {
@ -2979,3 +2981,70 @@ describe('formatExecutionKPIAggBuckets', () => {
);
});
});
describe('getExecutionSummaryAggregation', () => {
test('should correctly generate the aggregation', () => {
expect(getExecutionSummaryAggregation()).toMatchSnapshot();
});
});
describe('formatExecutionSummary', () => {
it('should format the latest execution summary correctly', () => {
const results = {
aggregations: {
executionsCount: { doc_count: 18 },
latestExecutionOutcome: {
doc_count: 18,
by_rule_id: {
buckets: [
{
key: '89b2e1a0-1282-4601-97e4-2a3b1a2ef43b',
doc_count: 12,
latest_execution: {
hits: {
total: { value: 12, relation: 'eq' },
hits: [
{
_source: { event: { outcome: 'success' } },
sort: [1742893715888],
},
],
},
},
},
{
key: 'e1e5eddc-5251-4bb4-8aa4-a76943dd82e1',
doc_count: 6,
latest_execution: {
hits: {
total: { value: 6, relation: 'eq' },
hits: [
{
_source: { event: { outcome: 'failure' } },
sort: [1742893718878],
},
],
},
},
},
],
},
},
successfulExecutionsCount: { doc_count: 17 },
},
hits: {
total: { value: 36, relation: 'eq' },
hits: [],
} as estypes.SearchHitsMetadata<unknown>,
};
expect(formatExecutionSummaryResult(results)).toEqual({
executions: { total: 18, success: 17 },
latestExecutionSummary: {
success: 1,
failure: 1,
warning: 0,
},
});
});
});

View file

@ -12,9 +12,10 @@ import Boom from '@hapi/boom';
import { flatMap, get, isEmpty } from 'lodash';
import type { AggregateEventsBySavedObjectResult } from '@kbn/event-log-plugin/server';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { TIMESTAMP } from '@kbn/rule-data-utils';
import { parseDuration } from '.';
import type { IExecutionLog, IExecutionLogResult } from '../../common';
import { EMPTY_EXECUTION_KPI_RESULT } from '../../common';
import { EMPTY_EXECUTION_KPI_RESULT, EMPTY_EXECUTION_SUMMARY_RESULT } from '../../common';
const DEFAULT_MAX_BUCKETS_LIMIT = 10000; // do not retrieve more than this number of executions. UI limits 1000 to display, but we need to fetch all 10000 to accurately reflect the KPIs
const DEFAULT_MAX_KPI_BUCKETS_LIMIT = 10000;
@ -94,6 +95,30 @@ interface IExecutionUuidAggBucket extends estypes.AggregationsStringTermsBucketK
};
}
interface ILatestExecutionOutcomeAggBucket extends estypes.AggregationsStringTermsBucketKeys {
latest_execution: {
hits: estypes.SearchHitsMetadata<{
event: {
outcome: string;
};
}>;
};
}
interface IExecutionsCount {
doc_count: number;
}
interface ISuccessfulExecutionsCount {
doc_count: number;
}
interface LatestExecutionOutcomeAggResult extends estypes.AggregationsAggregateBase {
by_rule_id: {
buckets: ILatestExecutionOutcomeAggBucket[];
};
}
export interface ExecutionUuidAggResult<TBucket = IExecutionUuidAggBucket>
extends estypes.AggregationsAggregateBase {
buckets: TBucket[];
@ -462,6 +487,55 @@ export function getExecutionLogAggregation({
};
}
export const getExecutionSummaryAggregation = () => ({
executionsCount: {
filter: {
exists: {
field: OUTCOME_FIELD,
},
},
},
successfulExecutionsCount: {
filter: {
term: {
[OUTCOME_FIELD]: 'success',
},
},
},
latestExecutionOutcome: {
filter: {
exists: {
field: OUTCOME_FIELD,
},
},
aggs: {
by_rule_id: {
terms: {
field: RULE_ID_FIELD,
size: DEFAULT_MAX_BUCKETS_LIMIT,
},
aggs: {
latest_execution: {
top_hits: {
sort: [
{
[TIMESTAMP]: {
order: 'desc' as estypes.SortOrder,
},
},
],
_source: {
includes: [OUTCOME_FIELD],
},
size: 1,
},
},
},
},
},
},
});
function buildDslFilterQuery(filter: IExecutionLogAggOptions['filter']) {
try {
const filterKueryNode = typeof filter === 'string' ? fromKueryExpression(filter) : filter;
@ -554,6 +628,24 @@ function formatExecutionLogAggBucket(bucket: IExecutionUuidAggBucket): IExecutio
};
}
function formatLatestExecutionSummary(buckets: ILatestExecutionOutcomeAggBucket[]) {
const summary = {
success: 0,
failure: 0,
warning: 0,
};
buckets.forEach((bucket) => {
const latestExecution = bucket.latest_execution.hits.hits[0]._source;
const outcome = latestExecution?.event?.outcome ?? '';
if (Object.keys(summary).includes(outcome)) {
const outcomeLabel = outcome as keyof typeof summary;
summary[outcomeLabel]++;
}
});
return summary;
}
function formatExecutionKPIAggBuckets(buckets: IExecutionUuidKpiAggBucket[]) {
const objToReturn = {
success: 0,
@ -641,6 +733,29 @@ export function formatExecutionKPIResult(results: AggregateEventsBySavedObjectRe
return formatExecutionKPIAggBuckets(buckets);
}
export function formatExecutionSummaryResult(results: AggregateEventsBySavedObjectResult) {
const { aggregations } = results;
if (!aggregations) {
return EMPTY_EXECUTION_SUMMARY_RESULT;
}
const executionsCountAgg = aggregations.executionsCount as IExecutionsCount;
const successfulExecutionsCountAgg =
aggregations.successfulExecutionsCount as ISuccessfulExecutionsCount;
const latestExecutionSummaryResults =
aggregations.latestExecutionOutcome as LatestExecutionOutcomeAggResult;
return {
executions: {
total: executionsCountAgg.doc_count,
success: successfulExecutionsCountAgg.doc_count,
},
latestExecutionSummary: formatLatestExecutionSummary(
latestExecutionSummaryResults.by_rule_id.buckets
),
};
}
export function formatExecutionLogResult(
results: AggregateEventsBySavedObjectResult
): IExecutionLogResult {

View file

@ -0,0 +1,72 @@
/*
* 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 { mockHandlerArguments } from './_mock_handler_arguments';
import { rulesClientMock } from '../rules_client.mock';
import { getGlobalExecutionSummaryRoute } from './get_global_execution_summary';
const rulesClient = rulesClientMock.create();
jest.mock('../lib/license_api_access', () => ({
verifyApiAccess: jest.fn(),
}));
beforeEach(() => {
jest.resetAllMocks();
});
describe('getGlobalExecutionSummaryRoute', () => {
const dateStart = new Date().toISOString();
const dateEnd = new Date(Date.now() + 60 * 1000).toISOString();
const mockedSummary = {
executions: {
total: 18,
success: 18,
},
latestExecutionSummary: {
success: 2,
failure: 0,
warning: 0,
},
};
it('gets global execution summary', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
getGlobalExecutionSummaryRoute(router, licenseState);
const [config, handler] = router.get.mock.calls[0];
expect(config.path).toMatchInlineSnapshot(`"/internal/alerting/_global_execution_summary"`);
rulesClient.getGlobalExecutionSummaryWithAuth.mockResolvedValue(mockedSummary);
const [context, req, res] = mockHandlerArguments(
{ rulesClient },
{
query: {
date_start: dateStart,
date_end: dateEnd,
},
},
['ok']
);
await handler(context, req, res);
expect(rulesClient.getGlobalExecutionSummaryWithAuth).toHaveBeenCalledTimes(1);
expect(rulesClient.getGlobalExecutionSummaryWithAuth.mock.calls[0]).toEqual([
{
dateStart,
dateEnd,
},
]);
expect(res.ok).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,52 @@
/*
* 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 { IRouter } from '@kbn/core/server';
import type { GetGlobalExecutionSummaryResponseV1 } from '../../common/routes/rule/apis/global_execution_summary';
import { getGlobalExecutionSummarySchemaV1 } from '../../common/routes/rule/apis/global_execution_summary';
import type { AlertingRequestHandlerContext } from '../types';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../types';
import type { RewriteRequestCase } from './lib';
import { verifyAccessAndContext } from './lib';
import type { GetGlobalExecutionSummaryParams } from '../rules_client';
import type { ILicenseState } from '../lib';
import { DEFAULT_ALERTING_ROUTE_SECURITY } from './constants';
const rewriteReq: RewriteRequestCase<GetGlobalExecutionSummaryParams> = ({
date_start: dateStart,
date_end: dateEnd,
}) => ({
dateStart,
dateEnd,
});
export const getGlobalExecutionSummaryRoute = (
router: IRouter<AlertingRequestHandlerContext>,
licenseState: ILicenseState
) => {
router.get(
{
path: `${INTERNAL_BASE_ALERTING_API_PATH}/_global_execution_summary`,
security: DEFAULT_ALERTING_ROUTE_SECURITY,
options: {
access: 'internal',
},
validate: {
query: getGlobalExecutionSummarySchemaV1,
},
},
router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const alertingContext = await context.alerting;
const rulesClient = await alertingContext.getRulesClient();
const response: GetGlobalExecutionSummaryResponseV1 = {
body: await rulesClient.getGlobalExecutionSummaryWithAuth(rewriteReq(req.query)),
};
return res.ok(response);
})
)
);
};

View file

@ -86,7 +86,7 @@ import { findGapsRoute } from './gaps/apis/find/find_gaps_route';
import { fillGapByIdRoute } from './gaps/apis/fill/fill_gap_by_id_route';
import { getRuleIdsWithGapsRoute } from './gaps/apis/get_rule_ids_with_gaps/get_rule_ids_with_gaps_route';
import { getGapsSummaryByRuleIdsRoute } from './gaps/apis/get_gaps_summary_by_rule_ids/get_gaps_summary_by_rule_ids_route';
import { getGlobalExecutionSummaryRoute } from './get_global_execution_summary';
export interface RouteOptions {
router: IRouter<AlertingRequestHandlerContext>;
licenseState: ILicenseState;
@ -190,4 +190,5 @@ export function defineRoutes(opts: RouteOptions) {
runSoonRoute(router, licenseState);
healthRoute(router, licenseState, encryptedSavedObjects);
getGlobalExecutionKPIRoute(router, licenseState);
getGlobalExecutionSummaryRoute(router, licenseState);
}

View file

@ -38,6 +38,7 @@ const createRulesClientMock = () => {
getExecutionLogForRule: jest.fn(),
getRuleExecutionKPI: jest.fn(),
getGlobalExecutionKpiWithAuth: jest.fn(),
getGlobalExecutionSummaryWithAuth: jest.fn(),
getGlobalExecutionLogWithAuth: jest.fn(),
getActionErrorLog: jest.fn(),
getActionErrorLogWithAuth: jest.fn(),

View file

@ -29,6 +29,7 @@ export enum RuleAuditAction {
GET_EXECUTION_LOG = 'rule_get_execution_log',
GET_GLOBAL_EXECUTION_LOG = 'rule_get_global_execution_log',
GET_GLOBAL_EXECUTION_KPI = 'rule_get_global_execution_kpi',
GET_GLOBAL_EXECUTION_SUMMARY = 'rule_get_global_execution_summary',
GET_ACTION_ERROR_LOG = 'rule_get_action_error_log',
GET_RULE_EXECUTION_KPI = 'rule_get_execution_kpi',
SNOOZE = 'rule_snooze',
@ -95,6 +96,11 @@ const ruleEventVerbs: Record<RuleAuditAction, VerbsTuple> = {
'accessing global execution KPI for',
'accessed global execution KPI for',
],
rule_get_global_execution_summary: [
'access global execution summary for',
'accessing global execution summary for',
'accessed global execution summary for',
],
rule_alert_untrack: ['untrack', 'untracking', 'untracked'],
rule_schedule_backfill: [
'schedule backfill for',
@ -146,6 +152,7 @@ const ruleEventTypes: Record<RuleAuditAction, ArrayElement<EcsEvent['type']>> =
rule_run_soon: 'access',
rule_get_execution_kpi: 'access',
rule_get_global_execution_kpi: 'access',
rule_get_global_execution_summary: 'access',
rule_alert_untrack: 'change',
rule_schedule_backfill: 'access',
rule_find_gaps: 'access',

View file

@ -0,0 +1,82 @@
/*
* 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 { KueryNode } from '@kbn/es-query';
import { AlertingAuthorizationEntity, AlertingAuthorizationFilterType } from '../../authorization';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import type { RulesClientContext } from '../types';
import { parseDate } from '../common';
import { RULE_SAVED_OBJECT_TYPE } from '../../saved_objects';
import {
formatExecutionSummaryResult,
getExecutionSummaryAggregation,
} from '../../lib/get_execution_log_aggregation';
export interface GetGlobalExecutionSummaryParams {
dateStart: string;
dateEnd?: string;
}
export async function getGlobalExecutionSummaryWithAuth(
context: RulesClientContext,
{ dateStart, dateEnd }: GetGlobalExecutionSummaryParams
) {
context.logger.debug(`getGlobalExecutionSummaryWithAuth(): getting global execution summary`);
let authorizationTuple;
try {
authorizationTuple = await context.authorization.getFindAuthorizationFilter({
authorizationEntity: AlertingAuthorizationEntity.Alert,
filterOpts: {
type: AlertingAuthorizationFilterType.KQL,
fieldNames: {
ruleTypeId: 'kibana.alert.rule.rule_type_id',
consumer: 'kibana.alert.rule.consumer',
},
},
});
} catch (error) {
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.GET_GLOBAL_EXECUTION_SUMMARY,
error,
})
);
throw error;
}
context.auditLogger?.log(
ruleAuditEvent({
action: RuleAuditAction.GET_GLOBAL_EXECUTION_SUMMARY,
})
);
const dateNow = new Date();
const parsedDateStart = parseDate(dateStart, 'dateStart', dateNow);
const parsedDateEnd = parseDate(dateEnd, 'dateEnd', dateNow);
const eventLogClient = await context.getEventLogClient();
try {
const aggResult = await eventLogClient.aggregateEventsWithAuthFilter(
RULE_SAVED_OBJECT_TYPE,
authorizationTuple.filter as KueryNode,
{
start: parsedDateStart.toISOString(),
end: parsedDateEnd.toISOString(),
aggs: getExecutionSummaryAggregation(),
}
);
return formatExecutionSummaryResult(aggResult);
} catch (err) {
context.logger.debug(
`rulesClient.getGlobalExecutionSummaryWithAuth(): error searching global execution summary: ${err.message}`
);
throw err;
}
}

View file

@ -85,6 +85,8 @@ import { getRuleIdsWithGaps } from '../application/rule/methods/get_rule_ids_wit
import { getGapsSummaryByRuleIds } from '../application/rule/methods/get_gaps_summary_by_rule_ids';
import type { GetGapsSummaryByRuleIdsParams } from '../application/rule/methods/get_gaps_summary_by_rule_ids/types';
import type { FindGapsParams } from '../lib/rule_gaps/types';
import type { GetGlobalExecutionSummaryParams } from './methods/get_execution_summary';
import { getGlobalExecutionSummaryWithAuth } from './methods/get_execution_summary';
export type ConstructorOptions = Omit<
RulesClientContext,
@ -161,6 +163,8 @@ export class RulesClient {
getRuleExecutionKPI(this.context, params);
public getGlobalExecutionKpiWithAuth = (params: GetGlobalExecutionKPIParams) =>
getGlobalExecutionKpiWithAuth(this.context, params);
public getGlobalExecutionSummaryWithAuth = (params: GetGlobalExecutionSummaryParams) =>
getGlobalExecutionSummaryWithAuth(this.context, params);
public getActionErrorLog = (params: GetActionErrorLogByIdParams) =>
getActionErrorLog(this.context, params);
public getActionErrorLogWithAuth = (params: GetActionErrorLogByIdParams) =>

View file

@ -53,6 +53,7 @@ export type {
GetGlobalExecutionKPIParams,
GetRuleExecutionKPIParams,
} from './methods/get_execution_kpi';
export type { GetGlobalExecutionSummaryParams } from './methods/get_execution_summary';
export type { GetActionErrorLogByIdParams } from './methods/get_action_error_log';
export interface RulesClientContext {

View file

@ -0,0 +1,126 @@
/*
* 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 { UserAtSpaceScenarios } from '../../../scenarios';
import { getUrlPrefix, getTestRuleData, ObjectRemover, getEventLog } from '../../../../common/lib';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function getGlobalExecutionSummaryTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
const retry = getService('retry');
describe('getGlobalExecutionSummary', () => {
const objectRemover = new ObjectRemover(supertest);
afterEach(() => objectRemover.removeAll());
it('should return summary only from the current space', async () => {
const startTime = new Date().toISOString();
const spaceId = UserAtSpaceScenarios[1].space.id;
const user = UserAtSpaceScenarios[1].user;
const response = await supertest
.post(`${getUrlPrefix(spaceId)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.noop',
schedule: { interval: '1s' },
throttle: null,
})
);
expect(response.status).to.eql(200);
const ruleId = response.body.id;
objectRemover.add(spaceId, ruleId, 'rule', 'alerting');
const spaceId2 = UserAtSpaceScenarios[4].space.id;
const response2 = await supertest
.post(`${getUrlPrefix(spaceId2)}/api/alerting/rule`)
.set('kbn-xsrf', 'foo')
.send(
getTestRuleData({
rule_type_id: 'test.noop',
schedule: { interval: '1s' },
throttle: null,
})
);
expect(response2.status).to.eql(200);
const ruleId2 = response2.body.id;
objectRemover.add(spaceId2, ruleId2, 'rule', 'alerting');
await retry.try(async () => {
// Wait for 2 successful executions
const someEvents = await getEventLog({
getService,
spaceId,
type: 'alert',
id: ruleId,
provider: 'alerting',
actions: new Map([['execute', { gte: 1 }]]),
});
const successfulEvents = someEvents.filter((event) => event?.event?.outcome === 'success');
expect(successfulEvents.length).to.be.above(2);
});
await retry.try(async () => {
// break AAD
await supertest
.put(`${getUrlPrefix(spaceId)}/api/alerts_fixture/saved_object/alert/${ruleId}`)
.set('kbn-xsrf', 'foo')
.send({
attributes: {
name: 'bar',
},
})
.expect(200);
});
await retry.try(async () => {
// wait for 1 error
const someEvents = await getEventLog({
getService,
spaceId,
type: 'alert',
id: ruleId,
provider: 'alerting',
actions: new Map([['execute', { gte: 1 }]]),
});
const errorEvents = someEvents.filter((event) => event?.event?.outcome === 'failure');
expect(errorEvents.length).to.be.above(1);
});
const executionSummary = await retry.try(async () => {
// there can be a successful execute before the error one
const logResponse = await supertestWithoutAuth
.get(
`${getUrlPrefix(
spaceId
)}/internal/alerting/_global_execution_summary?date_start=${startTime}&date_end=9999-12-31T23:59:59Z`
)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(logResponse.statusCode).to.be(200);
return logResponse.body;
});
expect(Object.keys(executionSummary)).to.eql(['executions', 'latestExecutionSummary']);
expect(executionSummary.executions.success).to.be.above(2);
expect(executionSummary.executions.total).to.be.above(3);
expect(executionSummary.latestExecutionSummary.success).to.be(0);
expect(executionSummary.latestExecutionSummary.warning).to.be(0);
expect(executionSummary.latestExecutionSummary.failure).to.be(1);
});
});
}

View file

@ -31,6 +31,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
loadTestFile(require.resolve('./unsnooze_internal'));
loadTestFile(require.resolve('./global_execution_log'));
loadTestFile(require.resolve('./get_global_execution_kpi'));
loadTestFile(require.resolve('./get_global_execution_summary'));
loadTestFile(require.resolve('./get_action_error_log'));
loadTestFile(require.resolve('./get_rule_execution_kpi'));
});