Skip to content

Commit

Permalink
Onboard Synthetics Monitor Status rule type with FAAD (elastic#186214)
Browse files Browse the repository at this point in the history
Towards: elastic#169867

This PR onboards the Synthetics Monitor Status rule type with FAAD.

### To verify
I can't get the rule to alert, so I modified the status check to report
the monitor as down. If you know of an easier way pls let me know 🙂

1. Create a [monitor](http://localhost:5601/app/synthetics/monitors), by
default creating a monitor creates a rule.
2. Click on the monitor and grab the id and locationId from the url
3. Go to [the status check
code](https://github.com/elastic/kibana/blob/main/x-pack/plugins/observability_solution/synthetics/server/queries/query_monitor_status.ts#L208)
and replace the object that is returned with the following using the id
and locationId you got from the monitor.
```
{
    up: 0,
    down: 1,
    pending: 0,
    upConfigs: {},
    pendingConfigs: {},
    downConfigs: {
      '${id}-${locationId}': {
        configId: '${id}',
        monitorQueryId: '${id}',
        status: 'down',
        locationId: '${locationId}',
        ping: {
          '@timestamp': new Date().toISOString(),
          state: {
            id: 'test-state',
          },
          monitor: {
            name: 'test-monitor',
          },
          observer: {
            name: 'test-monitor',
          },
        } as any,
        timestamp: new Date().toISOString(),
      },
    },
    enabledMonitorQueryIds: ['${id}'],
  };
```
5. Your rule should create an alert and should saved it in
`.internal.alerts-observability.uptime.alerts-default-000001`
Example:
```
GET .internal.alerts-*/_search
```
6. Recover repeating step 3 using
```
{
    up: 1,
    down: 0,
    pending: 0,
    downConfigs: {},
    pendingConfigs: {},
    upConfigs: {
      '${id}-${locationId}': {
        configId: '${id}',
        monitorQueryId: '${id}',
        status: 'down',
        locationId: '${locationId}',
        ping: {
          '@timestamp': new Date().toISOString(),
          state: {
            id: 'test-state',
          },
          monitor: {
            name: 'test-monitor',
          },
          observer: {
            name: 'test-monitor',
          },
        } as any,
        timestamp: new Date().toISOString(),
      },
    },
    enabledMonitorQueryIds: ['${id}'],
  };
```
8. The alert should be recovered and the AAD in the above index should
be updated `kibana.alert.status: recovered`.
  • Loading branch information
doakalexi authored and bhapas committed Jun 24, 2024
1 parent d863514 commit 24bdd63
Show file tree
Hide file tree
Showing 4 changed files with 206 additions and 159 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
import { IBasePath } from '@kbn/core/server';
import { updateState, setRecoveredAlertsContext } from './common';
import { SyntheticsCommonState } from '../../common/runtime_types/alert_rules/common';
Expand Down Expand Up @@ -186,16 +185,13 @@ describe('updateState', () => {
});

describe('setRecoveredAlertsContext', () => {
const { alertFactory } = alertsMock.createRuleExecutorServices();
const { getRecoveredAlerts } = alertFactory.done();
const alertUuid = 'alert-id';
const location = 'US Central';
const configId = '12345';
const idWithLocation = `${configId}-${location}`;
const basePath = {
publicBaseUrl: 'https://localhost:5601',
} as IBasePath;
const getAlertUuid = () => alertUuid;

const upConfigs = {
[idWithLocation]: {
Expand All @@ -219,17 +215,26 @@ describe('setRecoveredAlertsContext', () => {
};

it('sets context correctly when monitor is deleted', () => {
const setContext = jest.fn();
getRecoveredAlerts.mockReturnValue([
{
getId: () => alertUuid,
getState: () => ({
idWithLocation,
monitorName: 'test-monitor',
}),
setContext,
},
]);
const alertsClientMock = {
report: jest.fn(),
getAlertLimitValue: jest.fn().mockReturnValue(10),
setAlertLimitReached: jest.fn(),
getRecoveredAlerts: jest.fn().mockReturnValue([
{
alert: {
getId: () => alertUuid,
getState: () => ({
idWithLocation,
monitorName: 'test-monitor',
}),
setContext: jest.fn(),
getUuid: () => alertUuid,
},
},
]),
setAlertData: jest.fn(),
isTrackedAlert: jest.fn(),
};
const staleDownConfigs = {
[idWithLocation]: {
configId,
Expand All @@ -250,45 +255,56 @@ describe('setRecoveredAlertsContext', () => {
},
};
setRecoveredAlertsContext({
alertFactory,
alertsClient: alertsClientMock,
basePath,
getAlertUuid,
spaceId: 'default',
staleDownConfigs,
upConfigs: {},
dateFormat,
tz: 'UTC',
});
expect(setContext).toBeCalledWith({
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
configId: '12345',
idWithLocation,
linkMessage: '',
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
monitorName: 'test-monitor',
recoveryReason: 'the monitor has been deleted',
recoveryStatus: 'has been deleted',
monitorUrl: '(unavailable)',
monitorUrlLabel: 'URL',
reason:
'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
stateId: '123456',
status: 'recovered',
expect(alertsClientMock.setAlertData).toBeCalledWith({
id: 'alert-id',
context: {
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
configId: '12345',
idWithLocation,
linkMessage: '',
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
monitorName: 'test-monitor',
recoveryReason: 'the monitor has been deleted',
recoveryStatus: 'has been deleted',
monitorUrl: '(unavailable)',
monitorUrlLabel: 'URL',
reason:
'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
stateId: '123456',
status: 'recovered',
},
});
});

it('sets context correctly when location is removed', () => {
const setContext = jest.fn();
getRecoveredAlerts.mockReturnValue([
{
getId: () => alertUuid,
getState: () => ({
idWithLocation,
monitorName: 'test-monitor',
}),
setContext,
},
]);
const alertsClientMock = {
report: jest.fn(),
getAlertLimitValue: jest.fn().mockReturnValue(10),
setAlertLimitReached: jest.fn(),
getRecoveredAlerts: jest.fn().mockReturnValue([
{
alert: {
getId: () => alertUuid,
getState: () => ({
idWithLocation,
monitorName: 'test-monitor',
}),
setContext: jest.fn(),
getUuid: () => alertUuid,
},
},
]),
setAlertData: jest.fn(),
isTrackedAlert: jest.fn(),
};
const staleDownConfigs = {
[idWithLocation]: {
configId,
Expand All @@ -309,47 +325,58 @@ describe('setRecoveredAlertsContext', () => {
},
};
setRecoveredAlertsContext({
alertFactory,
alertsClient: alertsClientMock,
basePath,
getAlertUuid,
spaceId: 'default',
staleDownConfigs,
upConfigs: {},
dateFormat,
tz: 'UTC',
});
expect(setContext).toBeCalledWith({
configId: '12345',
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
monitorUrl: '(unavailable)',
reason:
'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
idWithLocation,
linkMessage: '',
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
monitorName: 'test-monitor',
recoveryReason: 'this location has been removed from the monitor',
recoveryStatus: 'has recovered',
stateId: '123456',
status: 'recovered',
monitorUrlLabel: 'URL',
expect(alertsClientMock.setAlertData).toBeCalledWith({
id: 'alert-id',
context: {
configId: '12345',
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
monitorUrl: '(unavailable)',
reason:
'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
idWithLocation,
linkMessage: '',
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
monitorName: 'test-monitor',
recoveryReason: 'this location has been removed from the monitor',
recoveryStatus: 'has recovered',
stateId: '123456',
status: 'recovered',
monitorUrlLabel: 'URL',
},
});
});

it('sets context correctly when monitor is up', () => {
const setContext = jest.fn();
getRecoveredAlerts.mockReturnValue([
{
getId: () => alertUuid,
getState: () => ({
idWithLocation,
monitorName: 'test-monitor',
locationId: 'us_west',
configId: '12345-67891',
}),
setContext,
},
]);
const alertsClientMock = {
report: jest.fn(),
getAlertLimitValue: jest.fn().mockReturnValue(10),
setAlertLimitReached: jest.fn(),
getRecoveredAlerts: jest.fn().mockReturnValue([
{
alert: {
getId: () => alertUuid,
getState: () => ({
idWithLocation,
monitorName: 'test-monitor',
locationId: 'us_west',
configId: '12345-67891',
}),
setContext: jest.fn(),
getUuid: () => alertUuid,
},
},
]),
setAlertData: jest.fn(),
isTrackedAlert: jest.fn(),
};
const staleDownConfigs = {
[idWithLocation]: {
configId,
Expand All @@ -370,33 +397,35 @@ describe('setRecoveredAlertsContext', () => {
},
};
setRecoveredAlertsContext({
alertFactory,
alertsClient: alertsClientMock,
basePath,
getAlertUuid,
spaceId: 'default',
staleDownConfigs,
upConfigs,
dateFormat,
tz: 'UTC',
});
expect(setContext).toBeCalledWith({
configId: '12345-67891',
idWithLocation,
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
monitorName: 'test-monitor',
status: 'up',
recoveryReason:
'the monitor is now up again. It ran successfully at Feb 26, 2023 @ 00:00:00.000',
recoveryStatus: 'is now up',
locationId: 'us_west',
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
linkMessage:
'- Link: https://localhost:5601/app/synthetics/monitor/12345-67891/errors/123456?locationId=us_west',
monitorUrl: '(unavailable)',
monitorUrlLabel: 'URL',
reason:
'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
stateId: null,
expect(alertsClientMock.setAlertData).toBeCalledWith({
id: 'alert-id',
context: {
configId: '12345-67891',
idWithLocation,
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
monitorName: 'test-monitor',
status: 'up',
recoveryReason:
'the monitor is now up again. It ran successfully at Feb 26, 2023 @ 00:00:00.000',
recoveryStatus: 'is now up',
locationId: 'us_west',
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',
linkMessage:
'- Link: https://localhost:5601/app/synthetics/monitor/12345-67891/errors/123456?locationId=us_west',
monitorUrl: '(unavailable)',
monitorUrlLabel: 'URL',
reason:
'Monitor "test-monitor" from Unnamed-location is recovered. Checked at February 25, 2023 7:00 PM.',
stateId: null,
},
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,22 @@ import moment, { Moment } from 'moment';
import { isRight } from 'fp-ts/lib/Either';
import Mustache from 'mustache';
import { IBasePath } from '@kbn/core/server';
import { IRuleTypeAlerts, RuleExecutorServices } from '@kbn/alerting-plugin/server';
import {
IRuleTypeAlerts,
ActionGroupIdsOf,
AlertInstanceContext as AlertContext,
AlertInstanceState as AlertState,
} from '@kbn/alerting-plugin/server';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
import { i18n } from '@kbn/i18n';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { legacyExperimentalFieldMap } from '@kbn/alerts-as-data-utils';
import { legacyExperimentalFieldMap, ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
import { PublicAlertsClient } from '@kbn/alerting-plugin/server/alerts_client/types';
import { combineFiltersAndUserSearch, stringifyKueries } from '../../common/lib';
import { SYNTHETICS_RULE_TYPES_ALERT_CONTEXT } from '../../common/constants/synthetics_alerts';
import {
MonitorStatusActionGroup,
SYNTHETICS_RULE_TYPES_ALERT_CONTEXT,
} from '../../common/constants/synthetics_alerts';
import { uptimeRuleFieldMap } from '../../common/rules/uptime_rule_field_map';
import {
getUptimeIndexPattern,
Expand All @@ -26,7 +35,6 @@ import { getMonitorSummary } from './status_rule/message_utils';
import {
SyntheticsCommonState,
SyntheticsCommonStateCodec,
SyntheticsMonitorStatusAlertState,
} from '../../common/runtime_types/alert_rules/common';
import { getSyntheticsErrorRouteFromMonitorId } from '../../common/utils/get_synthetics_monitor_url';
import { ALERT_DETAILS_URL, RECOVERY_REASON } from './action_variables';
Expand Down Expand Up @@ -154,30 +162,33 @@ export const getErrorDuration = (startedAt: Moment, endsAt: Moment) => {
};

export const setRecoveredAlertsContext = ({
alertFactory,
alertsClient,
basePath,
getAlertUuid,
spaceId,
staleDownConfigs,
upConfigs,
dateFormat,
tz,
}: {
alertFactory: RuleExecutorServices['alertFactory'];
alertsClient: PublicAlertsClient<
ObservabilityUptimeAlert,
AlertState,
AlertContext,
ActionGroupIdsOf<MonitorStatusActionGroup>
>;
basePath?: IBasePath;
getAlertUuid?: (alertId: string) => string | null;
spaceId?: string;
staleDownConfigs: AlertOverviewStatus['staleDownConfigs'];
upConfigs: AlertOverviewStatus['upConfigs'];
dateFormat: string;
tz: string;
}) => {
const { getRecoveredAlerts } = alertFactory.done();
for (const alert of getRecoveredAlerts()) {
const recoveredAlertId = alert.getId();
const alertUuid = getAlertUuid?.(recoveredAlertId) || undefined;
const recoveredAlerts = alertsClient.getRecoveredAlerts() ?? [];
for (const recoveredAlert of recoveredAlerts) {
const recoveredAlertId = recoveredAlert.alert.getId();
const alertUuid = recoveredAlert.alert.getUuid();

const state = alert.getState() as SyntheticsCommonState & SyntheticsMonitorStatusAlertState;
const state = recoveredAlert.alert.getState();

let recoveryReason = '';
let recoveryStatus = i18n.translate(
Expand Down Expand Up @@ -279,7 +290,7 @@ export const setRecoveredAlertsContext = ({
}
}

alert.setContext({
const context = {
...state,
...(monitorSummary ? monitorSummary : {}),
lastErrorMessage,
Expand All @@ -290,7 +301,8 @@ export const setRecoveredAlertsContext = ({
...(basePath && spaceId && alertUuid
? { [ALERT_DETAILS_URL]: getAlertDetailsUrl(basePath, spaceId, alertUuid) }
: {}),
});
};
alertsClient.setAlertData({ id: recoveredAlertId, context });
}
};

Expand Down
Loading

0 comments on commit 24bdd63

Please sign in to comment.