Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ pageLoadAssetSize:
telemetry: 51957
telemetryManagementSection: 38586
transform: 41007
triggersActionsUi: 100000
triggersActionsUi: 102400
upgradeAssistant: 81241
uptime: 40825
urlForwarding: 32579
Expand Down
40 changes: 40 additions & 0 deletions x-pack/plugins/alerting/common/execution_log_types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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 const executionLogSortableColumns = [
'timestamp',
'execution_duration',
'total_search_duration',
'es_search_duration',
'schedule_delay',
'num_triggered_actions',
] as const;

export type ExecutionLogSortFields = typeof executionLogSortableColumns[number];

export interface IExecutionLog {
id: string;
timestamp: string;
duration_ms: number;
status: string;
message: string;
num_active_alerts: number;
num_new_alerts: number;
num_recovered_alerts: number;
num_triggered_actions: number;
num_succeeded_actions: number;
num_errored_actions: number;
total_search_duration_ms: number;
es_search_duration_ms: number;
schedule_delay_ms: number;
timed_out: boolean;
}

export interface IExecutionLogResult {
total: number;
data: IExecutionLog[];
}
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export * from './builtin_action_groups';
export * from './disabled_action_groups';
export * from './alert_notify_when_type';
export * from './parse_duration';
export * from './execution_log_types';

export interface AlertingFrameworkHealth {
isSufficientlySecure: boolean;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import Boom from '@hapi/boom';
import { flatMap, get } from 'lodash';
import { parseDuration } from '.';
import { AggregateEventsBySavedObjectResult } from '../../../event_log/server';
import { IExecutionLog, IExecutionLogResult } from '../../common';

const DEFAULT_MAX_BUCKETS_LIMIT = 1000; // do not retrieve more than this number of executions

Expand All @@ -29,29 +30,6 @@ const EXECUTION_UUID_FIELD = 'kibana.alert.rule.execution.uuid';

const Millis2Nanos = 1000 * 1000;

export interface IExecutionLog {
id: string;
timestamp: string;
duration_ms: number;
status: string;
message: string;
num_active_alerts: number;
num_new_alerts: number;
num_recovered_alerts: number;
num_triggered_actions: number;
num_succeeded_actions: number;
num_errored_actions: number;
total_search_duration_ms: number;
es_search_duration_ms: number;
schedule_delay_ms: number;
timed_out: boolean;
}

export interface IExecutionLogResult {
total: number;
data: IExecutionLog[];
}

export const EMPTY_EXECUTION_LOG_RESULT = {
total: 0,
data: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ import { AlertingRulesConfig } from '../config';
import {
formatExecutionLogResult,
getExecutionLogAggregation,
IExecutionLogResult,
} from '../lib/get_execution_log_aggregation';
import { IExecutionLogResult } from '../../common';
import { validateSnoozeDate } from '../lib/validate_snooze_date';
import { RuleMutedError } from '../lib/errors/rule_muted';
import {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export type ExperimentalFeatures = typeof allowedExperimentalValues;
*/
export const allowedExperimentalValues = Object.freeze({
rulesListDatagrid: true,
rulesDetailLogs: false,
rulesDetailLogs: true,
});

type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,35 @@ export enum SORT_ORDERS {
export const DEFAULT_SEARCH_PAGE_SIZE: number = 10;

export const DEFAULT_RULE_INTERVAL = '1m';

export const RULE_EXECUTION_LOG_COLUMN_IDS = [
'id',
'timestamp',
'execution_duration',
'status',
'message',
'num_active_alerts',
'num_new_alerts',
'num_recovered_alerts',
'num_triggered_actions',
'num_succeeded_actions',
'num_errored_actions',
'total_search_duration',
'es_search_duration',
'schedule_delay',
'timed_out',
] as const;

export const RULE_EXECUTION_LOG_DURATION_COLUMNS = [
'execution_duration',
'total_search_duration',
'es_search_duration',
'schedule_delay',
];

export const RULE_EXECUTION_DEFAULT_INITIAL_VISIBLE_COLUMNS = [
'timestamp',
'execution_duration',
'status',
'message',
];
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ describe('monitoring_utils', () => {
it('should return a formatted duration', () => {
expect(getFormattedDuration(0)).toEqual('00:00');
expect(getFormattedDuration(100.111)).toEqual('00:00');
expect(getFormattedDuration(500)).toEqual('00:01');
expect(getFormattedDuration(50000)).toEqual('00:50');
expect(getFormattedDuration(59900)).toEqual('01:00');
expect(getFormattedDuration(500000)).toEqual('08:20');
expect(getFormattedDuration(5000000)).toEqual('83:20');
expect(getFormattedDuration(50000000)).toEqual('833:20');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,21 @@ export function getFormattedDuration(value: number) {
if (!value) {
return '00:00';
}

const duration = moment.duration(value);
const minutes = Math.floor(duration.asMinutes()).toString().padStart(2, '0');
const seconds = duration.seconds().toString().padStart(2, '0');
return `${minutes}:${seconds}`;
let minutes = Math.floor(duration.asMinutes());
let seconds = duration.seconds();
const ms = duration.milliseconds();

if (ms >= 500) {
seconds += 1;
if (seconds === 60) {
seconds = 0;
minutes += 1;
}
}

return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}

export function getFormattedMilliseconds(value: number) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export { muteRule, muteRules } from './mute';
export { loadRuleTypes } from './rule_types';
export { loadRules } from './rules';
export { loadRuleState } from './state';
export type { LoadExecutionLogAggregationsProps } from './load_execution_log_aggregations';
export { loadExecutionLogAggregations } from './load_execution_log_aggregations';
export { unmuteAlertInstance } from './unmute_alert';
export { unmuteRule, unmuteRules } from './unmute';
export { updateRule } from './update';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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 '../../../../../../../src/core/public/mocks';
import { loadExecutionLogAggregations, SortField } from './load_execution_log_aggregations';

const http = httpServiceMock.createStartContract();

const mockResponse = {
data: [
{
duration_ms: 50,
es_search_duration_ms: 1,
id: '13af2138-1c9d-4d34-95c1-c25fbfbb8eeb',
message: "rule executed: .index-threshold:c8f2ccb0-aac4-11ec-a5ae-2101bb96406d: 'test'",
num_active_alerts: 0,
num_errored_actions: 0,
num_new_alerts: 0,
num_recovered_alerts: 0,
num_succeeded_actions: 0,
num_triggered_actions: 0,
schedule_delay_ms: 1623,
status: 'success',
timed_out: false,
timestamp: '2022-03-23T16:17:53.482Z',
total_search_duration_ms: 4,
},
],
total: 5,
};

describe('loadExecutionLogAggregations', () => {
test('should call load execution log aggregation API', async () => {
http.get.mockResolvedValueOnce(mockResponse);

const sortTimestamp = {
timestamp: {
order: 'asc',
},
} as SortField;

const result = await loadExecutionLogAggregations({
id: 'test-id',
dateStart: '2022-03-23T16:17:53.482Z',
dateEnd: '2022-03-23T16:17:53.482Z',
filter: ['success', 'unknown'],
perPage: 10,
page: 0,
sort: [sortTimestamp],
http,
});

expect(result).toEqual({
...mockResponse,
data: [
{
execution_duration: 50,
es_search_duration: 1,
id: '13af2138-1c9d-4d34-95c1-c25fbfbb8eeb',
message: "rule executed: .index-threshold:c8f2ccb0-aac4-11ec-a5ae-2101bb96406d: 'test'",
num_active_alerts: 0,
num_errored_actions: 0,
num_new_alerts: 0,
num_recovered_alerts: 0,
num_succeeded_actions: 0,
num_triggered_actions: 0,
schedule_delay: 1623,
status: 'success',
timed_out: false,
timestamp: '2022-03-23T16:17:53.482Z',
total_search_duration: 4,
},
],
});

expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"/internal/alerting/rule/test-id/_execution_log",
Object {
"query": Object {
"date_end": "2022-03-23T16:17:53.482Z",
"date_start": "2022-03-23T16:17:53.482Z",
"filter": "success OR unknown",
"page": 1,
"per_page": 10,
"sort": "[{\\"timestamp\\":{\\"order\\":\\"asc\\"}}]",
},
},
]
`);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* 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.
*/

/* eslint-disable @typescript-eslint/naming-convention */

import { HttpSetup } from 'kibana/public';
import type { SortOrder } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';

import {
IExecutionLogResult,
IExecutionLog,
ExecutionLogSortFields,
} from '../../../../../alerting/common';
import { AsApiContract, RewriteRequestCase } from '../../../../../actions/common';

const getRenamedLog = (data: IExecutionLog) => {
const {
duration_ms,
total_search_duration_ms,
es_search_duration_ms,
schedule_delay_ms,
...rest
} = data;

return {
execution_duration: data.duration_ms,
total_search_duration: data.total_search_duration_ms,
es_search_duration: data.es_search_duration_ms,
schedule_delay: data.schedule_delay_ms,
...rest,
};
};

const rewriteBodyRes: RewriteRequestCase<IExecutionLogResult> = ({ data, total }: any) => ({
data: data.map((log: IExecutionLog) => getRenamedLog(log)),
total,
});

const getFilter = (filter: string[] | undefined) => {
if (!filter || !filter.length) {
return;
}
return filter.join(' OR ');
};

export type SortField = Record<
ExecutionLogSortFields,
{
order: SortOrder;
}
>;

export interface LoadExecutionLogAggregationsProps {
id: string;
dateStart: string;
dateEnd?: string;
filter?: string[];
perPage?: number;
page?: number;
sort?: SortField[];
}

export const loadExecutionLogAggregations = async ({
id,
http,
dateStart,
dateEnd,
filter,
perPage = 10,
page = 0,
sort = [],
}: LoadExecutionLogAggregationsProps & { http: HttpSetup }) => {
const sortField: any[] = sort;

const result = await http.get<AsApiContract<IExecutionLogResult>>(
`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${id}/_execution_log`,
{
query: {
date_start: dateStart,
date_end: dateEnd,
filter: getFilter(filter),
per_page: perPage,
// Need to add the + 1 for pages because APIs are 1 indexed,
// whereas data grid sorts are 0 indexed.
page: page + 1,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably worth adding a comment explaining why we have to add 1 - honestly, I don't know why the API works this way when it just subtracts 1 from the page variable later. I'd add something here to remind us about this.

sort: sortField.length ? JSON.stringify(sortField) : undefined,
},
}
);

return rewriteBodyRes(result);
};
Loading