[Uptime] migrate to observability rules registry (#100699)
* uptime - migrate to observability rules registry * Modify Uptime alert types to work with server rule registry. * Export `RuleType` type for consumption by client plugins. * Add platinum as an option for `minimumLicenseRequired` field of `RuleTypeBase`. * Simplify alert bootstrapping, inherit `RuleType` for alert factories. * update rule field map * adjust rule registery to be created within setup instead of mount * adjust plugin setup to account for rule registry changes * export types from rule registry * move alert action message translations to common * update rule field map * update monitor status public alert model * update tls public alert model * update monitor status alert server model * update tls alert sever model * update server plugin file to scope alerts indices to synthetics * add initContext to server Plugin class * adjust public plugin to register alerts when core start is availabile * update mappings * update asset names * adjust dependencies for alert initialization * adjust duration anomaly server alert model * adjust duration anomaly and monitor status public alert model to account for undefined types * add duration_anomaly tests * add anomaly severity * adjust types * update uptime server plugin * remove test_helpers * add getMonitorRouteFromMonitorId helper * export AlertTypeWithExecutor from rule_registry * adjust types * mock time zone * update types * update types for legacy tls alert * update mappings * update monitor status check tls types * update tls types and indexed fields * update duration anomaly types and indexed fields * update mappings * delete unnecessary file * adjust types * adjust ruleDataClient initialization * index anomaly bucket span * update types * adjust registration of legacy tls alert type * adjust types * update index alias name * update anomaly detection rule mappings * adjust import for certificate alert * adjust rbac settings * adjust content * adjust uptime server plugin Co-authored-by: Justin Kambic <justin.kambic@elastic.co> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
ce5a798fdd
commit
743c8c2a14
|
@ -14,13 +14,17 @@ export type { RacRequestHandlerContext, RacApiRequestHandlerContext } from './ty
|
|||
export { RuleDataClient } from './rule_data_client';
|
||||
export { IRuleDataClient } from './rule_data_client/types';
|
||||
export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data';
|
||||
export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory';
|
||||
export {
|
||||
createLifecycleRuleTypeFactory,
|
||||
LifecycleAlertService,
|
||||
} from './utils/create_lifecycle_rule_type_factory';
|
||||
export {
|
||||
LifecycleRuleExecutor,
|
||||
LifecycleAlertServices,
|
||||
createLifecycleExecutor,
|
||||
} from './utils/create_lifecycle_executor';
|
||||
export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory';
|
||||
export type { AlertTypeWithExecutor } from './types';
|
||||
|
||||
export const plugin = (initContext: PluginInitializerContext) =>
|
||||
new RuleRegistryPlugin(initContext);
|
||||
|
|
|
@ -16,10 +16,13 @@ import { AlertInstance } from '../../../alerting/server';
|
|||
import { AlertTypeWithExecutor } from '../types';
|
||||
import { createLifecycleExecutor } from './create_lifecycle_executor';
|
||||
|
||||
export type LifecycleAlertService<TAlertInstanceContext extends Record<string, unknown>> = (alert: {
|
||||
export type LifecycleAlertService<
|
||||
TAlertInstanceContext extends Record<string, unknown>,
|
||||
TActionGroupIds extends string = string
|
||||
> = (alert: {
|
||||
id: string;
|
||||
fields: Record<string, unknown>;
|
||||
}) => AlertInstance<AlertInstanceState, TAlertInstanceContext, string>;
|
||||
}) => AlertInstance<AlertInstanceState, TAlertInstanceContext, TActionGroupIds>;
|
||||
|
||||
export const createLifecycleRuleTypeFactory = ({
|
||||
logger,
|
||||
|
|
|
@ -19,6 +19,7 @@ export const mapConsumerToIndexName = {
|
|||
infrastructure: '.alerts-observability.metrics',
|
||||
observability: '.alerts-observability',
|
||||
siem: ['.alerts-security.alerts', '.siem-signals'],
|
||||
synthetics: '.alerts-observability-synthetics',
|
||||
};
|
||||
export type ValidFeatureId = keyof typeof mapConsumerToIndexName;
|
||||
|
||||
|
|
60
x-pack/plugins/uptime/common/rules/uptime_rule_field_map.ts
Normal file
60
x-pack/plugins/uptime/common/rules/uptime_rule_field_map.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* 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 uptimeRuleFieldMap = {
|
||||
// common fields
|
||||
'monitor.id': {
|
||||
type: 'keyword',
|
||||
},
|
||||
'url.full': {
|
||||
type: 'keyword',
|
||||
},
|
||||
'observer.geo.name': {
|
||||
type: 'keyword',
|
||||
},
|
||||
reason: {
|
||||
type: 'text',
|
||||
},
|
||||
// monitor status alert fields
|
||||
'error.message': {
|
||||
type: 'text',
|
||||
},
|
||||
'agent.name': {
|
||||
type: 'keyword',
|
||||
},
|
||||
'monitor.name': {
|
||||
type: 'keyword',
|
||||
},
|
||||
'monitor.type': {
|
||||
type: 'keyword',
|
||||
},
|
||||
// tls alert fields
|
||||
'tls.server.x509.issuer.common_name': {
|
||||
type: 'keyword',
|
||||
},
|
||||
'tls.server.x509.subject.common_name': {
|
||||
type: 'keyword',
|
||||
},
|
||||
'tls.server.x509.not_after': {
|
||||
type: 'date',
|
||||
},
|
||||
'tls.server.x509.not_before': {
|
||||
type: 'date',
|
||||
},
|
||||
'tls.server.hash.sha256': {
|
||||
type: 'keyword',
|
||||
},
|
||||
// anomaly alert fields
|
||||
'anomaly.start': {
|
||||
type: 'date',
|
||||
},
|
||||
'anomaly.bucket_span.minutes': {
|
||||
type: 'keyword',
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type UptimeRuleFieldMap = typeof uptimeRuleFieldMap;
|
|
@ -37,3 +37,78 @@ export const MonitorStatusTranslations = {
|
|||
defaultMessage: 'Alert when a monitor is down or an availability threshold is breached.',
|
||||
}),
|
||||
};
|
||||
|
||||
export const TlsTranslations = {
|
||||
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', {
|
||||
defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary}
|
||||
`,
|
||||
values: {
|
||||
commonName: '{{state.commonName}}',
|
||||
issuer: '{{state.issuer}}',
|
||||
summary: '{{state.summary}}',
|
||||
status: '{{state.status}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', {
|
||||
defaultMessage: 'Uptime TLS (Legacy)',
|
||||
}),
|
||||
description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', {
|
||||
defaultMessage:
|
||||
'Alert when the TLS certificate of an Uptime monitor is about to expire. This alert will be deprecated in a future version.',
|
||||
}),
|
||||
};
|
||||
|
||||
export const TlsTranslationsLegacy = {
|
||||
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', {
|
||||
defaultMessage: `Detected {count} TLS certificates expiring or becoming too old.
|
||||
{expiringConditionalOpen}
|
||||
Expiring cert count: {expiringCount}
|
||||
Expiring Certificates: {expiringCommonNameAndDate}
|
||||
{expiringConditionalClose}
|
||||
{agingConditionalOpen}
|
||||
Aging cert count: {agingCount}
|
||||
Aging Certificates: {agingCommonNameAndDate}
|
||||
{agingConditionalClose}
|
||||
`,
|
||||
values: {
|
||||
count: '{{state.count}}',
|
||||
expiringCount: '{{state.expiringCount}}',
|
||||
expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}',
|
||||
expiringConditionalOpen: '{{#state.hasExpired}}',
|
||||
expiringConditionalClose: '{{/state.hasExpired}}',
|
||||
agingCount: '{{state.agingCount}}',
|
||||
agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}',
|
||||
agingConditionalOpen: '{{#state.hasAging}}',
|
||||
agingConditionalClose: '{{/state.hasAging}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.uptime.alerts.tls.clientName', {
|
||||
defaultMessage: 'Uptime TLS',
|
||||
}),
|
||||
description: i18n.translate('xpack.uptime.alerts.tls.description', {
|
||||
defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.',
|
||||
}),
|
||||
};
|
||||
|
||||
export const DurationAnomalyTranslations = {
|
||||
defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', {
|
||||
defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}.
|
||||
Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`,
|
||||
values: {
|
||||
severity: '{{state.severity}}',
|
||||
anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}',
|
||||
monitor: '{{state.monitor}}',
|
||||
monitorUrl: '{{{state.monitorUrl}}}',
|
||||
slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}',
|
||||
expectedResponseTime: '{{state.expectedResponseTime}}',
|
||||
severityScore: '{{state.severityScore}}',
|
||||
observerLocation: '{{state.observerLocation}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', {
|
||||
defaultMessage: 'Uptime Duration Anomaly',
|
||||
}),
|
||||
description: i18n.translate('xpack.uptime.alerts.durationAnomaly.description', {
|
||||
defaultMessage: 'Alert when the Uptime monitor duration is anaomalous.',
|
||||
}),
|
||||
};
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
{
|
||||
"configPath": ["xpack", "uptime"],
|
||||
"configPath": [
|
||||
"xpack",
|
||||
"uptime"
|
||||
],
|
||||
"id": "uptime",
|
||||
"kibanaVersion": "kibana",
|
||||
"optionalPlugins": ["data", "home", "ml", "fleet"],
|
||||
"optionalPlugins": [
|
||||
"data",
|
||||
"home",
|
||||
"ml",
|
||||
"fleet"
|
||||
],
|
||||
"requiredPlugins": [
|
||||
"alerting",
|
||||
"embeddable",
|
||||
|
@ -10,15 +18,24 @@
|
|||
"licensing",
|
||||
"triggersActionsUi",
|
||||
"usageCollection",
|
||||
"ruleRegistry",
|
||||
"observability"
|
||||
],
|
||||
"server": true,
|
||||
"ui": true,
|
||||
"version": "8.0.0",
|
||||
"requiredBundles": ["observability", "kibanaReact", "kibanaUtils", "home", "data", "ml", "fleet"],
|
||||
"requiredBundles": [
|
||||
"observability",
|
||||
"kibanaReact",
|
||||
"kibanaUtils",
|
||||
"home",
|
||||
"data",
|
||||
"ml",
|
||||
"fleet"
|
||||
],
|
||||
"owner": {
|
||||
"name": "Uptime",
|
||||
"githubTeam": "uptime"
|
||||
},
|
||||
"description": "This plugin visualizes data from Synthetics and Heartbeat, and integrates with other Observability solutions."
|
||||
}
|
||||
}
|
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
|
@ -29,7 +28,7 @@ import {
|
|||
DataPublicPluginSetup,
|
||||
DataPublicPluginStart,
|
||||
} from '../../../../../src/plugins/data/public';
|
||||
import { alertTypeInitializers } from '../lib/alert_types';
|
||||
import { alertTypeInitializers, legacyAlertTypeInitializers } from '../lib/alert_types';
|
||||
import { FleetStart } from '../../../fleet/public';
|
||||
import {
|
||||
FetchDataParams,
|
||||
|
@ -141,6 +140,36 @@ export class UptimePlugin
|
|||
)
|
||||
);
|
||||
|
||||
const { observabilityRuleTypeRegistry } = plugins.observability;
|
||||
|
||||
core.getStartServices().then(([coreStart, clientPluginsStart]) => {
|
||||
alertTypeInitializers.forEach((init) => {
|
||||
const alertInitializer = init({
|
||||
core: coreStart,
|
||||
plugins: clientPluginsStart,
|
||||
});
|
||||
if (
|
||||
clientPluginsStart.triggersActionsUi &&
|
||||
!clientPluginsStart.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id)
|
||||
) {
|
||||
observabilityRuleTypeRegistry.register(alertInitializer);
|
||||
}
|
||||
});
|
||||
|
||||
legacyAlertTypeInitializers.forEach((init) => {
|
||||
const alertInitializer = init({
|
||||
core: coreStart,
|
||||
plugins: clientPluginsStart,
|
||||
});
|
||||
if (
|
||||
clientPluginsStart.triggersActionsUi &&
|
||||
!clientPluginsStart.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id)
|
||||
) {
|
||||
plugins.triggersActionsUi.alertTypeRegistry.register(alertInitializer);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
core.application.register({
|
||||
id: PLUGIN.ID,
|
||||
euiIconType: 'logoObservability',
|
||||
|
@ -171,26 +200,12 @@ export class UptimePlugin
|
|||
const [coreStart, corePlugins] = await core.getStartServices();
|
||||
|
||||
const { renderApp } = await import('./render_app');
|
||||
|
||||
return renderApp(coreStart, plugins, corePlugins, params);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public start(start: CoreStart, plugins: ClientPluginsStart): void {
|
||||
alertTypeInitializers.forEach((init) => {
|
||||
const alertInitializer = init({
|
||||
core: start,
|
||||
plugins,
|
||||
});
|
||||
if (
|
||||
plugins.triggersActionsUi &&
|
||||
!plugins.triggersActionsUi.alertTypeRegistry.has(alertInitializer.id)
|
||||
) {
|
||||
plugins.triggersActionsUi.alertTypeRegistry.register(alertInitializer);
|
||||
}
|
||||
});
|
||||
|
||||
if (plugins.fleet) {
|
||||
const { registerExtension } = plugins.fleet;
|
||||
|
||||
|
|
40
x-pack/plugins/uptime/public/lib/alert_types/common.ts
Normal file
40
x-pack/plugins/uptime/public/lib/alert_types/common.ts
Normal file
|
@ -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.
|
||||
*/
|
||||
|
||||
import { stringify } from 'querystring';
|
||||
|
||||
export const format = ({
|
||||
pathname,
|
||||
query,
|
||||
}: {
|
||||
pathname: string;
|
||||
query: Record<string, any>;
|
||||
}): string => {
|
||||
return `${pathname}?${stringify(query)}`;
|
||||
};
|
||||
|
||||
export const getMonitorRouteFromMonitorId = ({
|
||||
monitorId,
|
||||
dateRangeStart,
|
||||
dateRangeEnd,
|
||||
filters = {},
|
||||
}: {
|
||||
monitorId: string;
|
||||
dateRangeStart: string;
|
||||
dateRangeEnd: string;
|
||||
filters?: Record<string, string[]>;
|
||||
}) =>
|
||||
format({
|
||||
pathname: `/app/uptime/monitor/${btoa(monitorId)}`,
|
||||
query: {
|
||||
dateRangeEnd,
|
||||
dateRangeStart,
|
||||
...(Object.keys(filters).length
|
||||
? { filters: JSON.stringify(Object.keys(filters).map((key) => [key, filters[key]])) }
|
||||
: {}),
|
||||
},
|
||||
});
|
|
@ -6,18 +6,23 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||
import moment from 'moment';
|
||||
|
||||
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
|
||||
import { DurationAnomalyTranslations } from './translations';
|
||||
import { DurationAnomalyTranslations } from '../../../common/translations';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
|
||||
import { getMonitorRouteFromMonitorId } from './common';
|
||||
|
||||
import { ObservabilityRuleTypeModel } from '../../../../observability/public';
|
||||
|
||||
const { defaultActionMessage, description } = DurationAnomalyTranslations;
|
||||
const DurationAnomalyAlert = React.lazy(() => import('./lazy_wrapper/duration_anomaly'));
|
||||
|
||||
export const initDurationAnomalyAlertType: AlertTypeInitializer = ({
|
||||
core,
|
||||
plugins,
|
||||
}): AlertTypeModel => ({
|
||||
}): ObservabilityRuleTypeModel => ({
|
||||
id: CLIENT_ALERT_TYPES.DURATION_ANOMALY,
|
||||
iconClass: 'uptimeApp',
|
||||
documentationUrl(docLinks) {
|
||||
|
@ -30,4 +35,13 @@ export const initDurationAnomalyAlertType: AlertTypeInitializer = ({
|
|||
validate: () => ({ errors: {} }),
|
||||
defaultActionMessage,
|
||||
requiresAppContext: true,
|
||||
format: ({ fields }) => ({
|
||||
reason: fields.reason,
|
||||
link: getMonitorRouteFromMonitorId({
|
||||
monitorId: fields['monitor.id']!,
|
||||
dateRangeEnd:
|
||||
fields['kibana.rac.alert.status'] === 'open' ? 'now' : fields['kibana.rac.alert.end']!,
|
||||
dateRangeStart: moment(new Date(fields['anomaly.start']!)).subtract('5', 'm').toISOString(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { ObservabilityRuleTypeModel } from '../../../../observability/public';
|
||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||
import { initMonitorStatusAlertType } from './monitor_status';
|
||||
import { initTlsAlertType } from './tls';
|
||||
|
@ -13,14 +14,17 @@ import { initTlsLegacyAlertType } from './tls_legacy';
|
|||
import { ClientPluginsStart } from '../../apps/plugin';
|
||||
import { initDurationAnomalyAlertType } from './duration_anomaly';
|
||||
|
||||
export type AlertTypeInitializer = (dependenies: {
|
||||
export type AlertTypeInitializer<TAlertTypeModel = ObservabilityRuleTypeModel> = (dependenies: {
|
||||
core: CoreStart;
|
||||
plugins: ClientPluginsStart;
|
||||
}) => AlertTypeModel;
|
||||
}) => TAlertTypeModel;
|
||||
|
||||
export const alertTypeInitializers: AlertTypeInitializer[] = [
|
||||
initMonitorStatusAlertType,
|
||||
initTlsAlertType,
|
||||
initTlsLegacyAlertType,
|
||||
initDurationAnomalyAlertType,
|
||||
];
|
||||
|
||||
export const legacyAlertTypeInitializers: Array<AlertTypeInitializer<AlertTypeModel>> = [
|
||||
initTlsLegacyAlertType,
|
||||
];
|
||||
|
|
|
@ -206,6 +206,7 @@ describe('monitor status alert type', () => {
|
|||
"defaultActionMessage": "Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}",
|
||||
"description": "Alert when a monitor is down or an availability threshold is breached.",
|
||||
"documentationUrl": [Function],
|
||||
"format": [Function],
|
||||
"iconClass": "uptimeApp",
|
||||
"id": "xpack.uptime.alerts.monitorStatus",
|
||||
"requiresAppContext": false,
|
||||
|
|
|
@ -6,12 +6,18 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AlertTypeModel, ValidationResult } from '../../../../triggers_actions_ui/public';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
import moment from 'moment';
|
||||
|
||||
import { ObservabilityRuleTypeModel } from '../../../../observability/public';
|
||||
import { ValidationResult } from '../../../../triggers_actions_ui/public';
|
||||
|
||||
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
|
||||
import { MonitorStatusTranslations } from '../../../common/translations';
|
||||
|
||||
import { getMonitorRouteFromMonitorId } from './common';
|
||||
|
||||
import { AlertTypeInitializer } from '.';
|
||||
|
||||
const { defaultActionMessage, description } = MonitorStatusTranslations;
|
||||
|
||||
const MonitorStatusAlert = React.lazy(() => import('./lazy_wrapper/monitor_status'));
|
||||
|
@ -21,7 +27,7 @@ let validateFunc: (alertParams: any) => ValidationResult;
|
|||
export const initMonitorStatusAlertType: AlertTypeInitializer = ({
|
||||
core,
|
||||
plugins,
|
||||
}): AlertTypeModel => ({
|
||||
}): ObservabilityRuleTypeModel => ({
|
||||
id: CLIENT_ALERT_TYPES.MONITOR_STATUS,
|
||||
description,
|
||||
iconClass: 'uptimeApp',
|
||||
|
@ -44,4 +50,18 @@ export const initMonitorStatusAlertType: AlertTypeInitializer = ({
|
|||
},
|
||||
defaultActionMessage,
|
||||
requiresAppContext: false,
|
||||
format: ({ fields }) => ({
|
||||
reason: fields.reason,
|
||||
link: getMonitorRouteFromMonitorId({
|
||||
monitorId: fields['monitor.id']!,
|
||||
dateRangeEnd:
|
||||
fields['kibana.rac.alert.status'] === 'open' ? 'now' : fields['kibana.rac.alert.end']!,
|
||||
dateRangeStart: moment(new Date(fields['kibana.rac.alert.start']!))
|
||||
.subtract('5', 'm')
|
||||
.toISOString(),
|
||||
filters: {
|
||||
'observer.geo.name': [fields['observer.geo.name'][0]],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -6,14 +6,19 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||
import { ObservabilityRuleTypeModel } from '../../../../observability/public';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
|
||||
import { TlsTranslations } from './translations';
|
||||
import { TlsTranslations } from '../../../common/translations';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
|
||||
import { CERTIFICATES_ROUTE } from '../../../common/constants/ui';
|
||||
|
||||
const { defaultActionMessage, description } = TlsTranslations;
|
||||
const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert'));
|
||||
export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): AlertTypeModel => ({
|
||||
export const initTlsAlertType: AlertTypeInitializer = ({
|
||||
core,
|
||||
plugins,
|
||||
}): ObservabilityRuleTypeModel => ({
|
||||
id: CLIENT_ALERT_TYPES.TLS,
|
||||
iconClass: 'uptimeApp',
|
||||
documentationUrl(docLinks) {
|
||||
|
@ -26,4 +31,8 @@ export const initTlsAlertType: AlertTypeInitializer = ({ core, plugins }): Alert
|
|||
validate: () => ({ errors: {} }),
|
||||
defaultActionMessage,
|
||||
requiresAppContext: false,
|
||||
format: ({ fields }) => ({
|
||||
reason: fields.reason,
|
||||
link: `/app/uptime${CERTIFICATES_ROUTE}`,
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
import React from 'react';
|
||||
import { AlertTypeModel } from '../../../../triggers_actions_ui/public';
|
||||
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
|
||||
import { TlsTranslationsLegacy } from './translations';
|
||||
import { TlsTranslationsLegacy } from '../../../common/translations';
|
||||
import { AlertTypeInitializer } from '.';
|
||||
|
||||
const { defaultActionMessage, description } = TlsTranslationsLegacy;
|
||||
const TLSAlert = React.lazy(() => import('./lazy_wrapper/tls_alert'));
|
||||
export const initTlsLegacyAlertType: AlertTypeInitializer = ({
|
||||
export const initTlsLegacyAlertType: AlertTypeInitializer<AlertTypeModel> = ({
|
||||
core,
|
||||
plugins,
|
||||
}): AlertTypeModel => ({
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const TlsTranslations = {
|
||||
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.defaultActionMessage', {
|
||||
defaultMessage: `Detected TLS certificate {commonName} from issuer {issuer} is {status}. Certificate {summary}
|
||||
`,
|
||||
values: {
|
||||
commonName: '{{state.commonName}}',
|
||||
issuer: '{{state.issuer}}',
|
||||
summary: '{{state.summary}}',
|
||||
status: '{{state.status}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.uptime.alerts.tls.clientName', {
|
||||
defaultMessage: 'Uptime TLS',
|
||||
}),
|
||||
description: i18n.translate('xpack.uptime.alerts.tls.description', {
|
||||
defaultMessage: 'Alert when the TLS certificate of an Uptime monitor is about to expire.',
|
||||
}),
|
||||
};
|
||||
|
||||
export const TlsTranslationsLegacy = {
|
||||
defaultActionMessage: i18n.translate('xpack.uptime.alerts.tls.legacy.defaultActionMessage', {
|
||||
defaultMessage: `Detected {count} TLS certificates expiring or becoming too old.
|
||||
{expiringConditionalOpen}
|
||||
Expiring cert count: {expiringCount}
|
||||
Expiring Certificates: {expiringCommonNameAndDate}
|
||||
{expiringConditionalClose}
|
||||
{agingConditionalOpen}
|
||||
Aging cert count: {agingCount}
|
||||
Aging Certificates: {agingCommonNameAndDate}
|
||||
{agingConditionalClose}
|
||||
`,
|
||||
values: {
|
||||
count: '{{state.count}}',
|
||||
expiringCount: '{{state.expiringCount}}',
|
||||
expiringCommonNameAndDate: '{{state.expiringCommonNameAndDate}}',
|
||||
expiringConditionalOpen: '{{#state.hasExpired}}',
|
||||
expiringConditionalClose: '{{/state.hasExpired}}',
|
||||
agingCount: '{{state.agingCount}}',
|
||||
agingCommonNameAndDate: '{{state.agingCommonNameAndDate}}',
|
||||
agingConditionalOpen: '{{#state.hasAging}}',
|
||||
agingConditionalClose: '{{/state.hasAging}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.uptime.alerts.tls.legacy.clientName', {
|
||||
defaultMessage: 'Uptime TLS',
|
||||
}),
|
||||
description: i18n.translate('xpack.uptime.alerts.tls.legacy.description', {
|
||||
defaultMessage:
|
||||
'Alert when the TLS certificate of an Uptime monitor is about to expire. This rule type will be deprecated in a future version.',
|
||||
}),
|
||||
};
|
||||
|
||||
export const DurationAnomalyTranslations = {
|
||||
defaultActionMessage: i18n.translate('xpack.uptime.alerts.durationAnomaly.defaultActionMessage', {
|
||||
defaultMessage: `Abnormal ({severity} level) response time detected on {monitor} with url {monitorUrl} at {anomalyStartTimestamp}. Anomaly severity score is {severityScore}.
|
||||
Response times as high as {slowestAnomalyResponse} have been detected from location {observerLocation}. Expected response time is {expectedResponseTime}.`,
|
||||
values: {
|
||||
severity: '{{state.severity}}',
|
||||
anomalyStartTimestamp: '{{state.anomalyStartTimestamp}}',
|
||||
monitor: '{{state.monitor}}',
|
||||
monitorUrl: '{{{state.monitorUrl}}}',
|
||||
slowestAnomalyResponse: '{{state.slowestAnomalyResponse}}',
|
||||
expectedResponseTime: '{{state.expectedResponseTime}}',
|
||||
severityScore: '{{state.severityScore}}',
|
||||
observerLocation: '{{state.observerLocation}}',
|
||||
},
|
||||
}),
|
||||
name: i18n.translate('xpack.uptime.alerts.durationAnomaly.clientName', {
|
||||
defaultMessage: 'Uptime Duration Anomaly',
|
||||
}),
|
||||
description: i18n.translate('xpack.uptime.alerts.durationAnomaly.description', {
|
||||
defaultMessage: 'Alert when the Uptime monitor duration is anaomalous.',
|
||||
}),
|
||||
};
|
|
@ -6,12 +6,14 @@
|
|||
*/
|
||||
|
||||
import { Request, Server } from '@hapi/hapi';
|
||||
import { Logger } from 'kibana/server';
|
||||
import { DEFAULT_APP_CATEGORIES } from '../../../../src/core/server';
|
||||
import { PLUGIN } from '../common/constants/plugin';
|
||||
import { compose } from './lib/compose/kibana';
|
||||
import { initUptimeServer } from './uptime_server';
|
||||
import { UptimeCorePlugins, UptimeCoreSetup } from './lib/adapters/framework';
|
||||
import { umDynamicSettings } from './lib/saved_objects';
|
||||
import { UptimeRuleRegistry } from './plugin';
|
||||
|
||||
export interface KibanaRouteOptions {
|
||||
path: string;
|
||||
|
@ -25,7 +27,12 @@ export interface KibanaServer extends Server {
|
|||
route: (options: KibanaRouteOptions) => void;
|
||||
}
|
||||
|
||||
export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCorePlugins) => {
|
||||
export const initServerWithKibana = (
|
||||
server: UptimeCoreSetup,
|
||||
plugins: UptimeCorePlugins,
|
||||
ruleRegistry: UptimeRuleRegistry,
|
||||
logger: Logger
|
||||
) => {
|
||||
const { features } = plugins;
|
||||
const libs = compose(server);
|
||||
|
||||
|
@ -86,5 +93,5 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor
|
|||
},
|
||||
});
|
||||
|
||||
initUptimeServer(server, libs, plugins);
|
||||
initUptimeServer(server, libs, plugins, ruleRegistry, logger);
|
||||
};
|
||||
|
|
|
@ -11,9 +11,11 @@ import type {
|
|||
ISavedObjectsRepository,
|
||||
IScopedClusterClient,
|
||||
} from 'src/core/server';
|
||||
import { ObservabilityPluginSetup } from '../../../../../observability/server';
|
||||
import { UMKibanaRoute } from '../../../rest_api';
|
||||
import { PluginSetupContract } from '../../../../../features/server';
|
||||
import { MlPluginSetup as MlSetup } from '../../../../../ml/server';
|
||||
import { RuleRegistryPluginSetupContract } from '../../../../../rule_registry/server';
|
||||
import { UptimeESClient } from '../../lib';
|
||||
import type { UptimeRouter } from '../../../types';
|
||||
|
||||
|
@ -37,8 +39,10 @@ export interface UptimeCorePlugins {
|
|||
features: PluginSetupContract;
|
||||
alerting: any;
|
||||
elasticsearch: any;
|
||||
observability: ObservabilityPluginSetup;
|
||||
usageCollection: UsageCollectionSetup;
|
||||
ml: MlSetup;
|
||||
ruleRegistry: RuleRegistryPluginSetupContract;
|
||||
}
|
||||
|
||||
export interface UMBackendFrameworkAdapter {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { isRight } from 'fp-ts/lib/Either';
|
||||
import Mustache from 'mustache';
|
||||
import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types';
|
||||
|
||||
export type UpdateUptimeAlertState = (
|
||||
|
@ -55,3 +56,7 @@ export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => {
|
|||
isTriggered: isTriggeredNow,
|
||||
};
|
||||
};
|
||||
|
||||
export const generateAlertMessage = (messageTemplate: string, fields: Record<string, any>) => {
|
||||
return Mustache.render(messageTemplate, { state: { ...fields } });
|
||||
};
|
||||
|
|
210
x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts
Normal file
210
x-pack/plugins/uptime/server/lib/alerts/duration_anomaly.test.ts
Normal file
|
@ -0,0 +1,210 @@
|
|||
/*
|
||||
* 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 { durationAnomalyAlertFactory } from './duration_anomaly';
|
||||
import { DURATION_ANOMALY } from '../../../common/constants/alerts';
|
||||
import { AnomaliesTableRecord, AnomalyRecordDoc } from '../../../../ml/common/types/anomalies';
|
||||
import { DynamicSettings } from '../../../common/runtime_types';
|
||||
import { createRuleTypeMocks, bootstrapDependencies } from './test_utils';
|
||||
import { getSeverityType } from '../../../../ml/common/util/anomaly_utils';
|
||||
import { Ping } from '../../../common/runtime_types/ping';
|
||||
import {
|
||||
ALERT_SEVERITY_LEVEL,
|
||||
ALERT_SEVERITY_VALUE,
|
||||
ALERT_EVALUATION_VALUE,
|
||||
ALERT_EVALUATION_THRESHOLD,
|
||||
} from '@kbn/rule-data-utils/target/technical_field_names';
|
||||
|
||||
interface MockAnomaly {
|
||||
severity: AnomaliesTableRecord['severity'];
|
||||
source: Partial<AnomalyRecordDoc>;
|
||||
actualSort: AnomaliesTableRecord['actualSort'];
|
||||
typicalSort: AnomaliesTableRecord['typicalSort'];
|
||||
entityValue: AnomaliesTableRecord['entityValue'];
|
||||
}
|
||||
|
||||
interface MockAnomalyResult {
|
||||
anomalies: MockAnomaly[];
|
||||
}
|
||||
|
||||
const monitorId = 'uptime-monitor';
|
||||
const mockUrl = 'https://elastic.co';
|
||||
|
||||
/**
|
||||
* This function aims to provide an easy way to give mock props that will
|
||||
* reduce boilerplate for tests.
|
||||
* @param dynamic the expiration and aging thresholds received at alert creation time
|
||||
* @param params the params received at alert creation time
|
||||
* @param state the state the alert maintains
|
||||
*/
|
||||
const mockOptions = (
|
||||
dynamicCertSettings?: {
|
||||
certExpirationThreshold: DynamicSettings['certExpirationThreshold'];
|
||||
certAgeThreshold: DynamicSettings['certAgeThreshold'];
|
||||
},
|
||||
state = {},
|
||||
params = {
|
||||
timerange: { from: 'now-15m', to: 'now' },
|
||||
monitorId,
|
||||
severity: 'warning',
|
||||
}
|
||||
): any => {
|
||||
const { services } = createRuleTypeMocks(dynamicCertSettings);
|
||||
|
||||
return {
|
||||
params,
|
||||
state,
|
||||
services,
|
||||
};
|
||||
};
|
||||
|
||||
const mockAnomaliesResult: MockAnomalyResult = {
|
||||
anomalies: [
|
||||
{
|
||||
severity: 25,
|
||||
source: {
|
||||
timestamp: 1622137799,
|
||||
'monitor.id': 'uptime-monitor',
|
||||
bucket_span: 900,
|
||||
},
|
||||
actualSort: 200000,
|
||||
typicalSort: 10000,
|
||||
entityValue: 'harrisburg',
|
||||
},
|
||||
{
|
||||
severity: 10,
|
||||
source: {
|
||||
timestamp: 1622137799,
|
||||
'monitor.id': 'uptime-monitor',
|
||||
bucket_span: 900,
|
||||
},
|
||||
actualSort: 300000,
|
||||
typicalSort: 20000,
|
||||
entityValue: 'fairbanks',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockPing: Partial<Ping> = {
|
||||
url: {
|
||||
full: mockUrl,
|
||||
},
|
||||
};
|
||||
|
||||
describe('duration anomaly alert', () => {
|
||||
let toISOStringSpy: jest.SpyInstance<string, []>;
|
||||
const mockDate = 'date';
|
||||
beforeAll(() => {
|
||||
Date.now = jest.fn().mockReturnValue(new Date('2021-05-13T12:33:37.000Z'));
|
||||
jest.spyOn(Intl, 'DateTimeFormat').mockImplementation(() => ({
|
||||
format: jest.fn(),
|
||||
formatToParts: jest.fn(),
|
||||
resolvedOptions: () => ({
|
||||
locale: '',
|
||||
calendar: '',
|
||||
numberingSystem: '',
|
||||
timeZone: 'UTC',
|
||||
}),
|
||||
}));
|
||||
toISOStringSpy = jest.spyOn(Date.prototype, 'toISOString');
|
||||
});
|
||||
|
||||
describe('alert executor', () => {
|
||||
it('triggers when aging or expiring alerts are found', async () => {
|
||||
toISOStringSpy.mockImplementation(() => mockDate);
|
||||
const mockResultServiceProviderGetter: jest.Mock<{
|
||||
getAnomaliesTableData: jest.Mock<MockAnomalyResult>;
|
||||
}> = jest.fn();
|
||||
const mockGetAnomliesTableDataGetter: jest.Mock<MockAnomalyResult> = jest.fn();
|
||||
const mockGetLatestMonitorGetter: jest.Mock<Partial<Ping>> = jest.fn();
|
||||
|
||||
mockGetLatestMonitorGetter.mockReturnValue(mockPing);
|
||||
mockGetAnomliesTableDataGetter.mockReturnValue(mockAnomaliesResult);
|
||||
mockResultServiceProviderGetter.mockReturnValue({
|
||||
getAnomaliesTableData: mockGetAnomliesTableDataGetter,
|
||||
});
|
||||
const { server, libs, plugins } = bootstrapDependencies(
|
||||
{ getLatestMonitor: mockGetLatestMonitorGetter },
|
||||
{
|
||||
ml: {
|
||||
resultsServiceProvider: mockResultServiceProviderGetter,
|
||||
},
|
||||
}
|
||||
);
|
||||
const alert = durationAnomalyAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions();
|
||||
const {
|
||||
services: { alertWithLifecycle },
|
||||
} = options;
|
||||
// @ts-ignore the executor can return `void`, but ours never does
|
||||
const state: Record<string, any> = await alert.executor(options);
|
||||
expect(mockGetAnomliesTableDataGetter).toHaveBeenCalledTimes(1);
|
||||
expect(alertWithLifecycle).toHaveBeenCalledTimes(2);
|
||||
expect(mockGetAnomliesTableDataGetter).toBeCalledWith(
|
||||
['uptime_monitor_high_latency_by_geo'],
|
||||
[],
|
||||
[],
|
||||
'auto',
|
||||
options.params.severity,
|
||||
1620909217000,
|
||||
1620909217000,
|
||||
'UTC',
|
||||
500,
|
||||
10,
|
||||
undefined
|
||||
);
|
||||
const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
|
||||
expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(2);
|
||||
mockAnomaliesResult.anomalies.forEach((anomaly, index) => {
|
||||
const slowestResponse = Math.round(anomaly.actualSort / 1000);
|
||||
const typicalResponse = Math.round(anomaly.typicalSort / 1000);
|
||||
expect(alertWithLifecycle).toBeCalledWith({
|
||||
fields: {
|
||||
'monitor.id': options.params.monitorId,
|
||||
'url.full': mockPing.url?.full,
|
||||
'anomaly.start': mockDate,
|
||||
'anomaly.bucket_span.minutes': anomaly.source.bucket_span,
|
||||
'observer.geo.name': anomaly.entityValue,
|
||||
[ALERT_EVALUATION_VALUE]: anomaly.actualSort,
|
||||
[ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort,
|
||||
[ALERT_SEVERITY_LEVEL]: getSeverityType(anomaly.severity),
|
||||
[ALERT_SEVERITY_VALUE]: anomaly.severity,
|
||||
reason: `Abnormal (${getSeverityType(
|
||||
anomaly.severity
|
||||
)} level) response time detected on uptime-monitor with url ${
|
||||
mockPing.url?.full
|
||||
} at date. Anomaly severity score is ${anomaly.severity}.
|
||||
Response times as high as ${slowestResponse} ms have been detected from location ${
|
||||
anomaly.entityValue
|
||||
}. Expected response time is ${typicalResponse} ms.`,
|
||||
},
|
||||
id: `${DURATION_ANOMALY.id}${index}`,
|
||||
});
|
||||
expect(alertInstanceMock.replaceState).toBeCalledWith({
|
||||
firstCheckedAt: 'date',
|
||||
firstTriggeredAt: undefined,
|
||||
lastCheckedAt: 'date',
|
||||
lastResolvedAt: undefined,
|
||||
isTriggered: false,
|
||||
anomalyStartTimestamp: 'date',
|
||||
currentTriggerStarted: undefined,
|
||||
expectedResponseTime: `${typicalResponse} ms`,
|
||||
lastTriggeredAt: undefined,
|
||||
monitor: monitorId,
|
||||
monitorUrl: mockPing.url?.full,
|
||||
observerLocation: anomaly.entityValue,
|
||||
severity: getSeverityType(anomaly.severity),
|
||||
severityScore: anomaly.severity,
|
||||
slowestAnomalyResponse: `${slowestResponse} ms`,
|
||||
bucketSpan: anomaly.source.bucket_span,
|
||||
});
|
||||
});
|
||||
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(2);
|
||||
expect(alertInstanceMock.scheduleActions).toBeCalledWith(DURATION_ANOMALY.id);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -8,8 +8,14 @@
|
|||
import { KibanaRequest, SavedObjectsClientContract } from 'kibana/server';
|
||||
import moment from 'moment';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import {
|
||||
ALERT_SEVERITY_LEVEL,
|
||||
ALERT_SEVERITY_VALUE,
|
||||
ALERT_EVALUATION_VALUE,
|
||||
ALERT_EVALUATION_THRESHOLD,
|
||||
} from '@kbn/rule-data-utils/target/technical_field_names';
|
||||
import { ActionGroupIdsOf } from '../../../../alerting/common';
|
||||
import { updateState } from './common';
|
||||
import { updateState, generateAlertMessage } from './common';
|
||||
import { DURATION_ANOMALY } from '../../../common/constants/alerts';
|
||||
import { commonStateTranslations, durationAnomalyTranslations } from './translations';
|
||||
import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies';
|
||||
|
@ -18,8 +24,10 @@ import { UptimeCorePlugins } from '../adapters/framework';
|
|||
import { UptimeAlertTypeFactory } from './types';
|
||||
import { Ping } from '../../../common/runtime_types/ping';
|
||||
import { getMLJobId } from '../../../common/lib';
|
||||
import { getLatestMonitor } from '../requests/get_latest_monitor';
|
||||
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
|
||||
|
||||
import { DurationAnomalyTranslations as CommonDurationAnomalyTranslations } from '../../../common/translations';
|
||||
|
||||
import { createUptimeESClient } from '../lib';
|
||||
|
||||
export type ActionGroupIds = ActionGroupIdsOf<typeof DURATION_ANOMALY>;
|
||||
|
||||
|
@ -33,6 +41,7 @@ export const getAnomalySummary = (anomaly: AnomaliesTableRecord, monitorInfo: Pi
|
|||
slowestAnomalyResponse: Math.round(anomaly.actualSort / 1000) + ' ms',
|
||||
expectedResponseTime: Math.round(anomaly.typicalSort / 1000) + ' ms',
|
||||
observerLocation: anomaly.entityValue,
|
||||
bucketSpan: anomaly.source.bucket_span,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -65,61 +74,83 @@ const getAnomalies = async (
|
|||
|
||||
export const durationAnomalyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
|
||||
_server,
|
||||
_libs,
|
||||
libs,
|
||||
plugins
|
||||
) =>
|
||||
uptimeAlertWrapper<ActionGroupIds>({
|
||||
id: 'xpack.uptime.alerts.durationAnomaly',
|
||||
name: durationAnomalyTranslations.alertFactoryName,
|
||||
validate: {
|
||||
params: schema.object({
|
||||
monitorId: schema.string(),
|
||||
severity: schema.number(),
|
||||
}),
|
||||
) => ({
|
||||
id: 'xpack.uptime.alerts.durationAnomaly',
|
||||
producer: 'uptime',
|
||||
name: durationAnomalyTranslations.alertFactoryName,
|
||||
validate: {
|
||||
params: schema.object({
|
||||
monitorId: schema.string(),
|
||||
severity: schema.number(),
|
||||
}),
|
||||
},
|
||||
defaultActionGroupId: DURATION_ANOMALY.id,
|
||||
actionGroups: [
|
||||
{
|
||||
id: DURATION_ANOMALY.id,
|
||||
name: DURATION_ANOMALY.name,
|
||||
},
|
||||
defaultActionGroupId: DURATION_ANOMALY.id,
|
||||
actionGroups: [
|
||||
{
|
||||
id: DURATION_ANOMALY.id,
|
||||
name: DURATION_ANOMALY.name,
|
||||
},
|
||||
],
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
|
||||
},
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
async executor({ options, uptimeEsClient, savedObjectsClient, dynamicSettings }) {
|
||||
const {
|
||||
services: { alertInstanceFactory },
|
||||
state,
|
||||
params,
|
||||
} = options;
|
||||
],
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
|
||||
},
|
||||
isExportable: true,
|
||||
minimumLicenseRequired: 'platinum',
|
||||
async executor({
|
||||
params,
|
||||
services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient },
|
||||
state,
|
||||
}) {
|
||||
const uptimeEsClient = createUptimeESClient({
|
||||
esClient: scopedClusterClient.asCurrentUser,
|
||||
savedObjectsClient,
|
||||
});
|
||||
const { anomalies } =
|
||||
(await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt as string)) ??
|
||||
{};
|
||||
|
||||
const { anomalies } =
|
||||
(await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt)) ?? {};
|
||||
const foundAnomalies = anomalies?.length > 0;
|
||||
|
||||
const foundAnomalies = anomalies?.length > 0;
|
||||
if (foundAnomalies) {
|
||||
const monitorInfo = await libs.requests.getLatestMonitor({
|
||||
uptimeEsClient,
|
||||
dateStart: 'now-15m',
|
||||
dateEnd: 'now',
|
||||
monitorId: params.monitorId,
|
||||
});
|
||||
|
||||
if (foundAnomalies) {
|
||||
const monitorInfo = await getLatestMonitor({
|
||||
uptimeEsClient,
|
||||
dateStart: 'now-15m',
|
||||
dateEnd: 'now',
|
||||
monitorId: params.monitorId,
|
||||
anomalies.forEach((anomaly, index) => {
|
||||
const summary = getAnomalySummary(anomaly, monitorInfo);
|
||||
|
||||
const alertInstance = alertWithLifecycle({
|
||||
id: DURATION_ANOMALY.id + index,
|
||||
fields: {
|
||||
'monitor.id': params.monitorId,
|
||||
'url.full': summary.monitorUrl,
|
||||
'observer.geo.name': summary.observerLocation,
|
||||
'anomaly.start': summary.anomalyStartTimestamp,
|
||||
'anomaly.bucket_span.minutes': summary.bucketSpan,
|
||||
[ALERT_EVALUATION_VALUE]: anomaly.actualSort,
|
||||
[ALERT_EVALUATION_THRESHOLD]: anomaly.typicalSort,
|
||||
[ALERT_SEVERITY_LEVEL]: summary.severity,
|
||||
[ALERT_SEVERITY_VALUE]: summary.severityScore,
|
||||
reason: generateAlertMessage(
|
||||
CommonDurationAnomalyTranslations.defaultActionMessage,
|
||||
summary
|
||||
),
|
||||
},
|
||||
});
|
||||
anomalies.forEach((anomaly, index) => {
|
||||
const alertInstance = alertInstanceFactory(DURATION_ANOMALY.id + index);
|
||||
const summary = getAnomalySummary(anomaly, monitorInfo);
|
||||
alertInstance.replaceState({
|
||||
...updateState(state, false),
|
||||
...summary,
|
||||
});
|
||||
alertInstance.scheduleActions(DURATION_ANOMALY.id);
|
||||
alertInstance.replaceState({
|
||||
...updateState(state, false),
|
||||
...summary,
|
||||
});
|
||||
}
|
||||
alertInstance.scheduleActions(DURATION_ANOMALY.id);
|
||||
});
|
||||
}
|
||||
|
||||
return updateState(state, foundAnomalies);
|
||||
},
|
||||
});
|
||||
return updateState(state, foundAnomalies);
|
||||
},
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,13 +4,11 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import datemath from '@elastic/datemath';
|
||||
import { min } from 'lodash';
|
||||
import datemath from '@elastic/datemath';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import Mustache from 'mustache';
|
||||
import { JsonObject } from '@kbn/common-utils';
|
||||
import { ActionGroupIdsOf } from '../../../../alerting/common';
|
||||
import { UptimeAlertTypeFactory } from './types';
|
||||
import { esKuery } from '../../../../../../src/plugins/data/server';
|
||||
import {
|
||||
|
@ -19,16 +17,16 @@ import {
|
|||
GetMonitorAvailabilityParams,
|
||||
} from '../../../common/runtime_types';
|
||||
import { MONITOR_STATUS } from '../../../common/constants/alerts';
|
||||
import { updateState } from './common';
|
||||
import { updateState, generateAlertMessage } from './common';
|
||||
import { commonMonitorStateI18, commonStateTranslations, DOWN_LABEL } from './translations';
|
||||
import { stringifyKueries, combineFiltersAndUserSearch } from '../../../common/lib';
|
||||
import { GetMonitorAvailabilityResult } from '../requests/get_monitor_availability';
|
||||
import { GetMonitorStatusResult } from '../requests/get_monitor_status';
|
||||
import { UNNAMED_LOCATION } from '../../../common/constants';
|
||||
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
|
||||
import { MonitorStatusTranslations } from '../../../common/translations';
|
||||
import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/get_index_pattern';
|
||||
import { UMServerLibs, UptimeESClient } from '../lib';
|
||||
import { UMServerLibs, UptimeESClient, createUptimeESClient } from '../lib';
|
||||
import { ActionGroupIdsOf } from '../../../../alerting/common';
|
||||
|
||||
export type ActionGroupIds = ActionGroupIdsOf<typeof MONITOR_STATUS>;
|
||||
|
||||
|
@ -134,8 +132,8 @@ export const formatFilterString = async (
|
|||
search
|
||||
);
|
||||
|
||||
export const getMonitorSummary = (monitorInfo: Ping) => {
|
||||
return {
|
||||
export const getMonitorSummary = (monitorInfo: Ping, statusMessage: string) => {
|
||||
const summary = {
|
||||
monitorUrl: monitorInfo.url?.full,
|
||||
monitorId: monitorInfo.monitor?.id,
|
||||
monitorName: monitorInfo.monitor?.name ?? monitorInfo.monitor?.id,
|
||||
|
@ -144,16 +142,26 @@ export const getMonitorSummary = (monitorInfo: Ping) => {
|
|||
observerLocation: monitorInfo.observer?.geo?.name ?? UNNAMED_LOCATION,
|
||||
observerHostname: monitorInfo.agent?.name,
|
||||
};
|
||||
const reason = generateAlertMessage(MonitorStatusTranslations.defaultActionMessage, {
|
||||
...summary,
|
||||
statusMessage,
|
||||
});
|
||||
return {
|
||||
...summary,
|
||||
reason,
|
||||
};
|
||||
};
|
||||
|
||||
const generateMessageForOlderVersions = (fields: Record<string, any>) => {
|
||||
const messageTemplate = MonitorStatusTranslations.defaultActionMessage;
|
||||
|
||||
// Monitor {{state.monitorName}} with url {{{state.monitorUrl}}} is {{state.statusMessage}} from
|
||||
// {{state.observerLocation}}. The latest error message is {{{state.latestErrorMessage}}}
|
||||
|
||||
return Mustache.render(messageTemplate, { state: { ...fields } });
|
||||
};
|
||||
export const getMonitorAlertDocument = (monitorSummary: Record<string, string | undefined>) => ({
|
||||
'monitor.id': monitorSummary.monitorId,
|
||||
'monitor.type': monitorSummary.monitorType,
|
||||
'monitor.name': monitorSummary.monitorName,
|
||||
'url.full': monitorSummary.monitorUrl,
|
||||
'observer.geo.name': monitorSummary.observerLocation,
|
||||
'error.message': monitorSummary.latestErrorMessage,
|
||||
'agent.name': monitorSummary.observerHostname,
|
||||
reason: monitorSummary.reason,
|
||||
});
|
||||
|
||||
export const getStatusMessage = (
|
||||
downMonInfo?: Ping,
|
||||
|
@ -194,7 +202,7 @@ export const getStatusMessage = (
|
|||
return statusMessage + availabilityMessage;
|
||||
};
|
||||
|
||||
const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
|
||||
export const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
|
||||
const normalizeText = (txt: string) => {
|
||||
// replace url and name special characters with -
|
||||
return txt.replace(/[^A-Z0-9]+/gi, '_').toLowerCase();
|
||||
|
@ -209,200 +217,204 @@ const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
|
|||
return `${urlText}_${monIdByLoc}`;
|
||||
};
|
||||
|
||||
export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) =>
|
||||
uptimeAlertWrapper<ActionGroupIds>({
|
||||
id: 'xpack.uptime.alerts.monitorStatus',
|
||||
name: i18n.translate('xpack.uptime.alerts.monitorStatus', {
|
||||
defaultMessage: 'Uptime monitor status',
|
||||
export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) => ({
|
||||
id: 'xpack.uptime.alerts.monitorStatus',
|
||||
producer: 'uptime',
|
||||
name: i18n.translate('xpack.uptime.alerts.monitorStatus', {
|
||||
defaultMessage: 'Uptime monitor status',
|
||||
}),
|
||||
validate: {
|
||||
params: schema.object({
|
||||
availability: schema.maybe(
|
||||
schema.object({
|
||||
range: schema.number(),
|
||||
rangeUnit: schema.string(),
|
||||
threshold: schema.string(),
|
||||
})
|
||||
),
|
||||
filters: schema.maybe(
|
||||
schema.oneOf([
|
||||
// deprecated
|
||||
schema.object({
|
||||
'monitor.type': schema.maybe(schema.arrayOf(schema.string())),
|
||||
'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())),
|
||||
tags: schema.maybe(schema.arrayOf(schema.string())),
|
||||
'url.port': schema.maybe(schema.arrayOf(schema.string())),
|
||||
}),
|
||||
schema.string(),
|
||||
])
|
||||
),
|
||||
// deprecated
|
||||
locations: schema.maybe(schema.arrayOf(schema.string())),
|
||||
numTimes: schema.number(),
|
||||
search: schema.maybe(schema.string()),
|
||||
shouldCheckStatus: schema.boolean(),
|
||||
shouldCheckAvailability: schema.boolean(),
|
||||
timerangeCount: schema.maybe(schema.number()),
|
||||
timerangeUnit: schema.maybe(schema.string()),
|
||||
// deprecated
|
||||
timerange: schema.maybe(
|
||||
schema.object({
|
||||
from: schema.string(),
|
||||
to: schema.string(),
|
||||
})
|
||||
),
|
||||
version: schema.maybe(schema.number()),
|
||||
isAutoGenerated: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
validate: {
|
||||
params: schema.object({
|
||||
availability: schema.maybe(
|
||||
schema.object({
|
||||
range: schema.number(),
|
||||
rangeUnit: schema.string(),
|
||||
threshold: schema.string(),
|
||||
})
|
||||
),
|
||||
filters: schema.maybe(
|
||||
schema.oneOf([
|
||||
// deprecated
|
||||
schema.object({
|
||||
'monitor.type': schema.maybe(schema.arrayOf(schema.string())),
|
||||
'observer.geo.name': schema.maybe(schema.arrayOf(schema.string())),
|
||||
tags: schema.maybe(schema.arrayOf(schema.string())),
|
||||
'url.port': schema.maybe(schema.arrayOf(schema.string())),
|
||||
}),
|
||||
schema.string(),
|
||||
])
|
||||
),
|
||||
// deprecated
|
||||
locations: schema.maybe(schema.arrayOf(schema.string())),
|
||||
numTimes: schema.number(),
|
||||
search: schema.maybe(schema.string()),
|
||||
shouldCheckStatus: schema.boolean(),
|
||||
shouldCheckAvailability: schema.boolean(),
|
||||
timerangeCount: schema.maybe(schema.number()),
|
||||
timerangeUnit: schema.maybe(schema.string()),
|
||||
// deprecated
|
||||
timerange: schema.maybe(
|
||||
schema.object({
|
||||
from: schema.string(),
|
||||
to: schema.string(),
|
||||
})
|
||||
),
|
||||
version: schema.maybe(schema.number()),
|
||||
isAutoGenerated: schema.maybe(schema.boolean()),
|
||||
}),
|
||||
},
|
||||
defaultActionGroupId: MONITOR_STATUS.id,
|
||||
actionGroups: [
|
||||
{
|
||||
id: MONITOR_STATUS.id,
|
||||
name: MONITOR_STATUS.name,
|
||||
},
|
||||
defaultActionGroupId: MONITOR_STATUS.id,
|
||||
actionGroups: [
|
||||
],
|
||||
actionVariables: {
|
||||
context: [
|
||||
{
|
||||
id: MONITOR_STATUS.id,
|
||||
name: MONITOR_STATUS.name,
|
||||
name: 'message',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description',
|
||||
{
|
||||
defaultMessage: 'A generated message summarizing the currently down monitors',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'downMonitorsWithGeo',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'A generated summary that shows some or all of the monitors detected as "down" by the alert',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
actionVariables: {
|
||||
context: [
|
||||
{
|
||||
name: 'message',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.context.message.description',
|
||||
{
|
||||
defaultMessage: 'A generated message summarizing the currently down monitors',
|
||||
}
|
||||
),
|
||||
},
|
||||
{
|
||||
name: 'downMonitorsWithGeo',
|
||||
description: i18n.translate(
|
||||
'xpack.uptime.alerts.monitorStatus.actionVariables.context.downMonitorsWithGeo.description',
|
||||
{
|
||||
defaultMessage:
|
||||
'A generated summary that shows some or all of the monitors detected as "down" by the alert',
|
||||
}
|
||||
),
|
||||
},
|
||||
],
|
||||
state: [...commonMonitorStateI18, ...commonStateTranslations],
|
||||
state: [...commonMonitorStateI18, ...commonStateTranslations],
|
||||
},
|
||||
isExportable: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
async executor({
|
||||
params: rawParams,
|
||||
state,
|
||||
services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle },
|
||||
rule: {
|
||||
schedule: { interval },
|
||||
},
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
async executor({
|
||||
options: {
|
||||
params: rawParams,
|
||||
state,
|
||||
services: { alertInstanceFactory },
|
||||
rule: {
|
||||
schedule: { interval },
|
||||
},
|
||||
},
|
||||
uptimeEsClient,
|
||||
}) {
|
||||
const {
|
||||
filters,
|
||||
search,
|
||||
}) {
|
||||
const {
|
||||
filters,
|
||||
search,
|
||||
numTimes,
|
||||
timerangeCount,
|
||||
timerangeUnit,
|
||||
availability,
|
||||
shouldCheckAvailability,
|
||||
shouldCheckStatus,
|
||||
isAutoGenerated,
|
||||
timerange: oldVersionTimeRange,
|
||||
} = rawParams;
|
||||
const uptimeEsClient = createUptimeESClient({
|
||||
esClient: scopedClusterClient.asCurrentUser,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
const filterString = await formatFilterString(uptimeEsClient, filters, search, libs);
|
||||
|
||||
const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`;
|
||||
|
||||
// Range filter for `monitor.timespan`, the range of time the ping is valid
|
||||
const timespanRange = oldVersionTimeRange || {
|
||||
from: `now-${timespanInterval}`,
|
||||
to: 'now',
|
||||
};
|
||||
|
||||
// Range filter for `@timestamp`, the time the document was indexed
|
||||
const timestampRange = getTimestampRange({
|
||||
ruleScheduleLookback: `now-${interval}`,
|
||||
timerangeLookback: timespanRange.from,
|
||||
});
|
||||
|
||||
let downMonitorsByLocation: GetMonitorStatusResult[] = [];
|
||||
|
||||
// if oldVersionTimeRange present means it's 7.7 format and
|
||||
// after that shouldCheckStatus should be explicitly false
|
||||
if (!(!oldVersionTimeRange && shouldCheckStatus === false)) {
|
||||
downMonitorsByLocation = await libs.requests.getMonitorStatus({
|
||||
uptimeEsClient,
|
||||
timespanRange,
|
||||
timestampRange,
|
||||
numTimes,
|
||||
timerangeCount,
|
||||
timerangeUnit,
|
||||
availability,
|
||||
shouldCheckAvailability,
|
||||
shouldCheckStatus,
|
||||
isAutoGenerated,
|
||||
timerange: oldVersionTimeRange,
|
||||
} = rawParams;
|
||||
const filterString = await formatFilterString(uptimeEsClient, filters, search, libs);
|
||||
|
||||
const timespanInterval = `${String(timerangeCount)}${timerangeUnit}`;
|
||||
|
||||
// Range filter for `monitor.timespan`, the range of time the ping is valid
|
||||
const timespanRange = oldVersionTimeRange || {
|
||||
from: `now-${timespanInterval}`,
|
||||
to: 'now',
|
||||
};
|
||||
|
||||
// Range filter for `@timestamp`, the time the document was indexed
|
||||
const timestampRange = getTimestampRange({
|
||||
ruleScheduleLookback: `now-${interval}`,
|
||||
timerangeLookback: timespanRange.from,
|
||||
locations: [],
|
||||
filters: filterString,
|
||||
});
|
||||
}
|
||||
|
||||
let downMonitorsByLocation: GetMonitorStatusResult[] = [];
|
||||
if (isAutoGenerated) {
|
||||
for (const monitorLoc of downMonitorsByLocation) {
|
||||
const monitorInfo = monitorLoc.monitorInfo;
|
||||
|
||||
// if oldVersionTimeRange present means it's 7.7 format and
|
||||
// after that shouldCheckStatus should be explicitly false
|
||||
if (!(!oldVersionTimeRange && shouldCheckStatus === false)) {
|
||||
downMonitorsByLocation = await libs.requests.getMonitorStatus({
|
||||
uptimeEsClient,
|
||||
timespanRange,
|
||||
timestampRange,
|
||||
numTimes,
|
||||
locations: [],
|
||||
filters: filterString,
|
||||
const statusMessage = getStatusMessage(monitorInfo);
|
||||
const monitorSummary = getMonitorSummary(monitorInfo, statusMessage);
|
||||
|
||||
const alert = alertWithLifecycle({
|
||||
id: getInstanceId(monitorInfo, monitorLoc.location),
|
||||
fields: getMonitorAlertDocument(monitorSummary),
|
||||
});
|
||||
}
|
||||
|
||||
if (isAutoGenerated) {
|
||||
for (const monitorLoc of downMonitorsByLocation) {
|
||||
const monitorInfo = monitorLoc.monitorInfo;
|
||||
|
||||
const alertInstance = alertInstanceFactory(
|
||||
getInstanceId(monitorInfo, monitorLoc.location)
|
||||
);
|
||||
|
||||
const monitorSummary = getMonitorSummary(monitorInfo);
|
||||
const statusMessage = getStatusMessage(monitorInfo);
|
||||
|
||||
alertInstance.replaceState({
|
||||
...state,
|
||||
...monitorSummary,
|
||||
statusMessage,
|
||||
...updateState(state, true),
|
||||
});
|
||||
|
||||
alertInstance.scheduleActions(MONITOR_STATUS.id);
|
||||
}
|
||||
return updateState(state, downMonitorsByLocation.length > 0);
|
||||
}
|
||||
|
||||
let availabilityResults: GetMonitorAvailabilityResult[] = [];
|
||||
if (shouldCheckAvailability) {
|
||||
availabilityResults = await libs.requests.getMonitorAvailability({
|
||||
uptimeEsClient,
|
||||
...availability,
|
||||
filters: JSON.stringify(filterString) || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults);
|
||||
|
||||
mergedIdsByLoc.forEach((monIdByLoc) => {
|
||||
const availMonInfo = availabilityResults.find(
|
||||
({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
|
||||
);
|
||||
|
||||
const downMonInfo = downMonitorsByLocation.find(
|
||||
({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
|
||||
)?.monitorInfo;
|
||||
|
||||
const monitorInfo = downMonInfo || availMonInfo?.monitorInfo!;
|
||||
|
||||
const monitorSummary = getMonitorSummary(monitorInfo);
|
||||
const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability);
|
||||
|
||||
const alertInstance = alertInstanceFactory(getInstanceId(monitorInfo, monIdByLoc));
|
||||
|
||||
alertInstance.replaceState({
|
||||
...updateState(state, true),
|
||||
alert.replaceState({
|
||||
...state,
|
||||
...monitorSummary,
|
||||
statusMessage,
|
||||
...updateState(state, true),
|
||||
});
|
||||
|
||||
alertInstance.scheduleActions(MONITOR_STATUS.id, {
|
||||
message: generateMessageForOlderVersions({ ...monitorSummary, statusMessage }),
|
||||
});
|
||||
alert.scheduleActions(MONITOR_STATUS.id);
|
||||
}
|
||||
return updateState(state, downMonitorsByLocation.length > 0);
|
||||
}
|
||||
|
||||
let availabilityResults: GetMonitorAvailabilityResult[] = [];
|
||||
if (shouldCheckAvailability) {
|
||||
availabilityResults = await libs.requests.getMonitorAvailability({
|
||||
uptimeEsClient,
|
||||
...availability,
|
||||
filters: JSON.stringify(filterString) || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
const mergedIdsByLoc = getUniqueIdsByLoc(downMonitorsByLocation, availabilityResults);
|
||||
|
||||
mergedIdsByLoc.forEach((monIdByLoc) => {
|
||||
const availMonInfo = availabilityResults.find(
|
||||
({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
|
||||
);
|
||||
|
||||
const downMonInfo = downMonitorsByLocation.find(
|
||||
({ monitorId, location }) => getMonIdByLoc(monitorId, location) === monIdByLoc
|
||||
)?.monitorInfo;
|
||||
|
||||
const monitorInfo = downMonInfo || availMonInfo?.monitorInfo!;
|
||||
|
||||
const statusMessage = getStatusMessage(downMonInfo!, availMonInfo!, availability);
|
||||
const monitorSummary = getMonitorSummary(monitorInfo, statusMessage);
|
||||
|
||||
const alert = alertWithLifecycle({
|
||||
id: getInstanceId(monitorInfo, monIdByLoc),
|
||||
fields: getMonitorAlertDocument(monitorSummary),
|
||||
});
|
||||
|
||||
return updateState(state, downMonitorsByLocation.length > 0);
|
||||
},
|
||||
});
|
||||
alert.replaceState({
|
||||
...updateState(state, true),
|
||||
...monitorSummary,
|
||||
statusMessage,
|
||||
});
|
||||
|
||||
alert.scheduleActions(MONITOR_STATUS.id);
|
||||
});
|
||||
|
||||
return updateState(state, downMonitorsByLocation.length > 0);
|
||||
},
|
||||
});
|
||||
|
|
81
x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts
Normal file
81
x-pack/plugins/uptime/server/lib/alerts/test_utils/index.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* 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 { Logger } from 'kibana/server';
|
||||
import { UMServerLibs } from '../../lib';
|
||||
import { UptimeCorePlugins, UptimeCoreSetup } from '../../adapters';
|
||||
import type { UptimeRouter } from '../../../types';
|
||||
import type { RuleDataClient } from '../../../../../rule_registry/server';
|
||||
import { getUptimeESMockClient } from '../../requests/helper';
|
||||
import { alertsMock } from '../../../../../alerting/server/mocks';
|
||||
import { DynamicSettings } from '../../../../common/runtime_types';
|
||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
|
||||
|
||||
/**
|
||||
* The alert takes some dependencies as parameters; these are things like
|
||||
* kibana core services and plugins. This function helps reduce the amount of
|
||||
* boilerplate required.
|
||||
* @param customRequests client tests can use this paramter to provide their own request mocks,
|
||||
* so we don't have to mock them all for each test.
|
||||
*/
|
||||
export const bootstrapDependencies = (customRequests?: any, customPlugins: any = {}) => {
|
||||
const router = {} as UptimeRouter;
|
||||
// these server/libs parameters don't have any functionality, which is fine
|
||||
// because we aren't testing them here
|
||||
const server: UptimeCoreSetup = { router };
|
||||
const plugins: UptimeCorePlugins = customPlugins as any;
|
||||
const libs: UMServerLibs = { requests: {} } as UMServerLibs;
|
||||
libs.requests = { ...libs.requests, ...customRequests };
|
||||
return { server, libs, plugins };
|
||||
};
|
||||
|
||||
export const createRuleTypeMocks = (
|
||||
dynamicCertSettings: {
|
||||
certAgeThreshold: DynamicSettings['certAgeThreshold'];
|
||||
certExpirationThreshold: DynamicSettings['certExpirationThreshold'];
|
||||
} = {
|
||||
certAgeThreshold: DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
|
||||
certExpirationThreshold: DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
|
||||
}
|
||||
) => {
|
||||
const loggerMock = ({
|
||||
debug: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
error: jest.fn(),
|
||||
} as unknown) as Logger;
|
||||
|
||||
const scheduleActions = jest.fn();
|
||||
const replaceState = jest.fn();
|
||||
|
||||
const services = {
|
||||
...getUptimeESMockClient(),
|
||||
...alertsMock.createAlertServices(),
|
||||
alertWithLifecycle: jest.fn().mockReturnValue({ scheduleActions, replaceState }),
|
||||
logger: loggerMock,
|
||||
};
|
||||
|
||||
return {
|
||||
dependencies: {
|
||||
logger: loggerMock,
|
||||
ruleDataClient: ({
|
||||
getReader: () => {
|
||||
return {
|
||||
search: jest.fn(),
|
||||
};
|
||||
},
|
||||
getWriter: () => {
|
||||
return {
|
||||
bulk: jest.fn(),
|
||||
};
|
||||
},
|
||||
} as unknown) as RuleDataClient,
|
||||
},
|
||||
services,
|
||||
scheduleActions,
|
||||
replaceState,
|
||||
};
|
||||
};
|
|
@ -4,52 +4,180 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
import { getCertSummary } from './tls';
|
||||
import { Cert } from '../../../common/runtime_types';
|
||||
|
||||
import { tlsAlertFactory, getCertSummary, DEFAULT_SIZE } from './tls';
|
||||
import { TLS } from '../../../common/constants/alerts';
|
||||
import { CertResult, DynamicSettings } from '../../../common/runtime_types';
|
||||
import { createRuleTypeMocks, bootstrapDependencies } from './test_utils';
|
||||
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
|
||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
|
||||
|
||||
import { savedObjectsAdapter, UMSavedObjectsAdapter } from '../saved_objects';
|
||||
|
||||
/**
|
||||
* This function aims to provide an easy way to give mock props that will
|
||||
* reduce boilerplate for tests.
|
||||
* @param params the params received at alert creation time
|
||||
* @param state the state the alert maintains
|
||||
*/
|
||||
const mockOptions = (
|
||||
dynamicCertSettings?: {
|
||||
certExpirationThreshold: DynamicSettings['certExpirationThreshold'];
|
||||
certAgeThreshold: DynamicSettings['certAgeThreshold'];
|
||||
},
|
||||
state = {}
|
||||
): any => {
|
||||
const { services } = createRuleTypeMocks(dynamicCertSettings);
|
||||
const params = {
|
||||
timerange: { from: 'now-15m', to: 'now' },
|
||||
};
|
||||
|
||||
return {
|
||||
params,
|
||||
state,
|
||||
services,
|
||||
};
|
||||
};
|
||||
|
||||
const mockCertResult: CertResult = {
|
||||
certs: [
|
||||
{
|
||||
not_after: '2020-07-16T03:15:39.000Z',
|
||||
not_before: '2019-07-24T03:15:39.000Z',
|
||||
issuer: 'Sample issuer',
|
||||
common_name: 'Common-One',
|
||||
monitors: [{ name: 'monitor-one', id: 'monitor1' }],
|
||||
sha256: 'abc',
|
||||
},
|
||||
{
|
||||
not_after: '2020-07-18T03:15:39.000Z',
|
||||
not_before: '2019-07-20T03:15:39.000Z',
|
||||
issuer: 'Sample issuer',
|
||||
common_name: 'Common-Two',
|
||||
monitors: [{ name: 'monitor-two', id: 'monitor2' }],
|
||||
sha256: 'bcd',
|
||||
},
|
||||
{
|
||||
not_after: '2020-07-19T03:15:39.000Z',
|
||||
not_before: '2019-07-22T03:15:39.000Z',
|
||||
issuer: 'Sample issuer',
|
||||
common_name: 'Common-Three',
|
||||
monitors: [{ name: 'monitor-three', id: 'monitor3' }],
|
||||
sha256: 'cde',
|
||||
},
|
||||
{
|
||||
not_after: '2020-07-25T03:15:39.000Z',
|
||||
not_before: '2019-07-25T03:15:39.000Z',
|
||||
issuer: 'Sample issuer',
|
||||
common_name: 'Common-Four',
|
||||
monitors: [{ name: 'monitor-four', id: 'monitor4' }],
|
||||
sha256: 'def',
|
||||
},
|
||||
],
|
||||
total: 4,
|
||||
};
|
||||
|
||||
describe('tls alert', () => {
|
||||
let toISOStringSpy: jest.SpyInstance<string, []>;
|
||||
let savedObjectsAdapterSpy: jest.SpyInstance<
|
||||
ReturnType<UMSavedObjectsAdapter['getUptimeDynamicSettings']>
|
||||
>;
|
||||
const mockDate = 'date';
|
||||
beforeAll(() => {
|
||||
Date.now = jest.fn().mockReturnValue(new Date('2021-05-13T12:33:37.000Z'));
|
||||
});
|
||||
|
||||
describe('alert executor', () => {
|
||||
beforeEach(() => {
|
||||
toISOStringSpy = jest.spyOn(Date.prototype, 'toISOString');
|
||||
savedObjectsAdapterSpy = jest.spyOn(savedObjectsAdapter, 'getUptimeDynamicSettings');
|
||||
});
|
||||
|
||||
it('triggers when aging or expiring alerts are found', async () => {
|
||||
toISOStringSpy.mockImplementation(() => mockDate);
|
||||
const mockGetter: jest.Mock<CertResult> = jest.fn();
|
||||
|
||||
mockGetter.mockReturnValue(mockCertResult);
|
||||
const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter });
|
||||
const alert = tlsAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions();
|
||||
const {
|
||||
services: { alertWithLifecycle },
|
||||
} = options;
|
||||
await alert.executor(options);
|
||||
expect(mockGetter).toHaveBeenCalledTimes(1);
|
||||
expect(alertWithLifecycle).toHaveBeenCalledTimes(4);
|
||||
mockCertResult.certs.forEach((cert) => {
|
||||
expect(alertWithLifecycle).toBeCalledWith({
|
||||
fields: expect.objectContaining({
|
||||
'tls.server.x509.subject.common_name': cert.common_name,
|
||||
'tls.server.x509.issuer.common_name': cert.issuer,
|
||||
'tls.server.x509.not_after': cert.not_after,
|
||||
'tls.server.x509.not_before': cert.not_before,
|
||||
'tls.server.hash.sha256': cert.sha256,
|
||||
}),
|
||||
id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`,
|
||||
});
|
||||
});
|
||||
expect(mockGetter).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
from: DEFAULT_FROM,
|
||||
to: DEFAULT_TO,
|
||||
index: 0,
|
||||
size: DEFAULT_SIZE,
|
||||
notValidAfter: `now+${DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold}d`,
|
||||
notValidBefore: `now-${DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold}d`,
|
||||
sortBy: 'common_name',
|
||||
direction: 'desc',
|
||||
})
|
||||
);
|
||||
const [{ value: alertInstanceMock }] = alertWithLifecycle.mock.results;
|
||||
expect(alertInstanceMock.replaceState).toHaveBeenCalledTimes(4);
|
||||
mockCertResult.certs.forEach((cert) => {
|
||||
expect(alertInstanceMock.replaceState).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
commonName: cert.common_name,
|
||||
issuer: cert.issuer,
|
||||
status: 'expired',
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(alertInstanceMock.scheduleActions).toHaveBeenCalledTimes(4);
|
||||
expect(alertInstanceMock.scheduleActions).toBeCalledWith(TLS.id);
|
||||
});
|
||||
|
||||
it('handles dynamic settings for aging or expiration threshold', async () => {
|
||||
toISOStringSpy.mockImplementation(() => mockDate);
|
||||
const certSettings = {
|
||||
certAgeThreshold: 10,
|
||||
certExpirationThreshold: 5,
|
||||
heartbeatIndices: 'heartbeat-*',
|
||||
defaultConnectors: [],
|
||||
};
|
||||
savedObjectsAdapterSpy.mockImplementation(() => certSettings);
|
||||
const mockGetter: jest.Mock<CertResult> = jest.fn();
|
||||
|
||||
mockGetter.mockReturnValue(mockCertResult);
|
||||
const { server, libs, plugins } = bootstrapDependencies({ getCerts: mockGetter });
|
||||
const alert = tlsAlertFactory(server, libs, plugins);
|
||||
const options = mockOptions();
|
||||
await alert.executor(options);
|
||||
expect(mockGetter).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetter).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
notValidAfter: `now+${certSettings.certExpirationThreshold}d`,
|
||||
notValidBefore: `now-${certSettings.certAgeThreshold}d`,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCertSummary', () => {
|
||||
let mockCerts: Cert[];
|
||||
let diffSpy: jest.SpyInstance<any, unknown[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
diffSpy = jest.spyOn(moment.prototype, 'diff');
|
||||
mockCerts = [
|
||||
{
|
||||
not_after: '2020-07-16T03:15:39.000Z',
|
||||
not_before: '2019-07-24T03:15:39.000Z',
|
||||
common_name: 'Common-One',
|
||||
monitors: [{ name: 'monitor-one', id: 'monitor1' }],
|
||||
sha256: 'abc',
|
||||
issuer: 'Cloudflare Inc ECC CA-3',
|
||||
},
|
||||
{
|
||||
not_after: '2020-07-18T03:15:39.000Z',
|
||||
not_before: '2019-07-20T03:15:39.000Z',
|
||||
common_name: 'Common-Two',
|
||||
monitors: [{ name: 'monitor-two', id: 'monitor2' }],
|
||||
sha256: 'bcd',
|
||||
issuer: 'Cloudflare Inc ECC CA-3',
|
||||
},
|
||||
{
|
||||
not_after: '2020-07-19T03:15:39.000Z',
|
||||
not_before: '2019-07-22T03:15:39.000Z',
|
||||
common_name: 'Common-Three',
|
||||
monitors: [{ name: 'monitor-three', id: 'monitor3' }],
|
||||
sha256: 'cde',
|
||||
issuer: 'Cloudflare Inc ECC CA-3',
|
||||
},
|
||||
{
|
||||
not_after: '2020-07-25T03:15:39.000Z',
|
||||
not_before: '2019-07-25T03:15:39.000Z',
|
||||
common_name: 'Common-Four',
|
||||
monitors: [{ name: 'monitor-four', id: 'monitor4' }],
|
||||
sha256: 'def',
|
||||
issuer: 'Cloudflare Inc ECC CA-3',
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -59,13 +187,13 @@ describe('tls alert', () => {
|
|||
it('handles positive diffs for expired certs appropriately', () => {
|
||||
diffSpy.mockReturnValueOnce(900);
|
||||
const result = getCertSummary(
|
||||
mockCerts[0],
|
||||
mockCertResult.certs[0],
|
||||
new Date('2020-07-20T05:00:00.000Z').valueOf(),
|
||||
new Date('2019-03-01T00:00:00.000Z').valueOf()
|
||||
);
|
||||
expect(result).toEqual({
|
||||
commonName: mockCerts[0].common_name,
|
||||
issuer: mockCerts[0].issuer,
|
||||
commonName: mockCertResult.certs[0].common_name,
|
||||
issuer: mockCertResult.certs[0].issuer,
|
||||
summary: 'expired on Jul 15, 2020 EDT, 900 days ago.',
|
||||
status: 'expired',
|
||||
});
|
||||
|
@ -74,13 +202,13 @@ describe('tls alert', () => {
|
|||
it('handles positive diffs for agining certs appropriately', () => {
|
||||
diffSpy.mockReturnValueOnce(702);
|
||||
const result = getCertSummary(
|
||||
mockCerts[0],
|
||||
mockCertResult.certs[0],
|
||||
new Date('2020-07-01T12:00:00.000Z').valueOf(),
|
||||
new Date('2019-09-01T03:00:00.000Z').valueOf()
|
||||
);
|
||||
expect(result).toEqual({
|
||||
commonName: mockCerts[0].common_name,
|
||||
issuer: mockCerts[0].issuer,
|
||||
commonName: mockCertResult.certs[0].common_name,
|
||||
issuer: mockCertResult.certs[0].issuer,
|
||||
summary: 'valid since Jul 23, 2019 EDT, 702 days ago.',
|
||||
status: 'becoming too old',
|
||||
});
|
||||
|
@ -89,13 +217,13 @@ describe('tls alert', () => {
|
|||
it('handles negative diff values appropriately for aging certs', () => {
|
||||
diffSpy.mockReturnValueOnce(-90);
|
||||
const result = getCertSummary(
|
||||
mockCerts[0],
|
||||
mockCertResult.certs[0],
|
||||
new Date('2020-07-01T12:00:00.000Z').valueOf(),
|
||||
new Date('2019-09-01T03:00:00.000Z').valueOf()
|
||||
);
|
||||
expect(result).toEqual({
|
||||
commonName: mockCerts[0].common_name,
|
||||
issuer: mockCerts[0].issuer,
|
||||
commonName: mockCertResult.certs[0].common_name,
|
||||
issuer: mockCertResult.certs[0].issuer,
|
||||
summary: 'invalid until Jul 23, 2019 EDT, 90 days from now.',
|
||||
status: 'invalid',
|
||||
});
|
||||
|
@ -106,13 +234,13 @@ describe('tls alert', () => {
|
|||
// negative days are in the future, positive days are in the past
|
||||
.mockReturnValueOnce(-96);
|
||||
const result = getCertSummary(
|
||||
mockCerts[0],
|
||||
mockCertResult.certs[0],
|
||||
new Date('2020-07-20T05:00:00.000Z').valueOf(),
|
||||
new Date('2019-03-01T00:00:00.000Z').valueOf()
|
||||
);
|
||||
expect(result).toEqual({
|
||||
commonName: mockCerts[0].common_name,
|
||||
issuer: mockCerts[0].issuer,
|
||||
commonName: mockCertResult.certs[0].common_name,
|
||||
issuer: mockCertResult.certs[0].issuer,
|
||||
summary: 'expires on Jul 15, 2020 EDT in 96 days.',
|
||||
status: 'expiring',
|
||||
});
|
||||
|
|
|
@ -8,18 +8,22 @@
|
|||
import moment from 'moment';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { UptimeAlertTypeFactory } from './types';
|
||||
import { updateState } from './common';
|
||||
import { updateState, generateAlertMessage } from './common';
|
||||
import { TLS } from '../../../common/constants/alerts';
|
||||
import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
|
||||
import { Cert, CertResult } from '../../../common/runtime_types';
|
||||
import { commonStateTranslations, tlsTranslations } from './translations';
|
||||
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
|
||||
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
|
||||
import { TlsTranslations } from '../../../common/translations';
|
||||
|
||||
import { ActionGroupIdsOf } from '../../../../alerting/common';
|
||||
|
||||
import { savedObjectsAdapter } from '../saved_objects';
|
||||
import { createUptimeESClient } from '../lib';
|
||||
|
||||
export type ActionGroupIds = ActionGroupIdsOf<typeof TLS>;
|
||||
|
||||
const DEFAULT_SIZE = 20;
|
||||
export const DEFAULT_SIZE = 20;
|
||||
|
||||
interface TlsAlertState {
|
||||
commonName: string;
|
||||
|
@ -93,78 +97,92 @@ export const getCertSummary = (
|
|||
};
|
||||
};
|
||||
|
||||
export const tlsAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) =>
|
||||
uptimeAlertWrapper<ActionGroupIds>({
|
||||
id: 'xpack.uptime.alerts.tlsCertificate',
|
||||
name: tlsTranslations.alertFactoryName,
|
||||
validate: {
|
||||
params: schema.object({}),
|
||||
export const tlsAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) => ({
|
||||
id: 'xpack.uptime.alerts.tlsCertificate',
|
||||
producer: 'uptime',
|
||||
name: tlsTranslations.alertFactoryName,
|
||||
validate: {
|
||||
params: schema.object({}),
|
||||
},
|
||||
defaultActionGroupId: TLS.id,
|
||||
actionGroups: [
|
||||
{
|
||||
id: TLS.id,
|
||||
name: TLS.name,
|
||||
},
|
||||
defaultActionGroupId: TLS.id,
|
||||
actionGroups: [
|
||||
{
|
||||
id: TLS.id,
|
||||
name: TLS.name,
|
||||
},
|
||||
],
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
|
||||
},
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
async executor({ options, dynamicSettings, uptimeEsClient }) {
|
||||
const {
|
||||
services: { alertInstanceFactory },
|
||||
state,
|
||||
} = options;
|
||||
],
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
|
||||
},
|
||||
isExportable: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
async executor({
|
||||
services: { alertWithLifecycle, savedObjectsClient, scopedClusterClient },
|
||||
state,
|
||||
}) {
|
||||
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
|
||||
|
||||
const { certs, total }: CertResult = await libs.requests.getCerts({
|
||||
uptimeEsClient,
|
||||
from: DEFAULT_FROM,
|
||||
to: DEFAULT_TO,
|
||||
index: 0,
|
||||
size: DEFAULT_SIZE,
|
||||
notValidAfter: `now+${
|
||||
dynamicSettings?.certExpirationThreshold ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
|
||||
}d`,
|
||||
notValidBefore: `now-${
|
||||
dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
|
||||
}d`,
|
||||
sortBy: 'common_name',
|
||||
direction: 'desc',
|
||||
});
|
||||
const uptimeEsClient = createUptimeESClient({
|
||||
esClient: scopedClusterClient.asCurrentUser,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
const foundCerts = total > 0;
|
||||
const { certs, total }: CertResult = await libs.requests.getCerts({
|
||||
uptimeEsClient,
|
||||
from: DEFAULT_FROM,
|
||||
to: DEFAULT_TO,
|
||||
index: 0,
|
||||
size: DEFAULT_SIZE,
|
||||
notValidAfter: `now+${
|
||||
dynamicSettings?.certExpirationThreshold ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
|
||||
}d`,
|
||||
notValidBefore: `now-${
|
||||
dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
|
||||
}d`,
|
||||
sortBy: 'common_name',
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
if (foundCerts) {
|
||||
certs.forEach((cert) => {
|
||||
const absoluteExpirationThreshold = moment()
|
||||
.add(
|
||||
dynamicSettings.certExpirationThreshold ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const absoluteAgeThreshold = moment()
|
||||
.subtract(
|
||||
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const alertInstance = alertInstanceFactory(
|
||||
`${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`
|
||||
);
|
||||
const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold);
|
||||
alertInstance.replaceState({
|
||||
...updateState(state, foundCerts),
|
||||
...summary,
|
||||
});
|
||||
alertInstance.scheduleActions(TLS.id);
|
||||
const foundCerts = total > 0;
|
||||
|
||||
if (foundCerts) {
|
||||
certs.forEach((cert) => {
|
||||
const absoluteExpirationThreshold = moment()
|
||||
.add(
|
||||
dynamicSettings.certExpirationThreshold ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const absoluteAgeThreshold = moment()
|
||||
.subtract(
|
||||
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const summary = getCertSummary(cert, absoluteExpirationThreshold, absoluteAgeThreshold);
|
||||
|
||||
const alertInstance = alertWithLifecycle({
|
||||
id: `${cert.common_name}-${cert.issuer?.replace(/\s/g, '_')}-${cert.sha256}`,
|
||||
fields: {
|
||||
'tls.server.x509.subject.common_name': cert.common_name,
|
||||
'tls.server.x509.issuer.common_name': cert.issuer,
|
||||
'tls.server.x509.not_after': cert.not_after,
|
||||
'tls.server.x509.not_before': cert.not_before,
|
||||
'tls.server.hash.sha256': cert.sha256,
|
||||
reason: generateAlertMessage(TlsTranslations.defaultActionMessage, summary),
|
||||
},
|
||||
});
|
||||
}
|
||||
alertInstance.replaceState({
|
||||
...updateState(state, foundCerts),
|
||||
...summary,
|
||||
});
|
||||
alertInstance.scheduleActions(TLS.id);
|
||||
});
|
||||
}
|
||||
|
||||
return updateState(state, foundCerts);
|
||||
},
|
||||
});
|
||||
return updateState(state, foundCerts);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -14,11 +14,18 @@ import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../common/constants';
|
|||
import { Cert, CertResult } from '../../../common/runtime_types';
|
||||
import { commonStateTranslations, tlsTranslations } from './translations';
|
||||
import { DEFAULT_FROM, DEFAULT_TO } from '../../rest_api/certs/certs';
|
||||
import { uptimeAlertWrapper } from './uptime_alert_wrapper';
|
||||
import { ActionGroupIdsOf } from '../../../../alerting/common';
|
||||
|
||||
import { AlertInstanceContext } from '../../../../alerting/common';
|
||||
import { AlertInstance } from '../../../../alerting/server';
|
||||
|
||||
import { savedObjectsAdapter } from '../saved_objects';
|
||||
import { createUptimeESClient } from '../lib';
|
||||
|
||||
export type ActionGroupIds = ActionGroupIdsOf<typeof TLS_LEGACY>;
|
||||
|
||||
type TLSAlertInstance = AlertInstance<Record<string, any>, AlertInstanceContext, ActionGroupIds>;
|
||||
|
||||
const DEFAULT_SIZE = 20;
|
||||
|
||||
interface TlsAlertState {
|
||||
|
@ -84,74 +91,78 @@ export const getCertSummary = (
|
|||
};
|
||||
};
|
||||
|
||||
export const tlsLegacyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) =>
|
||||
uptimeAlertWrapper<ActionGroupIds>({
|
||||
id: 'xpack.uptime.alerts.tls',
|
||||
name: tlsTranslations.legacyAlertFactoryName,
|
||||
validate: {
|
||||
params: schema.object({}),
|
||||
export const tlsLegacyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) => ({
|
||||
id: 'xpack.uptime.alerts.tls',
|
||||
producer: 'uptime',
|
||||
name: tlsTranslations.legacyAlertFactoryName,
|
||||
validate: {
|
||||
params: schema.object({}),
|
||||
},
|
||||
defaultActionGroupId: TLS_LEGACY.id,
|
||||
actionGroups: [
|
||||
{
|
||||
id: TLS_LEGACY.id,
|
||||
name: TLS_LEGACY.name,
|
||||
},
|
||||
defaultActionGroupId: TLS_LEGACY.id,
|
||||
actionGroups: [
|
||||
{
|
||||
id: TLS_LEGACY.id,
|
||||
name: TLS_LEGACY.name,
|
||||
},
|
||||
],
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
|
||||
},
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
async executor({ options, dynamicSettings, uptimeEsClient }) {
|
||||
const {
|
||||
services: { alertInstanceFactory },
|
||||
state,
|
||||
} = options;
|
||||
],
|
||||
actionVariables: {
|
||||
context: [],
|
||||
state: [...tlsTranslations.actionVariables, ...commonStateTranslations],
|
||||
},
|
||||
isExportable: true,
|
||||
minimumLicenseRequired: 'basic',
|
||||
async executor({
|
||||
services: { alertInstanceFactory, scopedClusterClient, savedObjectsClient },
|
||||
state,
|
||||
}) {
|
||||
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(savedObjectsClient);
|
||||
|
||||
const { certs, total }: CertResult = await libs.requests.getCerts({
|
||||
uptimeEsClient,
|
||||
from: DEFAULT_FROM,
|
||||
to: DEFAULT_TO,
|
||||
index: 0,
|
||||
size: DEFAULT_SIZE,
|
||||
notValidAfter: `now+${
|
||||
dynamicSettings?.certExpirationThreshold ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
|
||||
}d`,
|
||||
notValidBefore: `now-${
|
||||
dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
|
||||
}d`,
|
||||
sortBy: 'common_name',
|
||||
direction: 'desc',
|
||||
const uptimeEsClient = createUptimeESClient({
|
||||
esClient: scopedClusterClient.asCurrentUser,
|
||||
savedObjectsClient,
|
||||
});
|
||||
const { certs, total }: CertResult = await libs.requests.getCerts({
|
||||
uptimeEsClient,
|
||||
from: DEFAULT_FROM,
|
||||
to: DEFAULT_TO,
|
||||
index: 0,
|
||||
size: DEFAULT_SIZE,
|
||||
notValidAfter: `now+${
|
||||
dynamicSettings?.certExpirationThreshold ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold
|
||||
}d`,
|
||||
notValidBefore: `now-${
|
||||
dynamicSettings?.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold
|
||||
}d`,
|
||||
sortBy: 'common_name',
|
||||
direction: 'desc',
|
||||
});
|
||||
|
||||
const foundCerts = total > 0;
|
||||
|
||||
if (foundCerts) {
|
||||
const absoluteExpirationThreshold = moment()
|
||||
.add(
|
||||
dynamicSettings.certExpirationThreshold ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const absoluteAgeThreshold = moment()
|
||||
.subtract(
|
||||
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const alertInstance: TLSAlertInstance = alertInstanceFactory(TLS_LEGACY.id);
|
||||
const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
|
||||
alertInstance.replaceState({
|
||||
...updateState(state, foundCerts),
|
||||
...summary,
|
||||
});
|
||||
alertInstance.scheduleActions(TLS_LEGACY.id);
|
||||
}
|
||||
|
||||
const foundCerts = total > 0;
|
||||
|
||||
if (foundCerts) {
|
||||
const absoluteExpirationThreshold = moment()
|
||||
.add(
|
||||
dynamicSettings.certExpirationThreshold ??
|
||||
DYNAMIC_SETTINGS_DEFAULTS.certExpirationThreshold,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const absoluteAgeThreshold = moment()
|
||||
.subtract(
|
||||
dynamicSettings.certAgeThreshold ?? DYNAMIC_SETTINGS_DEFAULTS.certAgeThreshold,
|
||||
'd'
|
||||
)
|
||||
.valueOf();
|
||||
const alertInstance = alertInstanceFactory(TLS_LEGACY.id);
|
||||
const summary = getCertSummary(certs, absoluteExpirationThreshold, absoluteAgeThreshold);
|
||||
alertInstance.replaceState({
|
||||
...updateState(state, foundCerts),
|
||||
...summary,
|
||||
});
|
||||
alertInstance.scheduleActions(TLS_LEGACY.id);
|
||||
}
|
||||
|
||||
return updateState(state, foundCerts);
|
||||
},
|
||||
});
|
||||
return updateState(state, foundCerts);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -4,21 +4,27 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { UptimeCorePlugins, UptimeCoreSetup } from '../adapters';
|
||||
import { UMServerLibs } from '../lib';
|
||||
import { AlertType, AlertInstanceState, AlertInstanceContext } from '../../../../alerting/server';
|
||||
import { AlertTypeWithExecutor, LifecycleAlertService } from '../../../../rule_registry/server';
|
||||
import { AlertInstanceContext } from '../../../../alerting/common';
|
||||
|
||||
export type UptimeAlertTypeParam = Record<string, any>;
|
||||
export type UptimeAlertTypeState = Record<string, any>;
|
||||
export type UptimeAlertTypeFactory<ActionGroupIds extends string> = (
|
||||
/**
|
||||
* Because all of our types are presumably going to list the `producer` as `'uptime'`,
|
||||
* we should just omit this field from the returned value to simplify the returned alert type.
|
||||
*
|
||||
* When we register all the alerts we can inject this field.
|
||||
*/
|
||||
export type DefaultUptimeAlertInstance<TActionGroupIds extends string> = AlertTypeWithExecutor<
|
||||
Record<string, any>,
|
||||
AlertInstanceContext,
|
||||
{
|
||||
alertWithLifecycle: LifecycleAlertService<AlertInstanceContext, TActionGroupIds>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type UptimeAlertTypeFactory<TActionGroupIds extends string> = (
|
||||
server: UptimeCoreSetup,
|
||||
libs: UMServerLibs,
|
||||
plugins: UptimeCorePlugins
|
||||
) => AlertType<
|
||||
UptimeAlertTypeParam,
|
||||
UptimeAlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
ActionGroupIds
|
||||
>;
|
||||
) => DefaultUptimeAlertInstance<TActionGroupIds>;
|
||||
|
|
|
@ -1,68 +0,0 @@
|
|||
/*
|
||||
* 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 { SavedObjectsClientContract } from 'kibana/server';
|
||||
import {
|
||||
AlertExecutorOptions,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
} from '../../../../alerting/server';
|
||||
import { savedObjectsAdapter } from '../saved_objects';
|
||||
import { DynamicSettings } from '../../../common/runtime_types';
|
||||
import { createUptimeESClient, UptimeESClient } from '../lib';
|
||||
import { UptimeAlertTypeFactory, UptimeAlertTypeParam, UptimeAlertTypeState } from './types';
|
||||
|
||||
export interface UptimeAlertType<ActionGroupIds extends string>
|
||||
extends Omit<ReturnType<UptimeAlertTypeFactory<ActionGroupIds>>, 'executor' | 'producer'> {
|
||||
executor: ({
|
||||
options,
|
||||
uptimeEsClient,
|
||||
dynamicSettings,
|
||||
}: {
|
||||
options: AlertExecutorOptions<
|
||||
UptimeAlertTypeParam,
|
||||
UptimeAlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
ActionGroupIds
|
||||
>;
|
||||
uptimeEsClient: UptimeESClient;
|
||||
dynamicSettings: DynamicSettings;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}) => Promise<UptimeAlertTypeState | void>;
|
||||
}
|
||||
|
||||
export const uptimeAlertWrapper = <ActionGroupIds extends string>(
|
||||
uptimeAlert: UptimeAlertType<ActionGroupIds>
|
||||
) => ({
|
||||
...uptimeAlert,
|
||||
producer: 'uptime',
|
||||
executor: async (
|
||||
options: AlertExecutorOptions<
|
||||
UptimeAlertTypeParam,
|
||||
UptimeAlertTypeState,
|
||||
AlertInstanceState,
|
||||
AlertInstanceContext,
|
||||
ActionGroupIds
|
||||
>
|
||||
) => {
|
||||
const {
|
||||
services: { scopedClusterClient: esClient, savedObjectsClient },
|
||||
} = options;
|
||||
|
||||
const dynamicSettings = await savedObjectsAdapter.getUptimeDynamicSettings(
|
||||
options.services.savedObjectsClient
|
||||
);
|
||||
|
||||
const uptimeEsClient = createUptimeESClient({
|
||||
esClient: esClient.asCurrentUser,
|
||||
savedObjectsClient,
|
||||
});
|
||||
|
||||
return uptimeAlert.executor({ options, dynamicSettings, uptimeEsClient, savedObjectsClient });
|
||||
},
|
||||
});
|
|
@ -4,30 +4,97 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { once } from 'lodash';
|
||||
import {
|
||||
PluginInitializerContext,
|
||||
CoreStart,
|
||||
CoreSetup,
|
||||
Plugin as PluginType,
|
||||
ISavedObjectsRepository,
|
||||
Logger,
|
||||
} from '../../../../src/core/server';
|
||||
import { uptimeRuleFieldMap } from '../common/rules/uptime_rule_field_map';
|
||||
import { initServerWithKibana } from './kibana.index';
|
||||
import { KibanaTelemetryAdapter, UptimeCorePlugins } from './lib/adapters';
|
||||
import { umDynamicSettings } from './lib/saved_objects';
|
||||
import { mappingFromFieldMap } from '../../rule_registry/common/mapping_from_field_map';
|
||||
import { TECHNICAL_COMPONENT_TEMPLATE_NAME } from '../../rule_registry/common/assets';
|
||||
|
||||
export type UptimeRuleRegistry = ReturnType<Plugin['setup']>['ruleRegistry'];
|
||||
|
||||
export class Plugin implements PluginType {
|
||||
private savedObjectsClient?: ISavedObjectsRepository;
|
||||
private initContext: PluginInitializerContext;
|
||||
private logger?: Logger;
|
||||
|
||||
constructor(_initializerContext: PluginInitializerContext) {}
|
||||
constructor(_initializerContext: PluginInitializerContext) {
|
||||
this.initContext = _initializerContext;
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup, plugins: UptimeCorePlugins) {
|
||||
initServerWithKibana({ router: core.http.createRouter() }, plugins);
|
||||
this.logger = this.initContext.logger.get();
|
||||
const { ruleDataService } = plugins.ruleRegistry;
|
||||
|
||||
const ready = once(async () => {
|
||||
const componentTemplateName = ruleDataService.getFullAssetName('synthetics-mappings');
|
||||
const alertsIndexPattern = ruleDataService.getFullAssetName('observability.synthetics*');
|
||||
|
||||
if (!ruleDataService.isWriteEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ruleDataService.createOrUpdateComponentTemplate({
|
||||
name: componentTemplateName,
|
||||
body: {
|
||||
template: {
|
||||
settings: {
|
||||
number_of_shards: 1,
|
||||
},
|
||||
mappings: mappingFromFieldMap(uptimeRuleFieldMap),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await ruleDataService.createOrUpdateIndexTemplate({
|
||||
name: ruleDataService.getFullAssetName('synthetics-index-template'),
|
||||
body: {
|
||||
index_patterns: [alertsIndexPattern],
|
||||
composed_of: [
|
||||
ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
|
||||
componentTemplateName,
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await ruleDataService.updateIndexMappingsMatchingPattern(alertsIndexPattern);
|
||||
});
|
||||
|
||||
// initialize eagerly
|
||||
const initializeRuleDataTemplatesPromise = ready().catch((err) => {
|
||||
this.logger!.error(err);
|
||||
});
|
||||
|
||||
const ruleDataClient = ruleDataService.getRuleDataClient(
|
||||
'synthetics',
|
||||
ruleDataService.getFullAssetName('observability.synthetics'),
|
||||
() => initializeRuleDataTemplatesPromise
|
||||
);
|
||||
|
||||
initServerWithKibana(
|
||||
{ router: core.http.createRouter() },
|
||||
plugins,
|
||||
ruleDataClient,
|
||||
this.logger
|
||||
);
|
||||
core.savedObjects.registerType(umDynamicSettings);
|
||||
KibanaTelemetryAdapter.registerUsageCollector(
|
||||
plugins.usageCollection,
|
||||
() => this.savedObjectsClient
|
||||
);
|
||||
|
||||
return {
|
||||
ruleRegistry: ruleDataClient,
|
||||
};
|
||||
}
|
||||
|
||||
public start(core: CoreStart, _plugins: any) {
|
||||
|
|
|
@ -5,21 +5,47 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Logger } from 'kibana/server';
|
||||
import { createLifecycleRuleTypeFactory, RuleDataClient } from '../../rule_registry/server';
|
||||
import { UMServerLibs } from './lib/lib';
|
||||
import { createRouteWithAuth, restApiRoutes, uptimeRouteWrapper } from './rest_api';
|
||||
import { UptimeCoreSetup, UptimeCorePlugins } from './lib/adapters';
|
||||
import { uptimeAlertTypeFactories } from './lib/alerts';
|
||||
|
||||
import { statusCheckAlertFactory } from './lib/alerts/status_check';
|
||||
import { tlsAlertFactory } from './lib/alerts/tls';
|
||||
import { tlsLegacyAlertFactory } from './lib/alerts/tls_legacy';
|
||||
import { durationAnomalyAlertFactory } from './lib/alerts/duration_anomaly';
|
||||
|
||||
export const initUptimeServer = (
|
||||
server: UptimeCoreSetup,
|
||||
libs: UMServerLibs,
|
||||
plugins: UptimeCorePlugins
|
||||
plugins: UptimeCorePlugins,
|
||||
ruleDataClient: RuleDataClient,
|
||||
logger: Logger
|
||||
) => {
|
||||
restApiRoutes.forEach((route) =>
|
||||
libs.framework.registerRoute(uptimeRouteWrapper(createRouteWithAuth(libs, route)))
|
||||
);
|
||||
|
||||
uptimeAlertTypeFactories.forEach((alertTypeFactory) =>
|
||||
plugins.alerting.registerType(alertTypeFactory(server, libs, plugins))
|
||||
);
|
||||
const {
|
||||
alerting: { registerType },
|
||||
} = plugins;
|
||||
|
||||
const statusAlert = statusCheckAlertFactory(server, libs, plugins);
|
||||
const tlsLegacyAlert = tlsLegacyAlertFactory(server, libs, plugins);
|
||||
const tlsAlert = tlsAlertFactory(server, libs, plugins);
|
||||
const durationAlert = durationAnomalyAlertFactory(server, libs, plugins);
|
||||
|
||||
const createLifecycleRuleType = createLifecycleRuleTypeFactory({
|
||||
ruleDataClient,
|
||||
logger,
|
||||
});
|
||||
|
||||
registerType(createLifecycleRuleType(statusAlert));
|
||||
registerType(createLifecycleRuleType(tlsAlert));
|
||||
registerType(createLifecycleRuleType(durationAlert));
|
||||
|
||||
/* TLS Legacy rule supported at least through 8.0.
|
||||
* Not registered with RAC */
|
||||
registerType(tlsLegacyAlert);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue