mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Add global execution summary endpoint to alerting API
This commit is contained in:
parent
265b46f8dd
commit
1396f42606
20 changed files with 712 additions and 2 deletions
|
@ -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];
|
||||
|
|
|
@ -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`;
|
||||
|
|
|
@ -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';
|
|
@ -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';
|
|
@ -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,
|
||||
}),
|
||||
});
|
|
@ -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';
|
|
@ -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;
|
||||
}
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
`;
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
})
|
||||
)
|
||||
);
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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) =>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -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'));
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue