[RAC][UPTIME] -126229 - Add view in app url as an action variable in the alert message for uptime app (#127478)

* Expose getMonitorRouteFromMonitorId in the common folder

* Remove unused import

* WIP

* Fix some issues

* Add 5 min before when the alert is raised

* Update status

* Cover the autogenerated use case

* Update tests

* Updated tests

* Use indexedStartedAt and full URL

* Take into consideration the kibanaBase path

* Fix URL

* LINT

* Fix tests

* Use IBasePath for clarity and update related tests

* Optim - use getViewInAppUrl

* Add duration anomaly and fix tests

* Fix tests

* Rename server var

* Remove join
This commit is contained in:
Faisal Kanout 2022-03-23 12:18:50 +03:00 committed by GitHub
parent d3d36cf0c1
commit daea5a8519
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 145 additions and 55 deletions

View 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]])) }
: {}),
},
});

View file

@ -5,40 +5,6 @@
* 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]])) }
: {}),
},
});
export const getUrlForAlert = (id: string, basePath: string) => {
return basePath + '/app/management/insightsAndAlerting/triggersActions/alert/' + id;
};

View file

@ -11,7 +11,7 @@ import moment from 'moment';
import { ALERT_END, ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_REASON } from '@kbn/rule-data-utils';
import { AlertTypeInitializer } from '.';
import { getMonitorRouteFromMonitorId } from './common';
import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
import { DurationAnomalyTranslations } from '../../../common/translations';
import { ObservabilityRuleTypeModel } from '../../../../observability/public';

View file

@ -17,7 +17,7 @@ import {
} from '@kbn/rule-data-utils';
import { AlertTypeInitializer } from '.';
import { getMonitorRouteFromMonitorId } from './common';
import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url';
import { MonitorStatusTranslations } from '../../../common/translations';
import { CLIENT_ALERT_TYPES } from '../../../common/constants/alerts';
import { ObservabilityRuleTypeModel } from '../../../../observability/public';

View file

@ -6,7 +6,12 @@
*/
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import type { SavedObjectsClientContract, IScopedClusterClient, Logger } from 'src/core/server';
import type {
SavedObjectsClientContract,
IScopedClusterClient,
Logger,
IBasePath,
} from 'src/core/server';
import type { TelemetryPluginSetup, TelemetryPluginStart } from 'src/plugins/telemetry/server';
import { ObservabilityPluginSetup } from '../../../../../observability/server';
import {
@ -56,6 +61,7 @@ export interface UptimeServerSetup {
logger: Logger;
telemetry: TelemetryEventsSender;
uptimeEsClient: UptimeESClient;
basePath: IBasePath;
}
export interface UptimeCorePluginsSetup {

View file

@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n';
export const MESSAGE = 'message';
export const MONITOR_WITH_GEO = 'downMonitorsWithGeo';
export const ALERT_REASON_MSG = 'reason';
export const VIEW_IN_APP_URL = 'viewInAppUrl';
export const ACTION_VARIABLES = {
[MESSAGE]: {
@ -40,4 +41,14 @@ export const ACTION_VARIABLES = {
}
),
},
[VIEW_IN_APP_URL]: {
name: VIEW_IN_APP_URL,
description: i18n.translate(
'xpack.uptime.alerts.monitorStatus.actionVariables.context.viewInAppUrl.description',
{
defaultMessage:
'Link to the view or feature within Elastic that can be used to investigate the alert and its context further',
}
),
},
};

View file

@ -7,6 +7,7 @@
import { isRight } from 'fp-ts/lib/Either';
import Mustache from 'mustache';
import { IBasePath } from 'kibana/server';
import { UptimeCommonState, UptimeCommonStateType } from '../../../common/runtime_types';
export type UpdateUptimeAlertState = (
@ -60,3 +61,7 @@ export const updateState: UpdateUptimeAlertState = (state, isTriggeredNow) => {
export const generateAlertMessage = (messageTemplate: string, fields: Record<string, any>) => {
return Mustache.render(messageTemplate, { state: { ...fields } });
};
export const getViewInAppUrl = (relativeViewInAppUrl: string, basePath: IBasePath) =>
basePath.publicBaseUrl
? new URL(basePath.prepend(relativeViewInAppUrl), basePath.publicBaseUrl).toString()
: relativeViewInAppUrl;

View file

@ -16,7 +16,7 @@ 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_REASON_MSG } from './action_variables';
import { ALERT_REASON_MSG, VIEW_IN_APP_URL } from './action_variables';
interface MockAnomaly {
severity: AnomaliesTableRecord['severity'];
@ -219,6 +219,7 @@ Response times as high as ${slowestResponse} ms have been detected from location
"xpack.uptime.alerts.actionGroups.durationAnomaly",
Object {
"${ALERT_REASON_MSG}": "${reasonMessages[0]}",
"${VIEW_IN_APP_URL}": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MA==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z",
},
]
`);
@ -227,6 +228,7 @@ Response times as high as ${slowestResponse} ms have been detected from location
"xpack.uptime.alerts.actionGroups.durationAnomaly",
Object {
"${ALERT_REASON_MSG}": "${reasonMessages[1]}",
"${VIEW_IN_APP_URL}": "http://localhost:5601/hfe/app/uptime/monitor/eHBhY2sudXB0aW1lLmFsZXJ0cy5hY3Rpb25Hcm91cHMuZHVyYXRpb25Bbm9tYWx5MQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z",
},
]
`);

View file

@ -13,7 +13,7 @@ import {
ALERT_REASON,
} from '@kbn/rule-data-utils';
import { ActionGroupIdsOf } from '../../../../alerting/common';
import { updateState, generateAlertMessage } from './common';
import { updateState, generateAlertMessage, getViewInAppUrl } from './common';
import { DURATION_ANOMALY } from '../../../common/constants/alerts';
import { commonStateTranslations, durationAnomalyTranslations } from './translations';
import { AnomaliesTableRecord } from '../../../../ml/common/types/anomalies';
@ -24,9 +24,10 @@ import { Ping } from '../../../common/runtime_types/ping';
import { getMLJobId } from '../../../common/lib';
import { DurationAnomalyTranslations as CommonDurationAnomalyTranslations } from '../../../common/translations';
import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url';
import { createUptimeESClient } from '../lib';
import { ALERT_REASON_MSG, ACTION_VARIABLES } from './action_variables';
import { ALERT_REASON_MSG, ACTION_VARIABLES, VIEW_IN_APP_URL } from './action_variables';
export type ActionGroupIds = ActionGroupIdsOf<typeof DURATION_ANOMALY>;
@ -72,7 +73,7 @@ const getAnomalies = async (
};
export const durationAnomalyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
_server,
server,
libs,
plugins
) => ({
@ -93,20 +94,23 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds>
},
],
actionVariables: {
context: [ACTION_VARIABLES[ALERT_REASON_MSG]],
context: [ACTION_VARIABLES[ALERT_REASON_MSG], ACTION_VARIABLES[VIEW_IN_APP_URL]],
state: [...durationAnomalyTranslations.actionVariables, ...commonStateTranslations],
},
isExportable: true,
minimumLicenseRequired: 'platinum',
async executor({
params,
services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient },
services: { alertWithLifecycle, scopedClusterClient, savedObjectsClient, getAlertStartedDate },
state,
startedAt,
}) {
const uptimeEsClient = createUptimeESClient({
esClient: scopedClusterClient.asCurrentUser,
savedObjectsClient,
});
const { basePath } = server;
const { anomalies } =
(await getAnomalies(plugins, savedObjectsClient, params, state.lastCheckedAt as string)) ??
{};
@ -128,8 +132,16 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds>
summary
);
const alertId = DURATION_ANOMALY.id + index;
const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString();
const relativeViewInAppUrl = getMonitorRouteFromMonitorId({
monitorId: DURATION_ANOMALY.id + index,
dateRangeEnd: 'now',
dateRangeStart: indexedStartedAt,
});
const alertInstance = alertWithLifecycle({
id: DURATION_ANOMALY.id + index,
id: alertId,
fields: {
'monitor.id': params.monitorId,
'url.full': summary.monitorUrl,
@ -147,6 +159,7 @@ export const durationAnomalyAlertFactory: UptimeAlertTypeFactory<ActionGroupIds>
});
alertInstance.scheduleActions(DURATION_ANOMALY.id, {
[ALERT_REASON_MSG]: alertReasonMessage,
[VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath),
});
});
}

View file

@ -243,6 +243,7 @@ describe('status check alert', () => {
"xpack.uptime.alerts.actionGroups.monitorStatus",
Object {
"reason": "First from harrisburg failed 234 times in the last 15 mins. Alert when > 5.",
"viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D",
},
]
`);
@ -313,6 +314,7 @@ describe('status check alert', () => {
"xpack.uptime.alerts.actionGroups.monitorStatus",
Object {
"reason": "First from harrisburg failed 234 times in the last 15m. Alert when > 5.",
"viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zmlyc3Q=?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D",
},
]
`);
@ -784,24 +786,28 @@ describe('status check alert', () => {
"xpack.uptime.alerts.actionGroups.monitorStatus",
Object {
"reason": "Foo from harrisburg 35 days availability is 99.28%. Alert when < 99.34%.",
"viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22harrisburg%22%5D%5D%5D",
},
],
Array [
"xpack.uptime.alerts.actionGroups.monitorStatus",
Object {
"reason": "Foo from fairbanks 35 days availability is 98.03%. Alert when < 99.34%.",
"viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/Zm9v?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D",
},
],
Array [
"xpack.uptime.alerts.actionGroups.monitorStatus",
Object {
"reason": "Unreliable from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.",
"viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/dW5yZWxpYWJsZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D",
},
],
Array [
"xpack.uptime.alerts.actionGroups.monitorStatus",
Object {
"reason": "no-name from fairbanks 35 days availability is 90.92%. Alert when < 99.34%.",
"viewInAppUrl": "http://localhost:5601/hfe/app/uptime/monitor/bm8tbmFtZQ==?dateRangeEnd=now&dateRangeStart=2022-03-17T13%3A13%3A33.755Z&filters=%5B%5B%22observer.geo.name%22%2C%5B%22fairbanks%22%5D%5D%5D",
},
],
]

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { min } from 'lodash';
import datemath from '@elastic/datemath';
import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
@ -18,7 +19,7 @@ import {
GetMonitorAvailabilityParams,
} from '../../../common/runtime_types';
import { MONITOR_STATUS } from '../../../common/constants/alerts';
import { updateState } from './common';
import { updateState, getViewInAppUrl } from './common';
import {
commonMonitorStateI18,
commonStateTranslations,
@ -36,7 +37,14 @@ import { getUptimeIndexPattern, IndexPatternTitleAndFields } from '../requests/g
import { UMServerLibs, UptimeESClient, createUptimeESClient } from '../lib';
import { ActionGroupIdsOf } from '../../../../alerting/common';
import { formatDurationFromTimeUnitChar, TimeUnitChar } from '../../../../observability/common';
import { ALERT_REASON_MSG, MESSAGE, MONITOR_WITH_GEO, ACTION_VARIABLES } from './action_variables';
import {
ALERT_REASON_MSG,
MESSAGE,
MONITOR_WITH_GEO,
ACTION_VARIABLES,
VIEW_IN_APP_URL,
} from './action_variables';
import { getMonitorRouteFromMonitorId } from '../../../common/utils/get_monitor_url';
export type ActionGroupIds = ActionGroupIdsOf<typeof MONITOR_STATUS>;
/**
@ -214,7 +222,7 @@ export const getInstanceId = (monitorInfo: Ping, monIdByLoc: string) => {
return `${urlText}_${monIdByLoc}`;
};
export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (_server, libs) => ({
export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (server, libs) => ({
id: 'xpack.uptime.alerts.monitorStatus',
producer: 'uptime',
name: i18n.translate('xpack.uptime.alerts.monitorStatus', {
@ -272,6 +280,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
ACTION_VARIABLES[MESSAGE],
ACTION_VARIABLES[MONITOR_WITH_GEO],
ACTION_VARIABLES[ALERT_REASON_MSG],
ACTION_VARIABLES[VIEW_IN_APP_URL],
],
state: [...commonMonitorStateI18, ...commonStateTranslations],
},
@ -280,10 +289,11 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
async executor({
params: rawParams,
state,
services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle },
services: { savedObjectsClient, scopedClusterClient, alertWithLifecycle, getAlertStartedDate },
rule: {
schedule: { interval },
},
startedAt,
}) {
const {
filters,
@ -297,7 +307,7 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
isAutoGenerated,
timerange: oldVersionTimeRange,
} = rawParams;
const { basePath } = server;
const uptimeEsClient = createUptimeESClient({
esClient: scopedClusterClient.asCurrentUser,
savedObjectsClient,
@ -336,7 +346,6 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
if (isAutoGenerated) {
for (const monitorLoc of downMonitorsByLocation) {
const monitorInfo = monitorLoc.monitorInfo;
const monitorStatusMessageParams = getMonitorDownStatusMessageParams(
monitorInfo,
monitorLoc.count,
@ -348,8 +357,10 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
const statusMessage = getStatusMessage(monitorStatusMessageParams);
const monitorSummary = getMonitorSummary(monitorInfo, statusMessage);
const alertId = getInstanceId(monitorInfo, monitorLoc.location);
const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString();
const alert = alertWithLifecycle({
id: getInstanceId(monitorInfo, monitorLoc.location),
id: alertId,
fields: getMonitorAlertDocument(monitorSummary),
});
@ -360,8 +371,18 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
...updateState(state, true),
});
const relativeViewInAppUrl = getMonitorRouteFromMonitorId({
monitorId: monitorSummary.monitorId,
dateRangeEnd: 'now',
dateRangeStart: indexedStartedAt,
filters: {
'observer.geo.name': [monitorSummary.observerLocation],
},
});
alert.scheduleActions(MONITOR_STATUS.id, {
[ALERT_REASON_MSG]: monitorSummary.reason,
[VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath),
});
}
return updateState(state, downMonitorsByLocation.length > 0);
@ -408,8 +429,10 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
availability
);
const monitorSummary = getMonitorSummary(monitorInfo, statusMessage);
const alertId = getInstanceId(monitorInfo, monIdByLoc);
const indexedStartedAt = getAlertStartedDate(alertId) ?? startedAt.toISOString();
const alert = alertWithLifecycle({
id: getInstanceId(monitorInfo, monIdByLoc),
id: alertId,
fields: getMonitorAlertDocument(monitorSummary),
});
@ -418,12 +441,20 @@ export const statusCheckAlertFactory: UptimeAlertTypeFactory<ActionGroupIds> = (
...monitorSummary,
statusMessage,
});
const relativeViewInAppUrl = getMonitorRouteFromMonitorId({
monitorId: monitorSummary.monitorId,
dateRangeEnd: 'now',
dateRangeStart: indexedStartedAt,
filters: {
'observer.geo.name': [monitorSummary.observerLocation],
},
});
alert.scheduleActions(MONITOR_STATUS.id, {
[ALERT_REASON_MSG]: monitorSummary.reason,
[VIEW_IN_APP_URL]: getViewInAppUrl(relativeViewInAppUrl, basePath),
});
});
return updateState(state, downMonitorsByLocation.length > 0);
},
});

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { Logger } from 'kibana/server';
import { IBasePath, Logger } from 'kibana/server';
import { UMServerLibs } from '../../lib';
import { UptimeCorePluginsSetup, UptimeServerSetup } from '../../adapters';
import type { UptimeRouter } from '../../../types';
@ -25,9 +25,16 @@ import { DYNAMIC_SETTINGS_DEFAULTS } from '../../../../common/constants';
*/
export const bootstrapDependencies = (customRequests?: any, customPlugins: any = {}) => {
const router = {} as UptimeRouter;
const basePath = {
prepend: (url: string) => {
return `/hfe${url}`;
},
publicBaseUrl: 'http://localhost:5601/hfe',
serverBasePath: '/hfe',
} as IBasePath;
// these server/libs parameters don't have any functionality, which is fine
// because we aren't testing them here
const server = { router, config: {} } as UptimeServerSetup;
const server = { router, config: {}, basePath } as UptimeServerSetup;
const plugins: UptimeCorePluginsSetup = customPlugins as any;
const libs: UMServerLibs = { requests: {} } as UMServerLibs;
libs.requests = { ...libs.requests, ...customRequests };
@ -56,6 +63,7 @@ export const createRuleTypeMocks = (
...getUptimeESMockClient(),
...alertsMock.createAlertServices(),
alertWithLifecycle: jest.fn().mockReturnValue({ scheduleActions, replaceState }),
getAlertStartedDate: jest.fn().mockReturnValue('2022-03-17T13:13:33.755Z'),
logger: loggerMock,
};

View file

@ -26,6 +26,7 @@ export type DefaultUptimeAlertInstance<TActionGroupIds extends string> = AlertTy
AlertInstanceContext,
TActionGroupIds
>;
getAlertStartedDate: (alertId: string) => string | null;
}
>;

View file

@ -78,6 +78,7 @@ export class Plugin implements PluginType {
router: core.http.createRouter(),
cloud: plugins.cloud,
kibanaVersion: this.initContext.env.packageInfo.version,
basePath: core.http.basePath,
logger: this.logger,
telemetry: this.telemetryEventsSender,
} as UptimeServerSetup;