Implemented Alerting health status pusher by using task manager and status pooler for Kibana status plugins 'kibanahost/api/status' (#79056)
* Implemented Alerting health status pusher by using task manager and status pooler for Kibana status plugins 'kibanahost/api/status' * Exposed health task registration to alerts plugin * Fixed type error * Extended health API endpoint with info about decryption failures, added correct health task implementation * adjusted query * Tested locally and got it working as expected, fixed tests and type check * Added unit tests * Changed AlertExecutionStatusErrorReasons to be enum * Uppercase the enum * Replaced string values to enum * Fixed types * Extended AlertsClient with getHealth method * added return type to healthStatus$ * Added configurable health check interval and timestamps * Extended update core status interval to 5mins * Fixed failing tests * Registered alerts config * Fixed date for ok health state * fixed jest test * fixed task state * Fixed due to comments, moved getHealth to a plugin level * fixed type checks * Added sorting to the latest Ok state last update * adjusted error queries * Fixed jest tests * removed unused * fixed type check
This commit is contained in:
parent
b08677b904
commit
802c6dccb4
|
@ -20,13 +20,12 @@ export interface IntervalSchedule extends SavedObjectAttributes {
|
|||
export const AlertExecutionStatusValues = ['ok', 'active', 'error', 'pending', 'unknown'] as const;
|
||||
export type AlertExecutionStatuses = typeof AlertExecutionStatusValues[number];
|
||||
|
||||
export const AlertExecutionStatusErrorReasonValues = [
|
||||
'read',
|
||||
'decrypt',
|
||||
'execute',
|
||||
'unknown',
|
||||
] as const;
|
||||
export type AlertExecutionStatusErrorReasons = typeof AlertExecutionStatusErrorReasonValues[number];
|
||||
export enum AlertExecutionStatusErrorReasons {
|
||||
Read = 'read',
|
||||
Decrypt = 'decrypt',
|
||||
Execute = 'execute',
|
||||
Unknown = 'unknown',
|
||||
}
|
||||
|
||||
export interface AlertExecutionStatus {
|
||||
status: AlertExecutionStatuses;
|
||||
|
@ -74,3 +73,24 @@ export interface Alert {
|
|||
}
|
||||
|
||||
export type SanitizedAlert = Omit<Alert, 'apiKey'>;
|
||||
|
||||
export enum HealthStatus {
|
||||
OK = 'ok',
|
||||
Warning = 'warn',
|
||||
Error = 'error',
|
||||
}
|
||||
|
||||
export interface AlertsHealth {
|
||||
decryptionHealth: {
|
||||
status: HealthStatus;
|
||||
timestamp: string;
|
||||
};
|
||||
executionHealth: {
|
||||
status: HealthStatus;
|
||||
timestamp: string;
|
||||
};
|
||||
readHealth: {
|
||||
status: HealthStatus;
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { AlertsHealth } from './alert';
|
||||
|
||||
export * from './alert';
|
||||
export * from './alert_type';
|
||||
export * from './alert_instance';
|
||||
|
@ -19,6 +21,7 @@ export interface ActionGroup {
|
|||
export interface AlertingFrameworkHealth {
|
||||
isSufficientlySecure: boolean;
|
||||
hasPermanentEncryptionKey: boolean;
|
||||
alertingFrameworkHeath: AlertsHealth;
|
||||
}
|
||||
|
||||
export const BASE_ALERT_API_PATH = '/api/alerts';
|
||||
|
|
19
x-pack/plugins/alerts/server/config.test.ts
Normal file
19
x-pack/plugins/alerts/server/config.test.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { configSchema } from './config';
|
||||
|
||||
describe('config validation', () => {
|
||||
test('alerts defaults', () => {
|
||||
const config: Record<string, unknown> = {};
|
||||
expect(configSchema.validate(config)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"healthCheck": Object {
|
||||
"interval": "60m",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
16
x-pack/plugins/alerts/server/config.ts
Normal file
16
x-pack/plugins/alerts/server/config.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { validateDurationSchema } from './lib';
|
||||
|
||||
export const configSchema = schema.object({
|
||||
healthCheck: schema.object({
|
||||
interval: schema.string({ validate: validateDurationSchema, defaultValue: '60m' }),
|
||||
}),
|
||||
});
|
||||
|
||||
export type AlertsConfig = TypeOf<typeof configSchema>;
|
221
x-pack/plugins/alerts/server/health/get_health.test.ts
Normal file
221
x-pack/plugins/alerts/server/health/get_health.test.ts
Normal file
|
@ -0,0 +1,221 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { savedObjectsRepositoryMock } from '../../../../../src/core/server/mocks';
|
||||
import { AlertExecutionStatusErrorReasons, HealthStatus } from '../types';
|
||||
import { getHealth } from './get_health';
|
||||
|
||||
const savedObjectsRepository = savedObjectsRepositoryMock.create();
|
||||
|
||||
describe('getHealth()', () => {
|
||||
test('return true if some of alerts has a decryption error', async () => {
|
||||
const lastExecutionDateError = new Date().toISOString();
|
||||
const lastExecutionDate = new Date().toISOString();
|
||||
savedObjectsRepository.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: 'myType',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
executionStatus: {
|
||||
status: 'error',
|
||||
lastExecutionDate: lastExecutionDateError,
|
||||
error: {
|
||||
reason: AlertExecutionStatusErrorReasons.Decrypt,
|
||||
message: 'Failed decrypt',
|
||||
},
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
savedObjectsRepository.find.mockResolvedValueOnce({
|
||||
total: 0,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
savedObjectsRepository.find.mockResolvedValueOnce({
|
||||
total: 0,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
savedObjectsRepository.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: 'myType',
|
||||
schedule: { interval: '1s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
actions: [],
|
||||
executionStatus: {
|
||||
status: 'ok',
|
||||
lastExecutionDate,
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await getHealth(savedObjectsRepository);
|
||||
expect(result).toStrictEqual({
|
||||
executionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: lastExecutionDate,
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: lastExecutionDate,
|
||||
},
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.Warning,
|
||||
timestamp: lastExecutionDateError,
|
||||
},
|
||||
});
|
||||
expect(savedObjectsRepository.find).toHaveBeenCalledTimes(4);
|
||||
});
|
||||
|
||||
test('return false if no alerts with a decryption error', async () => {
|
||||
const lastExecutionDateError = new Date().toISOString();
|
||||
const lastExecutionDate = new Date().toISOString();
|
||||
savedObjectsRepository.find.mockResolvedValueOnce({
|
||||
total: 0,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
savedObjectsRepository.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: 'myType',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
executionStatus: {
|
||||
status: 'error',
|
||||
lastExecutionDate: lastExecutionDateError,
|
||||
error: {
|
||||
reason: AlertExecutionStatusErrorReasons.Execute,
|
||||
message: 'Failed',
|
||||
},
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
savedObjectsRepository.find.mockResolvedValueOnce({
|
||||
total: 0,
|
||||
per_page: 10,
|
||||
page: 1,
|
||||
saved_objects: [],
|
||||
});
|
||||
|
||||
savedObjectsRepository.find.mockResolvedValueOnce({
|
||||
total: 1,
|
||||
per_page: 1,
|
||||
page: 1,
|
||||
saved_objects: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: 'myType',
|
||||
schedule: { interval: '1s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
createdAt: new Date().toISOString(),
|
||||
actions: [],
|
||||
executionStatus: {
|
||||
status: 'ok',
|
||||
lastExecutionDate,
|
||||
},
|
||||
},
|
||||
score: 1,
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
const result = await getHealth(savedObjectsRepository);
|
||||
expect(result).toStrictEqual({
|
||||
executionHealth: {
|
||||
status: HealthStatus.Warning,
|
||||
timestamp: lastExecutionDateError,
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: lastExecutionDate,
|
||||
},
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: lastExecutionDate,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
97
x-pack/plugins/alerts/server/health/get_health.ts
Normal file
97
x-pack/plugins/alerts/server/health/get_health.ts
Normal file
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { ISavedObjectsRepository } from 'src/core/server';
|
||||
import { AlertsHealth, HealthStatus, RawAlert, AlertExecutionStatusErrorReasons } from '../types';
|
||||
|
||||
export const getHealth = async (
|
||||
internalSavedObjectsRepository: ISavedObjectsRepository
|
||||
): Promise<AlertsHealth> => {
|
||||
const healthStatuses = {
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: '',
|
||||
},
|
||||
executionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: '',
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: '',
|
||||
},
|
||||
};
|
||||
|
||||
const { saved_objects: decryptErrorData } = await internalSavedObjectsRepository.find<RawAlert>({
|
||||
filter: `alert.attributes.executionStatus.status:error and alert.attributes.executionStatus.error.reason:${AlertExecutionStatusErrorReasons.Decrypt}`,
|
||||
fields: ['executionStatus'],
|
||||
type: 'alert',
|
||||
sortField: 'executionStatus.lastExecutionDate',
|
||||
sortOrder: 'desc',
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
});
|
||||
|
||||
if (decryptErrorData.length > 0) {
|
||||
healthStatuses.decryptionHealth = {
|
||||
status: HealthStatus.Warning,
|
||||
timestamp: decryptErrorData[0].attributes.executionStatus.lastExecutionDate,
|
||||
};
|
||||
}
|
||||
|
||||
const { saved_objects: executeErrorData } = await internalSavedObjectsRepository.find<RawAlert>({
|
||||
filter: `alert.attributes.executionStatus.status:error and alert.attributes.executionStatus.error.reason:${AlertExecutionStatusErrorReasons.Execute}`,
|
||||
fields: ['executionStatus'],
|
||||
type: 'alert',
|
||||
sortField: 'executionStatus.lastExecutionDate',
|
||||
sortOrder: 'desc',
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
});
|
||||
|
||||
if (executeErrorData.length > 0) {
|
||||
healthStatuses.executionHealth = {
|
||||
status: HealthStatus.Warning,
|
||||
timestamp: executeErrorData[0].attributes.executionStatus.lastExecutionDate,
|
||||
};
|
||||
}
|
||||
|
||||
const { saved_objects: readErrorData } = await internalSavedObjectsRepository.find<RawAlert>({
|
||||
filter: `alert.attributes.executionStatus.status:error and alert.attributes.executionStatus.error.reason:${AlertExecutionStatusErrorReasons.Read}`,
|
||||
fields: ['executionStatus'],
|
||||
type: 'alert',
|
||||
sortField: 'executionStatus.lastExecutionDate',
|
||||
sortOrder: 'desc',
|
||||
page: 1,
|
||||
perPage: 1,
|
||||
});
|
||||
|
||||
if (readErrorData.length > 0) {
|
||||
healthStatuses.readHealth = {
|
||||
status: HealthStatus.Warning,
|
||||
timestamp: readErrorData[0].attributes.executionStatus.lastExecutionDate,
|
||||
};
|
||||
}
|
||||
|
||||
const { saved_objects: noErrorData } = await internalSavedObjectsRepository.find<RawAlert>({
|
||||
filter: 'not alert.attributes.executionStatus.status:error',
|
||||
fields: ['executionStatus'],
|
||||
type: 'alert',
|
||||
sortField: 'executionStatus.lastExecutionDate',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
const lastExecutionDate =
|
||||
noErrorData.length > 0
|
||||
? noErrorData[0].attributes.executionStatus.lastExecutionDate
|
||||
: new Date().toISOString();
|
||||
|
||||
for (const [, statusItem] of Object.entries(healthStatuses)) {
|
||||
if (statusItem.status === HealthStatus.OK) {
|
||||
statusItem.timestamp = lastExecutionDate;
|
||||
}
|
||||
}
|
||||
|
||||
return healthStatuses;
|
||||
};
|
75
x-pack/plugins/alerts/server/health/get_state.test.ts
Normal file
75
x-pack/plugins/alerts/server/health/get_state.test.ts
Normal file
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { taskManagerMock } from '../../../task_manager/server/mocks';
|
||||
import { getHealthStatusStream } from '.';
|
||||
import { TaskStatus } from '../../../task_manager/server';
|
||||
import { HealthStatus } from '../types';
|
||||
|
||||
describe('getHealthStatusStream()', () => {
|
||||
const mockTaskManager = taskManagerMock.createStart();
|
||||
|
||||
it('should return an object with the "unavailable" level and proper summary of "Alerting framework is unhealthy"', async () => {
|
||||
mockTaskManager.get.mockReturnValue(
|
||||
new Promise((_resolve, _reject) => {
|
||||
return {
|
||||
id: 'test',
|
||||
attempts: 0,
|
||||
status: TaskStatus.Running,
|
||||
version: '123',
|
||||
runAt: new Date(),
|
||||
scheduledAt: new Date(),
|
||||
startedAt: new Date(),
|
||||
retryAt: new Date(Date.now() + 5 * 60 * 1000),
|
||||
state: {
|
||||
runs: 1,
|
||||
health_status: HealthStatus.Warning,
|
||||
},
|
||||
taskType: 'alerting:alerting_health_check',
|
||||
params: {
|
||||
alertId: '1',
|
||||
},
|
||||
ownerId: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
getHealthStatusStream(mockTaskManager).subscribe(
|
||||
(val: { level: Readonly<unknown>; summary: string }) => {
|
||||
expect(val.level).toBe(false);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an object with the "available" level and proper summary of "Alerting framework is healthy"', async () => {
|
||||
mockTaskManager.get.mockReturnValue(
|
||||
new Promise((_resolve, _reject) => {
|
||||
return {
|
||||
id: 'test',
|
||||
attempts: 0,
|
||||
status: TaskStatus.Running,
|
||||
version: '123',
|
||||
runAt: new Date(),
|
||||
scheduledAt: new Date(),
|
||||
startedAt: new Date(),
|
||||
retryAt: new Date(Date.now() + 5 * 60 * 1000),
|
||||
state: {
|
||||
runs: 1,
|
||||
health_status: HealthStatus.OK,
|
||||
},
|
||||
taskType: 'alerting:alerting_health_check',
|
||||
params: {
|
||||
alertId: '1',
|
||||
},
|
||||
ownerId: null,
|
||||
};
|
||||
})
|
||||
);
|
||||
getHealthStatusStream(mockTaskManager).subscribe(
|
||||
(val: { level: Readonly<unknown>; summary: string }) => {
|
||||
expect(val.level).toBe(true);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
73
x-pack/plugins/alerts/server/health/get_state.ts
Normal file
73
x-pack/plugins/alerts/server/health/get_state.ts
Normal file
|
@ -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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { interval, Observable } from 'rxjs';
|
||||
import { catchError, switchMap } from 'rxjs/operators';
|
||||
import { ServiceStatus, ServiceStatusLevels } from '../../../../../src/core/server';
|
||||
import { TaskManagerStartContract } from '../../../task_manager/server';
|
||||
import { HEALTH_TASK_ID } from './task';
|
||||
import { HealthStatus } from '../types';
|
||||
|
||||
async function getLatestTaskState(taskManager: TaskManagerStartContract) {
|
||||
try {
|
||||
const result = await taskManager.get(HEALTH_TASK_ID);
|
||||
return result;
|
||||
} catch (err) {
|
||||
const errMessage = err && err.message ? err.message : err.toString();
|
||||
if (!errMessage.includes('NotInitialized')) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const LEVEL_SUMMARY = {
|
||||
[ServiceStatusLevels.available.toString()]: i18n.translate(
|
||||
'xpack.alerts.server.healthStatus.available',
|
||||
{
|
||||
defaultMessage: 'Alerting framework is available',
|
||||
}
|
||||
),
|
||||
[ServiceStatusLevels.degraded.toString()]: i18n.translate(
|
||||
'xpack.alerts.server.healthStatus.degraded',
|
||||
{
|
||||
defaultMessage: 'Alerting framework is degraded',
|
||||
}
|
||||
),
|
||||
[ServiceStatusLevels.unavailable.toString()]: i18n.translate(
|
||||
'xpack.alerts.server.healthStatus.unavailable',
|
||||
{
|
||||
defaultMessage: 'Alerting framework is unavailable',
|
||||
}
|
||||
),
|
||||
};
|
||||
|
||||
export const getHealthStatusStream = (
|
||||
taskManager: TaskManagerStartContract
|
||||
): Observable<ServiceStatus<unknown>> => {
|
||||
return interval(60000 * 5).pipe(
|
||||
switchMap(async () => {
|
||||
const doc = await getLatestTaskState(taskManager);
|
||||
const level =
|
||||
doc?.state?.health_status === HealthStatus.OK
|
||||
? ServiceStatusLevels.available
|
||||
: doc?.state?.health_status === HealthStatus.Warning
|
||||
? ServiceStatusLevels.degraded
|
||||
: ServiceStatusLevels.unavailable;
|
||||
return {
|
||||
level,
|
||||
summary: LEVEL_SUMMARY[level.toString()],
|
||||
};
|
||||
}),
|
||||
catchError(async (error) => ({
|
||||
level: ServiceStatusLevels.unavailable,
|
||||
summary: LEVEL_SUMMARY[ServiceStatusLevels.unavailable.toString()],
|
||||
meta: { error },
|
||||
}))
|
||||
);
|
||||
};
|
8
x-pack/plugins/alerts/server/health/index.ts
Normal file
8
x-pack/plugins/alerts/server/health/index.ts
Normal 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;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
export { getHealthStatusStream } from './get_state';
|
||||
export { scheduleAlertingHealthCheck, initializeAlertingHealth } from './task';
|
94
x-pack/plugins/alerts/server/health/task.ts
Normal file
94
x-pack/plugins/alerts/server/health/task.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { CoreStart, Logger } from 'kibana/server';
|
||||
import {
|
||||
RunContext,
|
||||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
} from '../../../task_manager/server';
|
||||
import { AlertsConfig } from '../config';
|
||||
import { AlertingPluginsStart } from '../plugin';
|
||||
import { HealthStatus } from '../types';
|
||||
import { getHealth } from './get_health';
|
||||
|
||||
export const HEALTH_TASK_TYPE = 'alerting_health_check';
|
||||
|
||||
export const HEALTH_TASK_ID = `Alerting-${HEALTH_TASK_TYPE}`;
|
||||
|
||||
export function initializeAlertingHealth(
|
||||
logger: Logger,
|
||||
taskManager: TaskManagerSetupContract,
|
||||
coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>
|
||||
) {
|
||||
registerAlertingHealthCheckTask(logger, taskManager, coreStartServices);
|
||||
}
|
||||
|
||||
export async function scheduleAlertingHealthCheck(
|
||||
logger: Logger,
|
||||
config: Promise<AlertsConfig>,
|
||||
taskManager: TaskManagerStartContract
|
||||
) {
|
||||
try {
|
||||
const interval = (await config).healthCheck.interval;
|
||||
await taskManager.ensureScheduled({
|
||||
id: HEALTH_TASK_ID,
|
||||
taskType: HEALTH_TASK_TYPE,
|
||||
schedule: {
|
||||
interval,
|
||||
},
|
||||
state: {},
|
||||
params: {},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.debug(`Error scheduling task, received ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function registerAlertingHealthCheckTask(
|
||||
logger: Logger,
|
||||
taskManager: TaskManagerSetupContract,
|
||||
coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>
|
||||
) {
|
||||
taskManager.registerTaskDefinitions({
|
||||
[HEALTH_TASK_TYPE]: {
|
||||
title: 'Alerting framework health check task',
|
||||
createTaskRunner: healthCheckTaskRunner(logger, coreStartServices),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function healthCheckTaskRunner(
|
||||
logger: Logger,
|
||||
coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>
|
||||
) {
|
||||
return ({ taskInstance }: RunContext) => {
|
||||
const { state } = taskInstance;
|
||||
return {
|
||||
async run() {
|
||||
try {
|
||||
const alertingHealthStatus = await getHealth(
|
||||
(await coreStartServices)[0].savedObjects.createInternalRepository(['alert'])
|
||||
);
|
||||
return {
|
||||
state: {
|
||||
runs: (state.runs || 0) + 1,
|
||||
health_status: alertingHealthStatus.decryptionHealth.status,
|
||||
},
|
||||
};
|
||||
} catch (errMsg) {
|
||||
logger.warn(`Error executing alerting health check task: ${errMsg}`);
|
||||
return {
|
||||
state: {
|
||||
runs: (state.runs || 0) + 1,
|
||||
health_status: HealthStatus.Error,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
|
@ -5,8 +5,10 @@
|
|||
*/
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { AlertsClient as AlertsClientClass } from './alerts_client';
|
||||
import { PluginInitializerContext } from '../../../../src/core/server';
|
||||
import { PluginConfigDescriptor, PluginInitializerContext } from '../../../../src/core/server';
|
||||
import { AlertingPlugin } from './plugin';
|
||||
import { configSchema } from './config';
|
||||
import { AlertsConfigType } from './types';
|
||||
|
||||
export type AlertsClient = PublicMethodsOf<AlertsClientClass>;
|
||||
|
||||
|
@ -30,3 +32,7 @@ export { AlertInstance } from './alert_instance';
|
|||
export { parseDuration } from './lib';
|
||||
|
||||
export const plugin = (initContext: PluginInitializerContext) => new AlertingPlugin(initContext);
|
||||
|
||||
export const config: PluginConfigDescriptor<AlertsConfigType> = {
|
||||
schema: configSchema,
|
||||
};
|
||||
|
|
|
@ -57,7 +57,9 @@ describe('AlertExecutionStatus', () => {
|
|||
});
|
||||
|
||||
test('error with a reason', () => {
|
||||
const status = executionStatusFromError(new ErrorWithReason('execute', new Error('hoo!')));
|
||||
const status = executionStatusFromError(
|
||||
new ErrorWithReason(AlertExecutionStatusErrorReasons.Execute, new Error('hoo!'))
|
||||
);
|
||||
expect(status.status).toBe('error');
|
||||
expect(status.error).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
|
@ -71,7 +73,7 @@ describe('AlertExecutionStatus', () => {
|
|||
describe('alertExecutionStatusToRaw()', () => {
|
||||
const date = new Date('2020-09-03T16:26:58Z');
|
||||
const status = 'ok';
|
||||
const reason: AlertExecutionStatusErrorReasons = 'decrypt';
|
||||
const reason = AlertExecutionStatusErrorReasons.Decrypt;
|
||||
const error = { reason, message: 'wops' };
|
||||
|
||||
test('status without an error', () => {
|
||||
|
@ -102,7 +104,7 @@ describe('AlertExecutionStatus', () => {
|
|||
describe('alertExecutionStatusFromRaw()', () => {
|
||||
const date = new Date('2020-09-03T16:26:58Z').toISOString();
|
||||
const status = 'active';
|
||||
const reason: AlertExecutionStatusErrorReasons = 'execute';
|
||||
const reason = AlertExecutionStatusErrorReasons.Execute;
|
||||
const error = { reason, message: 'wops' };
|
||||
|
||||
test('no input', () => {
|
||||
|
|
|
@ -5,20 +5,21 @@
|
|||
*/
|
||||
|
||||
import { ErrorWithReason, getReasonFromError, isErrorWithReason } from './error_with_reason';
|
||||
import { AlertExecutionStatusErrorReasons } from '../types';
|
||||
|
||||
describe('ErrorWithReason', () => {
|
||||
const plainError = new Error('well, actually');
|
||||
const errorWithReason = new ErrorWithReason('decrypt', plainError);
|
||||
const errorWithReason = new ErrorWithReason(AlertExecutionStatusErrorReasons.Decrypt, plainError);
|
||||
|
||||
test('ErrorWithReason class', () => {
|
||||
expect(errorWithReason.message).toBe(plainError.message);
|
||||
expect(errorWithReason.error).toBe(plainError);
|
||||
expect(errorWithReason.reason).toBe('decrypt');
|
||||
expect(errorWithReason.reason).toBe(AlertExecutionStatusErrorReasons.Decrypt);
|
||||
});
|
||||
|
||||
test('getReasonFromError()', () => {
|
||||
expect(getReasonFromError(plainError)).toBe('unknown');
|
||||
expect(getReasonFromError(errorWithReason)).toBe('decrypt');
|
||||
expect(getReasonFromError(errorWithReason)).toBe(AlertExecutionStatusErrorReasons.Decrypt);
|
||||
});
|
||||
|
||||
test('isErrorWithReason()', () => {
|
||||
|
|
|
@ -21,7 +21,7 @@ export function getReasonFromError(error: Error): AlertExecutionStatusErrorReaso
|
|||
if (isErrorWithReason(error)) {
|
||||
return error.reason;
|
||||
}
|
||||
return 'unknown';
|
||||
return AlertExecutionStatusErrorReasons.Unknown;
|
||||
}
|
||||
|
||||
export function isErrorWithReason(error: Error | ErrorWithReason): error is ErrorWithReason {
|
||||
|
|
|
@ -8,6 +8,7 @@ import { isAlertSavedObjectNotFoundError } from './is_alert_not_found_error';
|
|||
import { ErrorWithReason } from './error_with_reason';
|
||||
import { SavedObjectsErrorHelpers } from '../../../../../src/core/server';
|
||||
import uuid from 'uuid';
|
||||
import { AlertExecutionStatusErrorReasons } from '../types';
|
||||
|
||||
describe('isAlertSavedObjectNotFoundError', () => {
|
||||
const id = uuid.v4();
|
||||
|
@ -25,7 +26,7 @@ describe('isAlertSavedObjectNotFoundError', () => {
|
|||
});
|
||||
|
||||
test('identifies SavedObjects Not Found errors wrapped in an ErrorWithReason', () => {
|
||||
const error = new ErrorWithReason('read', errorSONF);
|
||||
const error = new ErrorWithReason(AlertExecutionStatusErrorReasons.Read, errorSONF);
|
||||
expect(isAlertSavedObjectNotFoundError(error, id)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,6 +25,7 @@ const createStartMock = () => {
|
|||
const mock: jest.Mocked<PluginStartContract> = {
|
||||
listTypes: jest.fn(),
|
||||
getAlertsClientWithRequest: jest.fn().mockResolvedValue(alertsClientMock.create()),
|
||||
getFrameworkHealth: jest.fn(),
|
||||
};
|
||||
return mock;
|
||||
};
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
import { AlertingPlugin, AlertingPluginsSetup, AlertingPluginsStart } from './plugin';
|
||||
import { coreMock } from '../../../../src/core/server/mocks';
|
||||
import { coreMock, statusServiceMock } from '../../../../src/core/server/mocks';
|
||||
import { licensingMock } from '../../licensing/server/mocks';
|
||||
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
|
||||
import { taskManagerMock } from '../../task_manager/server/mocks';
|
||||
|
@ -13,15 +13,21 @@ import { eventLogServiceMock } from '../../event_log/server/event_log_service.mo
|
|||
import { KibanaRequest, CoreSetup } from 'kibana/server';
|
||||
import { featuresPluginMock } from '../../features/server/mocks';
|
||||
import { KibanaFeature } from '../../features/server';
|
||||
import { AlertsConfig } from './config';
|
||||
|
||||
describe('Alerting Plugin', () => {
|
||||
describe('setup()', () => {
|
||||
it('should log warning when Encrypted Saved Objects plugin is using an ephemeral encryption key', async () => {
|
||||
const context = coreMock.createPluginInitializerContext();
|
||||
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
});
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
|
||||
const statusMock = statusServiceMock.createSetupContract();
|
||||
await plugin.setup(
|
||||
({
|
||||
...coreSetup,
|
||||
|
@ -29,6 +35,7 @@ describe('Alerting Plugin', () => {
|
|||
...coreSetup.http,
|
||||
route: jest.fn(),
|
||||
},
|
||||
status: statusMock,
|
||||
} as unknown) as CoreSetup<AlertingPluginsStart, unknown>,
|
||||
({
|
||||
licensing: licensingMock.createSetup(),
|
||||
|
@ -38,6 +45,7 @@ describe('Alerting Plugin', () => {
|
|||
} as unknown) as AlertingPluginsSetup
|
||||
);
|
||||
|
||||
expect(statusMock.set).toHaveBeenCalledTimes(1);
|
||||
expect(encryptedSavedObjectsSetup.usingEphemeralEncryptionKey).toEqual(true);
|
||||
expect(context.logger.get().warn).toHaveBeenCalledWith(
|
||||
'APIs are disabled due to the Encrypted Saved Objects plugin using an ephemeral encryption key. Please set xpack.encryptedSavedObjects.encryptionKey in kibana.yml.'
|
||||
|
@ -55,7 +63,11 @@ describe('Alerting Plugin', () => {
|
|||
*/
|
||||
describe('getAlertsClientWithRequest()', () => {
|
||||
it('throws error when encryptedSavedObjects plugin has usingEphemeralEncryptionKey set to true', async () => {
|
||||
const context = coreMock.createPluginInitializerContext();
|
||||
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
});
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
|
@ -98,7 +110,11 @@ describe('Alerting Plugin', () => {
|
|||
});
|
||||
|
||||
it(`doesn't throw error when encryptedSavedObjects plugin has usingEphemeralEncryptionKey set to false`, async () => {
|
||||
const context = coreMock.createPluginInitializerContext();
|
||||
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
|
||||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
});
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
const coreSetup = coreMock.createSetup();
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import {
|
||||
EncryptedSavedObjectsPluginSetup,
|
||||
|
@ -30,6 +31,8 @@ import {
|
|||
SharedGlobalConfig,
|
||||
ElasticsearchServiceStart,
|
||||
ILegacyClusterClient,
|
||||
StatusServiceSetup,
|
||||
ServiceStatus,
|
||||
} from '../../../../src/core/server';
|
||||
|
||||
import {
|
||||
|
@ -56,12 +59,19 @@ import {
|
|||
PluginSetupContract as ActionsPluginSetupContract,
|
||||
PluginStartContract as ActionsPluginStartContract,
|
||||
} from '../../actions/server';
|
||||
import { Services } from './types';
|
||||
import { AlertsHealth, Services } from './types';
|
||||
import { registerAlertsUsageCollector } from './usage';
|
||||
import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/task';
|
||||
import { IEventLogger, IEventLogService, IEventLogClientService } from '../../event_log/server';
|
||||
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
|
||||
import { setupSavedObjects } from './saved_objects';
|
||||
import {
|
||||
getHealthStatusStream,
|
||||
scheduleAlertingHealthCheck,
|
||||
initializeAlertingHealth,
|
||||
} from './health';
|
||||
import { AlertsConfig } from './config';
|
||||
import { getHealth } from './health/get_health';
|
||||
|
||||
export const EVENT_LOG_PROVIDER = 'alerting';
|
||||
export const EVENT_LOG_ACTIONS = {
|
||||
|
@ -78,6 +88,7 @@ export interface PluginSetupContract {
|
|||
export interface PluginStartContract {
|
||||
listTypes: AlertTypeRegistry['list'];
|
||||
getAlertsClientWithRequest(request: KibanaRequest): PublicMethodsOf<AlertsClient>;
|
||||
getFrameworkHealth: () => Promise<AlertsHealth>;
|
||||
}
|
||||
|
||||
export interface AlertingPluginsSetup {
|
||||
|
@ -89,6 +100,7 @@ export interface AlertingPluginsSetup {
|
|||
spaces?: SpacesPluginSetup;
|
||||
usageCollection?: UsageCollectionSetup;
|
||||
eventLog: IEventLogService;
|
||||
statusService: StatusServiceSetup;
|
||||
}
|
||||
export interface AlertingPluginsStart {
|
||||
actions: ActionsPluginStartContract;
|
||||
|
@ -99,6 +111,7 @@ export interface AlertingPluginsStart {
|
|||
}
|
||||
|
||||
export class AlertingPlugin {
|
||||
private readonly config: Promise<AlertsConfig>;
|
||||
private readonly logger: Logger;
|
||||
private alertTypeRegistry?: AlertTypeRegistry;
|
||||
private readonly taskRunnerFactory: TaskRunnerFactory;
|
||||
|
@ -115,6 +128,7 @@ export class AlertingPlugin {
|
|||
private eventLogger?: IEventLogger;
|
||||
|
||||
constructor(initializerContext: PluginInitializerContext) {
|
||||
this.config = initializerContext.config.create<AlertsConfig>().pipe(first()).toPromise();
|
||||
this.logger = initializerContext.logger.get('plugins', 'alerting');
|
||||
this.taskRunnerFactory = new TaskRunnerFactory();
|
||||
this.alertsClientFactory = new AlertsClientFactory();
|
||||
|
@ -186,6 +200,25 @@ export class AlertingPlugin {
|
|||
});
|
||||
}
|
||||
|
||||
core.getStartServices().then(async ([, startPlugins]) => {
|
||||
core.status.set(
|
||||
combineLatest([
|
||||
core.status.derivedStatus$,
|
||||
getHealthStatusStream(startPlugins.taskManager),
|
||||
]).pipe(
|
||||
map(([derivedStatus, healthStatus]) => {
|
||||
if (healthStatus.level > derivedStatus.level) {
|
||||
return healthStatus as ServiceStatus;
|
||||
} else {
|
||||
return derivedStatus;
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
initializeAlertingHealth(this.logger, plugins.taskManager, core.getStartServices());
|
||||
|
||||
core.http.registerRouteHandlerContext('alerting', this.createRouteHandlerContext(core));
|
||||
|
||||
// Routes
|
||||
|
@ -275,10 +308,13 @@ export class AlertingPlugin {
|
|||
});
|
||||
|
||||
scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager);
|
||||
scheduleAlertingHealthCheck(this.logger, this.config, plugins.taskManager);
|
||||
|
||||
return {
|
||||
listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!),
|
||||
getAlertsClientWithRequest,
|
||||
getFrameworkHealth: async () =>
|
||||
await getHealth(core.savedObjects.createInternalRepository(['alert'])),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -293,6 +329,8 @@ export class AlertingPlugin {
|
|||
return alertsClientFactory!.create(request, savedObjects);
|
||||
},
|
||||
listTypes: alertTypeRegistry!.list.bind(alertTypeRegistry!),
|
||||
getFrameworkHealth: async () =>
|
||||
await getHealth(savedObjects.createInternalRepository(['alert'])),
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -14,7 +14,7 @@ import { identity } from 'lodash';
|
|||
import type { MethodKeysOf } from '@kbn/utility-types';
|
||||
import { httpServerMock } from '../../../../../src/core/server/mocks';
|
||||
import { alertsClientMock, AlertsClientMock } from '../alerts_client.mock';
|
||||
import { AlertType } from '../../common';
|
||||
import { AlertsHealth, AlertType } from '../../common';
|
||||
import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
|
||||
|
||||
export function mockHandlerArguments(
|
||||
|
@ -22,10 +22,13 @@ export function mockHandlerArguments(
|
|||
alertsClient = alertsClientMock.create(),
|
||||
listTypes: listTypesRes = [],
|
||||
esClient = elasticsearchServiceMock.createLegacyClusterClient(),
|
||||
getFrameworkHealth,
|
||||
}: {
|
||||
alertsClient?: AlertsClientMock;
|
||||
listTypes?: AlertType[];
|
||||
esClient?: jest.Mocked<ILegacyClusterClient>;
|
||||
getFrameworkHealth?: jest.MockInstance<Promise<AlertsHealth>, []> &
|
||||
(() => Promise<AlertsHealth>);
|
||||
},
|
||||
req: unknown,
|
||||
res?: Array<MethodKeysOf<KibanaResponseFactory>>
|
||||
|
@ -39,6 +42,7 @@ export function mockHandlerArguments(
|
|||
getAlertsClient() {
|
||||
return alertsClient || alertsClientMock.create();
|
||||
},
|
||||
getFrameworkHealth,
|
||||
},
|
||||
} as unknown) as RequestHandlerContext,
|
||||
req as KibanaRequest<unknown, unknown, unknown>,
|
||||
|
|
|
@ -11,13 +11,34 @@ import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
|
|||
import { verifyApiAccess } from '../lib/license_api_access';
|
||||
import { mockLicenseState } from '../lib/license_state.mock';
|
||||
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
|
||||
import { alertsClientMock } from '../alerts_client.mock';
|
||||
import { HealthStatus } from '../types';
|
||||
import { alertsMock } from '../mocks';
|
||||
const alertsClient = alertsClientMock.create();
|
||||
|
||||
jest.mock('../lib/license_api_access.ts', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
const alerting = alertsMock.createStart();
|
||||
|
||||
const currentDate = new Date().toISOString();
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
alerting.getFrameworkHealth.mockResolvedValue({
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
executionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('healthRoute', () => {
|
||||
|
@ -46,7 +67,7 @@ describe('healthRoute', () => {
|
|||
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
|
||||
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
|
||||
const [context, req, res] = mockHandlerArguments({ esClient, alertsClient }, {}, ['ok']);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
|
@ -75,16 +96,32 @@ describe('healthRoute', () => {
|
|||
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
|
||||
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
|
||||
{},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": false,
|
||||
"isSufficientlySecure": true,
|
||||
expect(await handler(context, req, res)).toStrictEqual({
|
||||
body: {
|
||||
alertingFrameworkHeath: {
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
executionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
hasPermanentEncryptionKey: false,
|
||||
isSufficientlySecure: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => {
|
||||
|
@ -99,16 +136,32 @@ describe('healthRoute', () => {
|
|||
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
|
||||
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
|
||||
{},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": true,
|
||||
expect(await handler(context, req, res)).toStrictEqual({
|
||||
body: {
|
||||
alertingFrameworkHeath: {
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
executionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
hasPermanentEncryptionKey: true,
|
||||
isSufficientlySecure: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => {
|
||||
|
@ -123,16 +176,32 @@ describe('healthRoute', () => {
|
|||
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
|
||||
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: {} }));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
|
||||
{},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": true,
|
||||
expect(await handler(context, req, res)).toStrictEqual({
|
||||
body: {
|
||||
alertingFrameworkHeath: {
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
executionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
hasPermanentEncryptionKey: true,
|
||||
isSufficientlySecure: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => {
|
||||
|
@ -147,16 +216,32 @@ describe('healthRoute', () => {
|
|||
const esClient = elasticsearchServiceMock.createLegacyClusterClient();
|
||||
esClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: { enabled: true } }));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
|
||||
{},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": false,
|
||||
expect(await handler(context, req, res)).toStrictEqual({
|
||||
body: {
|
||||
alertingFrameworkHeath: {
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
executionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
hasPermanentEncryptionKey: true,
|
||||
isSufficientlySecure: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => {
|
||||
|
@ -173,16 +258,32 @@ describe('healthRoute', () => {
|
|||
Promise.resolve({ security: { enabled: true, ssl: {} } })
|
||||
);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
|
||||
{},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": false,
|
||||
expect(await handler(context, req, res)).toStrictEqual({
|
||||
body: {
|
||||
alertingFrameworkHeath: {
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
executionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
hasPermanentEncryptionKey: true,
|
||||
isSufficientlySecure: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('evaluates security and tls enabled to mean that the user can generate keys', async () => {
|
||||
|
@ -199,15 +300,31 @@ describe('healthRoute', () => {
|
|||
Promise.resolve({ security: { enabled: true, ssl: { http: { enabled: true } } } })
|
||||
);
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ esClient }, {}, ['ok']);
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
{ esClient, alertsClient, getFrameworkHealth: alerting.getFrameworkHealth },
|
||||
{},
|
||||
['ok']
|
||||
);
|
||||
|
||||
expect(await handler(context, req, res)).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"hasPermanentEncryptionKey": true,
|
||||
"isSufficientlySecure": true,
|
||||
expect(await handler(context, req, res)).toStrictEqual({
|
||||
body: {
|
||||
alertingFrameworkHeath: {
|
||||
decryptionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
executionHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
readHealth: {
|
||||
status: HealthStatus.OK,
|
||||
timestamp: currentDate,
|
||||
},
|
||||
},
|
||||
}
|
||||
`);
|
||||
hasPermanentEncryptionKey: true,
|
||||
isSufficientlySecure: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -43,6 +43,9 @@ export function healthRoute(
|
|||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse> {
|
||||
verifyApiAccess(licenseState);
|
||||
if (!context.alerting) {
|
||||
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
|
||||
}
|
||||
try {
|
||||
const {
|
||||
security: {
|
||||
|
@ -57,9 +60,12 @@ export function healthRoute(
|
|||
path: '/_xpack/usage',
|
||||
});
|
||||
|
||||
const alertingFrameworkHeath = await context.alerting.getFrameworkHealth();
|
||||
|
||||
const frameworkHealth: AlertingFrameworkHealth = {
|
||||
isSufficientlySecure: !isSecurityEnabled || (isSecurityEnabled && isTLSEnabled),
|
||||
hasPermanentEncryptionKey: !encryptedSavedObjects.usingEphemeralEncryptionKey,
|
||||
alertingFrameworkHeath,
|
||||
};
|
||||
|
||||
return res.ok({
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
AlertExecutorOptions,
|
||||
SanitizedAlert,
|
||||
AlertExecutionStatus,
|
||||
AlertExecutionStatusErrorReasons,
|
||||
} from '../types';
|
||||
import { promiseResult, map, Resultable, asOk, asErr, resolveErr } from '../lib/result_type';
|
||||
import { taskInstanceToAlertTaskInstance } from './alert_task_instance';
|
||||
|
@ -211,7 +212,7 @@ export class TaskRunner {
|
|||
event.event = event.event || {};
|
||||
event.event.outcome = 'failure';
|
||||
eventLogger.logEvent(event);
|
||||
throw new ErrorWithReason('execute', err);
|
||||
throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Execute, err);
|
||||
}
|
||||
|
||||
eventLogger.stopTiming(event);
|
||||
|
@ -288,7 +289,7 @@ export class TaskRunner {
|
|||
try {
|
||||
apiKey = await this.getApiKeyForAlertPermissions(alertId, spaceId);
|
||||
} catch (err) {
|
||||
throw new ErrorWithReason('decrypt', err);
|
||||
throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Decrypt, err);
|
||||
}
|
||||
const [services, alertsClient] = this.getServicesWithSpaceLevelPermissions(spaceId, apiKey);
|
||||
|
||||
|
@ -298,7 +299,7 @@ export class TaskRunner {
|
|||
try {
|
||||
alert = await alertsClient.get({ id: alertId });
|
||||
} catch (err) {
|
||||
throw new ErrorWithReason('read', err);
|
||||
throw new ErrorWithReason(AlertExecutionStatusErrorReasons.Read, err);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
AlertInstanceState,
|
||||
AlertExecutionStatuses,
|
||||
AlertExecutionStatusErrorReasons,
|
||||
AlertsHealth,
|
||||
} from '../common';
|
||||
|
||||
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
|
||||
|
@ -39,6 +40,7 @@ declare module 'src/core/server' {
|
|||
alerting?: {
|
||||
getAlertsClient: () => AlertsClient;
|
||||
listTypes: AlertTypeRegistry['list'];
|
||||
getFrameworkHealth: () => Promise<AlertsHealth>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -172,4 +174,10 @@ export interface AlertingPlugin {
|
|||
start: PluginStartContract;
|
||||
}
|
||||
|
||||
export interface AlertsConfigType {
|
||||
healthCheck: {
|
||||
interval: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type AlertTypeRegistry = PublicMethodsOf<OrigAlertTypeRegistry>;
|
||||
|
|
|
@ -9,6 +9,7 @@ import { getFindResultStatus, ruleStatusRequest, getResult } from '../__mocks__/
|
|||
import { serverMock, requestContextMock, requestMock } from '../__mocks__';
|
||||
import { findRulesStatusesRoute } from './find_rules_status_route';
|
||||
import { RuleStatusResponse } from '../../rules/types';
|
||||
import { AlertExecutionStatusErrorReasons } from '../../../../../../alerts/common';
|
||||
|
||||
jest.mock('../../signals/rule_status_service');
|
||||
|
||||
|
@ -57,7 +58,7 @@ describe('find_statuses', () => {
|
|||
status: 'error',
|
||||
lastExecutionDate: failingExecutionRule.executionStatus.lastExecutionDate,
|
||||
error: {
|
||||
reason: 'read',
|
||||
reason: AlertExecutionStatusErrorReasons.Read,
|
||||
message: 'oops',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -27,6 +27,7 @@ import {
|
|||
import { responseMock } from './__mocks__';
|
||||
import { exampleRuleStatus, exampleFindRuleStatusResponse } from '../signals/__mocks__/es_results';
|
||||
import { getResult } from './__mocks__/request_responses';
|
||||
import { AlertExecutionStatusErrorReasons } from '../../../../../alerts/common';
|
||||
|
||||
let alertsClient: ReturnType<typeof alertsClientMock.create>;
|
||||
|
||||
|
@ -464,7 +465,7 @@ describe('utils', () => {
|
|||
status: 'error',
|
||||
lastExecutionDate: foundRule.executionStatus.lastExecutionDate,
|
||||
error: {
|
||||
reason: 'read',
|
||||
reason: AlertExecutionStatusErrorReasons.Read,
|
||||
message: 'oops',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -11,7 +11,10 @@ import { Alert, ActionType, ValidationResult } from '../../../../types';
|
|||
import { EuiTitle, EuiBadge, EuiFlexItem, EuiSwitch, EuiButtonEmpty, EuiText } from '@elastic/eui';
|
||||
import { ViewInApp } from './view_in_app';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common';
|
||||
import {
|
||||
AlertExecutionStatusErrorReasons,
|
||||
ALERTS_FEATURE_ID,
|
||||
} from '../../../../../../alerts/common';
|
||||
|
||||
const mockes = coreMock.createSetup();
|
||||
|
||||
|
@ -125,7 +128,7 @@ describe('alert_details', () => {
|
|||
status: 'error',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: {
|
||||
reason: 'unknown',
|
||||
reason: AlertExecutionStatusErrorReasons.Unknown,
|
||||
message: 'test',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -17,7 +17,10 @@ import { AppContextProvider } from '../../../app_context';
|
|||
import { chartPluginMock } from '../../../../../../../../src/plugins/charts/public/mocks';
|
||||
import { dataPluginMock } from '../../../../../../../../src/plugins/data/public/mocks';
|
||||
import { alertingPluginMock } from '../../../../../../alerts/public/mocks';
|
||||
import { ALERTS_FEATURE_ID } from '../../../../../../alerts/common';
|
||||
import {
|
||||
AlertExecutionStatusErrorReasons,
|
||||
ALERTS_FEATURE_ID,
|
||||
} from '../../../../../../alerts/common';
|
||||
import { featuresPluginMock } from '../../../../../../features/public/mocks';
|
||||
|
||||
jest.mock('../../../lib/action_connector_api', () => ({
|
||||
|
@ -245,7 +248,7 @@ describe('alerts_list component with items', () => {
|
|||
status: 'error',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: {
|
||||
reason: 'unknown',
|
||||
reason: AlertExecutionStatusErrorReasons.Unknown,
|
||||
message: 'test',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { AlertExecutionStatusErrorReasons } from '../../../../../plugins/alerts/common';
|
||||
import { Spaces } from '../../scenarios';
|
||||
import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
@ -49,7 +50,7 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
|
|||
|
||||
executionStatus = await waitForStatus(alertId, new Set(['error']));
|
||||
expect(executionStatus.error).to.be.ok();
|
||||
expect(executionStatus.error.reason).to.be('decrypt');
|
||||
expect(executionStatus.error.reason).to.be(AlertExecutionStatusErrorReasons.Decrypt);
|
||||
expect(executionStatus.error.message).to.be('Unable to decrypt attribute "apiKey"');
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue