[Synthetics] Settings add config to enable default rules (#190800)

## Summary

Settings add config to enable default rules !!

separated out of https://github.com/elastic/kibana/pull/186585 !!

<img width="1725" alt="image"
src="https://github.com/user-attachments/assets/3fc18d13-d0fe-4f08-8e19-ae43fcb83228">

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Dominique Belcher <dominique.clarke@elastic.co>
This commit is contained in:
Shahzad 2024-08-22 17:13:16 +02:00 committed by GitHub
parent 9653d7e1fc
commit 20ccb21c94
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
34 changed files with 382 additions and 140 deletions

View file

@ -16,4 +16,6 @@ export const DYNAMIC_SETTINGS_DEFAULTS: DynamicSettings = {
cc: [],
bcc: [],
},
defaultTLSRuleEnabled: true,
defaultStatusRuleEnabled: true,
};

View file

@ -34,6 +34,8 @@ export const DynamicSettingsCodec = t.intersection([
}),
t.partial({
defaultEmail: DefaultEmailCodec,
defaultTLSRuleEnabled: t.boolean,
defaultStatusRuleEnabled: t.boolean,
}),
]);

View file

@ -44,7 +44,7 @@ export const OverviewPendingStatusMetaDataCodec = t.intersection([
monitorQueryId: t.string,
configId: t.string,
status: t.string,
location: t.string,
locationId: t.string,
}),
t.partial({
timestamp: t.string,

View file

@ -0,0 +1,20 @@
/*
* 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 { SanitizedRule, SanitizedRuleAction, RuleSystemAction } from '@kbn/alerting-plugin/common';
import { SYNTHETICS_STATUS_RULE, SYNTHETICS_TLS_RULE } from '../constants/synthetics_alerts';
export type DefaultRuleType = typeof SYNTHETICS_STATUS_RULE | typeof SYNTHETICS_TLS_RULE;
type SYNTHETICS_DEFAULT_RULE = Omit<SanitizedRule<{}>, 'systemActions' | 'actions'> & {
actions: Array<SanitizedRuleAction | RuleSystemAction>;
ruleTypeId: SanitizedRule['alertTypeId'];
};
export interface DEFAULT_ALERT_RESPONSE {
statusRule: SYNTHETICS_DEFAULT_RULE | null;
tlsRule: SYNTHETICS_DEFAULT_RULE | null;
}

View file

@ -7,3 +7,4 @@
export * from './synthetics_monitor';
export * from './monitor_validation';
export * from './default_alerts';

View file

@ -25,7 +25,7 @@ import {
} from '../../../state';
import { ClientPluginsStart } from '../../../../../plugin';
export const useSyntheticsAlert = (isOpen: boolean) => {
export const useSyntheticsRules = (isOpen: boolean) => {
const dispatch = useDispatch();
const defaultRules = useSelector(selectSyntheticsAlerts);
@ -64,14 +64,15 @@ export const useSyntheticsAlert = (isOpen: boolean) => {
const { triggersActionsUi } = useKibana<ClientPluginsStart>().services;
const EditAlertFlyout = useMemo(() => {
if (!defaultRules) {
const initialRule =
alertFlyoutVisible === SYNTHETICS_TLS_RULE ? defaultRules?.tlsRule : defaultRules?.statusRule;
if (!initialRule) {
return null;
}
return triggersActionsUi.getEditRuleFlyout({
onClose: () => dispatch(setAlertFlyoutVisible(null)),
hideInterval: true,
initialRule:
alertFlyoutVisible === SYNTHETICS_TLS_RULE ? defaultRules.tlsRule : defaultRules.statusRule,
initialRule,
});
}, [defaultRules, dispatch, triggersActionsUi, alertFlyoutVisible]);

View file

@ -26,7 +26,7 @@ import {
import { ManageRulesLink } from '../common/links/manage_rules_link';
import { ClientPluginsStart } from '../../../../plugin';
import { ToggleFlyoutTranslations } from './hooks/translations';
import { useSyntheticsAlert } from './hooks/use_synthetics_alert';
import { useSyntheticsRules } from './hooks/use_synthetics_rules';
import {
selectAlertFlyoutVisibility,
selectMonitorListState,
@ -40,7 +40,7 @@ export const ToggleAlertFlyoutButton = () => {
const { application } = useKibana<ClientPluginsStart>().services;
const hasUptimeWrite = application?.capabilities.uptime?.save ?? false;
const { EditAlertFlyout, loading } = useSyntheticsAlert(isOpen);
const { EditAlertFlyout, loading } = useSyntheticsRules(isOpen);
const { loaded, data: monitors } = useSelector(selectMonitorListState);

View file

@ -15,6 +15,7 @@ import {
EuiFlexItem,
EuiForm,
EuiSpacer,
EuiSwitch,
} from '@elastic/eui';
import { useDispatch, useSelector } from 'react-redux';
import { useKibana } from '@kbn/kibana-react-plugin/public';
@ -80,6 +81,50 @@ export const AlertDefaultsForm = () => {
return (
<EuiForm>
<EuiSpacer size="m" />
<EuiDescribedFormGroup
title={
<h4>
<FormattedMessage
id="xpack.synthetics.settings.defaultConnectors"
defaultMessage="Default rules"
/>
</h4>
}
description={
<FormattedMessage
id="xpack.synthetics.settings.defaultConnectors.description"
defaultMessage="Default rules are automatically created. You can disable creation of default rules here."
/>
}
>
<EuiSpacer size="s" />
<EuiSwitch
label={i18n.translate('xpack.synthetics.ruleStatusDefaultsForm.euiSwitch.enabledLabel', {
defaultMessage: 'Status rule enabled',
})}
checked={formFields?.defaultStatusRuleEnabled ?? true}
onChange={() => {
setFormFields({
...formFields,
defaultStatusRuleEnabled: !(formFields.defaultStatusRuleEnabled ?? true),
});
}}
/>
<EuiSpacer size="m" />
<EuiSwitch
label={i18n.translate('xpack.synthetics.ruleTLSDefaultsForm.euiSwitch.enabledLabel', {
defaultMessage: 'TLS rule enabled',
})}
checked={formFields?.defaultTLSRuleEnabled ?? true}
onChange={() => {
setFormFields({
...formFields,
defaultTLSRuleEnabled: !(formFields.defaultTLSRuleEnabled ?? true),
});
}}
/>
</EuiDescribedFormGroup>
<EuiSpacer size="m" />
<EuiDescribedFormGroup
title={

View file

@ -154,7 +154,7 @@ describe('useMonitorsSortedByStatus', () => {
[`test-monitor-4-${location1.id}`]: {
configId: 'test-monitor-4',
monitorQueryId: 'test-monitor-4',
location: location1.id,
locationId: location1.id,
},
},
},

View file

@ -5,24 +5,21 @@
* 2.0.
*/
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { DEFAULT_ALERT_RESPONSE } from '../../../../../common/types/default_alerts';
import { createAsyncAction } from '../utils/actions';
export const getDefaultAlertingAction = createAsyncAction<
void,
{ statusRule: Rule; tlsRule: Rule }
>('getDefaultAlertingAction');
export const getDefaultAlertingAction = createAsyncAction<void, DEFAULT_ALERT_RESPONSE>(
'getDefaultAlertingAction'
);
export const enableDefaultAlertingAction = createAsyncAction<
void,
{ statusRule: Rule; tlsRule: Rule }
>('enableDefaultAlertingAction');
export const enableDefaultAlertingAction = createAsyncAction<void, DEFAULT_ALERT_RESPONSE>(
'enableDefaultAlertingAction'
);
export const enableDefaultAlertingSilentlyAction = createAsyncAction<
void,
{ statusRule: Rule; tlsRule: Rule }
>('enableDefaultAlertingSilentlyAction');
export const enableDefaultAlertingSilentlyAction = createAsyncAction<void, DEFAULT_ALERT_RESPONSE>(
'enableDefaultAlertingSilentlyAction'
);
export const updateDefaultAlertingAction = createAsyncAction<void, Rule>(
export const updateDefaultAlertingAction = createAsyncAction<void, DEFAULT_ALERT_RESPONSE>(
'updateDefaultAlertingAction'
);

View file

@ -5,18 +5,18 @@
* 2.0.
*/
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { SYNTHETICS_API_URLS } from '../../../../../common/constants';
import { DEFAULT_ALERT_RESPONSE } from '../../../../../common/types/default_alerts';
import { apiService } from '../../../../utils/api_service';
export async function getDefaultAlertingAPI(): Promise<{ statusRule: Rule; tlsRule: Rule }> {
export async function getDefaultAlertingAPI(): Promise<DEFAULT_ALERT_RESPONSE> {
return apiService.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
}
export async function enableDefaultAlertingAPI(): Promise<{ statusRule: Rule; tlsRule: Rule }> {
export async function enableDefaultAlertingAPI(): Promise<DEFAULT_ALERT_RESPONSE> {
return apiService.post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
}
export async function updateDefaultAlertingAPI(): Promise<Rule> {
export async function updateDefaultAlertingAPI(): Promise<DEFAULT_ALERT_RESPONSE> {
return apiService.put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING);
}

View file

@ -6,7 +6,7 @@
*/
import { createReducer } from '@reduxjs/toolkit';
import { Rule } from '@kbn/triggers-actions-ui-plugin/public';
import { DEFAULT_ALERT_RESPONSE } from '../../../../../common/types/default_alerts';
import { IHttpSerializedFetchError } from '..';
import {
enableDefaultAlertingAction,
@ -16,7 +16,7 @@ import {
} from './actions';
export interface DefaultAlertingState {
data?: { statusRule: Rule; tlsRule: Rule };
data?: DEFAULT_ALERT_RESPONSE;
success: boolean | null;
loading: boolean;
error: IHttpSerializedFetchError | null;

View file

@ -36,9 +36,17 @@ export const getDynamicSettings = async (): Promise<DynamicSettings> => {
export const setDynamicSettings = async ({
settings,
}: SaveApiRequest): Promise<DynamicSettingsSaveResponse> => {
const newSettings: DynamicSettings = {
certAgeThreshold: settings.certAgeThreshold,
certExpirationThreshold: settings.certExpirationThreshold,
defaultConnectors: settings.defaultConnectors,
defaultEmail: settings.defaultEmail,
defaultTLSRuleEnabled: settings.defaultTLSRuleEnabled,
defaultStatusRuleEnabled: settings.defaultStatusRuleEnabled,
};
return await apiService.put(
SYNTHETICS_API_URLS.DYNAMIC_SETTINGS,
settings,
newSettings,
DynamicSettingsSaveCodec,
{
version: '2023-10-31',

View file

@ -274,6 +274,7 @@ describe('setRecoveredAlertsContext', () => {
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
monitorName: 'test-monitor',
recoveryReason: 'the monitor has been deleted',
'kibana.alert.reason': 'the monitor has been deleted',
recoveryStatus: 'has been deleted',
monitorUrl: '(unavailable)',
monitorUrlLabel: 'URL',
@ -350,6 +351,7 @@ describe('setRecoveredAlertsContext', () => {
alertDetailsUrl: 'https://localhost:5601/app/observability/alerts/alert-id',
monitorName: 'test-monitor',
recoveryReason: 'this location has been removed from the monitor',
'kibana.alert.reason': 'this location has been removed from the monitor',
recoveryStatus: 'has recovered',
stateId: '123456',
status: 'recovered',
@ -421,6 +423,8 @@ describe('setRecoveredAlertsContext', () => {
status: 'up',
recoveryReason:
'the monitor is now up again. It ran successfully at Feb 26, 2023 @ 00:00:00.000',
'kibana.alert.reason':
'the monitor is now up again. It ran successfully at Feb 26, 2023 @ 00:00:00.000',
recoveryStatus: 'is now up',
locationId: location,
checkedAt: 'Feb 26, 2023 @ 00:00:00.000',

View file

@ -19,6 +19,7 @@ import { i18n } from '@kbn/i18n';
import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query';
import { legacyExperimentalFieldMap, ObservabilityUptimeAlert } from '@kbn/alerts-as-data-utils';
import { PublicAlertsClient } from '@kbn/alerting-plugin/server/alerts_client/types';
import { ALERT_REASON } from '@kbn/rule-data-utils';
import { syntheticsRuleFieldMap } from '../../common/rules/synthetics_rule_field_map';
import { combineFiltersAndUserSearch, stringifyKueries } from '../../common/lib';
import {
@ -298,6 +299,7 @@ export const setRecoveredAlertsContext = ({
linkMessage,
...(isUp ? { status: 'up' } : {}),
...(recoveryReason ? { [RECOVERY_REASON]: recoveryReason } : {}),
...(recoveryReason ? { [ALERT_REASON]: recoveryReason } : {}),
...(basePath && spaceId && alertUuid
? { [ALERT_DETAILS_URL]: getAlertDetailsUrl(basePath, spaceId, alertUuid) }
: {}),

View file

@ -79,7 +79,7 @@ export class StatusRuleExecutor {
monitorLocationMap,
projectMonitorsCount,
monitorQueryIdToConfigIdMap,
} = processMonitors(this.monitors, this.server, this.soClient, this.syntheticsMonitorClient);
} = processMonitors(this.monitors);
return {
enabledMonitorQueryIds,

View file

@ -74,7 +74,7 @@ export class TLSRuleExecutor {
monitorLocationMap,
projectMonitorsCount,
monitorQueryIdToConfigIdMap,
} = processMonitors(this.monitors, this.server, this.soClient, this.syntheticsMonitorClient);
} = processMonitors(this.monitors);
return {
enabledMonitorQueryIds,

View file

@ -201,7 +201,7 @@ export async function queryMonitorStatus(
configId: `${monitorQueryIdToConfigIdMap[queryId]}`,
monitorQueryId: queryId,
status: 'unknown',
location: loc,
locationId: loc,
};
});
}

View file

@ -34,13 +34,7 @@ export const getSyntheticsCertsRoute: SyntheticsRestApiRouteFactory<
to: schema.maybe(schema.string()),
}),
},
handler: async ({
request,
syntheticsEsClient,
savedObjectsClient,
server,
syntheticsMonitorClient,
}) => {
handler: async ({ request, syntheticsEsClient, savedObjectsClient }) => {
const queryParams = request.query;
const monitors = await getAllMonitors({
@ -57,12 +51,7 @@ export const getSyntheticsCertsRoute: SyntheticsRestApiRouteFactory<
};
}
const { enabledMonitorQueryIds } = processMonitors(
monitors,
server,
savedObjectsClient,
syntheticsMonitorClient
);
const { enabledMonitorQueryIds } = processMonitors(monitors);
const data = await getSyntheticsCerts({
...queryParams,

View file

@ -7,6 +7,7 @@
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
import { FindActionResult } from '@kbn/actions-plugin/server';
import { DynamicSettingsAttributes } from '../../runtime_types/settings';
import { savedObjectsAdapter } from '../../saved_objects';
import { populateAlertActions } from '../../../common/rules/alert_actions';
import {
@ -19,12 +20,12 @@ import {
SYNTHETICS_STATUS_RULE,
SYNTHETICS_TLS_RULE,
} from '../../../common/constants/synthetics_alerts';
type DefaultRuleType = typeof SYNTHETICS_STATUS_RULE | typeof SYNTHETICS_TLS_RULE;
import { DefaultRuleType } from '../../../common/types/default_alerts';
export class DefaultAlertService {
context: UptimeRequestHandlerContext;
soClient: SavedObjectsClientContract;
server: SyntheticsServerSetup;
settings?: DynamicSettingsAttributes;
constructor(
context: UptimeRequestHandlerContext,
@ -36,7 +37,16 @@ export class DefaultAlertService {
this.soClient = soClient;
}
async getSettings() {
if (!this.settings) {
this.settings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient);
}
return this.settings;
}
async setupDefaultAlerts() {
this.settings = await this.getSettings();
const [statusRule, tlsRule] = await Promise.allSettled([
this.setupStatusRule(),
this.setupTlsRule(),
@ -50,12 +60,15 @@ export class DefaultAlertService {
}
return {
statusRule: statusRule.status === 'fulfilled' ? statusRule.value : null,
tlsRule: tlsRule.status === 'fulfilled' ? tlsRule.value : null,
statusRule: statusRule.status === 'fulfilled' && statusRule.value ? statusRule.value : null,
tlsRule: tlsRule.status === 'fulfilled' && tlsRule.value ? tlsRule.value : null,
};
}
setupStatusRule() {
if (this.settings?.defaultStatusRuleEnabled === false) {
return;
}
return this.createDefaultAlertIfNotExist(
SYNTHETICS_STATUS_RULE,
`Synthetics status internal rule`,
@ -64,6 +77,9 @@ export class DefaultAlertService {
}
setupTlsRule() {
if (this.settings?.defaultTLSRuleEnabled === false) {
return;
}
return this.createDefaultAlertIfNotExist(
SYNTHETICS_TLS_RULE,
`Synthetics internal TLS rule`,
@ -78,7 +94,7 @@ export class DefaultAlertService {
options: {
page: 1,
perPage: 1,
filter: `alert.attributes.alertTypeId:(${ruleType})`,
filter: `alert.attributes.alertTypeId:(${ruleType}) AND alert.attributes.tags:"SYNTHETICS_DEFAULT_ALERT"`,
},
});
@ -88,6 +104,7 @@ export class DefaultAlertService {
const { actions = [], systemActions = [], ...alert } = data[0];
return { ...alert, actions: [...actions, ...systemActions], ruleTypeId: alert.alertTypeId };
}
async createDefaultAlertIfNotExist(ruleType: DefaultRuleType, name: string, interval: string) {
const alert = await this.getExistingAlert(ruleType);
if (alert) {
@ -121,11 +138,30 @@ export class DefaultAlertService {
};
}
updateStatusRule() {
return this.updateDefaultAlert(SYNTHETICS_STATUS_RULE, `Synthetics status internal rule`, '1m');
async updateStatusRule(enabled?: boolean) {
if (enabled) {
return this.updateDefaultAlert(
SYNTHETICS_STATUS_RULE,
`Synthetics status internal rule`,
'1m'
);
} else {
const rulesClient = (await this.context.alerting)?.getRulesClient();
await rulesClient.bulkDeleteRules({
filter: `alert.attributes.alertTypeId:"${SYNTHETICS_STATUS_RULE}" AND alert.attributes.tags:"SYNTHETICS_DEFAULT_ALERT"`,
});
}
}
updateTlsRule() {
return this.updateDefaultAlert(SYNTHETICS_TLS_RULE, `Synthetics internal TLS rule`, '1m');
async updateTlsRule(enabled?: boolean) {
if (enabled) {
return this.updateDefaultAlert(SYNTHETICS_TLS_RULE, `Synthetics internal TLS rule`, '1m');
} else {
const rulesClient = (await this.context.alerting)?.getRulesClient();
await rulesClient.bulkDeleteRules({
filter: `alert.attributes.alertTypeId:"${SYNTHETICS_TLS_RULE}" AND alert.attributes.tags:"SYNTHETICS_DEFAULT_ALERT"`,
});
}
}
async updateDefaultAlert(ruleType: DefaultRuleType, name: string, interval: string) {
@ -195,14 +231,15 @@ export class DefaultAlertService {
async getActionConnectors() {
const actionsClient = (await this.context.actions)?.getActionsClient();
const settings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient);
if (!this.settings) {
this.settings = await savedObjectsAdapter.getSyntheticsDynamicSettings(this.soClient);
}
let actionConnectors: FindActionResult[] = [];
try {
actionConnectors = await actionsClient.getAll();
} catch (e) {
this.server.logger.error(e);
}
return { actionConnectors, settings };
return { actionConnectors, settings: this.settings };
}
}

View file

@ -8,12 +8,13 @@
import { DefaultAlertService } from './default_alert_service';
import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts';
export const enableDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'POST',
path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING,
validate: {},
handler: async ({ context, server, savedObjectsClient }): Promise<any> => {
handler: async ({ context, server, savedObjectsClient }): Promise<DEFAULT_ALERT_RESPONSE> => {
const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient);
return defaultAlertService.setupDefaultAlerts();

View file

@ -12,19 +12,20 @@ import {
import { DefaultAlertService } from './default_alert_service';
import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts';
export const getDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'GET',
path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING,
validate: {},
handler: async ({ context, server, savedObjectsClient }): Promise<any> => {
handler: async ({ context, server, savedObjectsClient }): Promise<DEFAULT_ALERT_RESPONSE> => {
const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient);
const statusRule = defaultAlertService.getExistingAlert(SYNTHETICS_STATUS_RULE);
const tlsRule = defaultAlertService.getExistingAlert(SYNTHETICS_TLS_RULE);
const [status, tls] = await Promise.all([statusRule, tlsRule]);
return {
statusRule: status,
tlsRule: tls,
statusRule: status || null,
tlsRule: tls || null,
};
},
});

View file

@ -8,21 +8,41 @@
import { DefaultAlertService } from './default_alert_service';
import { SyntheticsRestApiRouteFactory } from '../types';
import { SYNTHETICS_API_URLS } from '../../../common/constants';
import { savedObjectsAdapter } from '../../saved_objects';
import { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts';
export const updateDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ({
method: 'PUT',
path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING,
validate: {},
handler: async ({ context, server, savedObjectsClient }): Promise<any> => {
handler: async ({
request,
context,
server,
savedObjectsClient,
}): Promise<DEFAULT_ALERT_RESPONSE> => {
const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient);
const { defaultTLSRuleEnabled, defaultStatusRuleEnabled } =
await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient);
const [statusRule, tlsRule] = await Promise.all([
defaultAlertService.updateStatusRule(),
defaultAlertService.updateTlsRule(),
]);
return {
statusRule,
tlsRule,
};
const updateStatusRulePromise = defaultAlertService.updateStatusRule(defaultStatusRuleEnabled);
const updateTLSRulePromise = defaultAlertService.updateTlsRule(defaultTLSRuleEnabled);
try {
const [statusRule, tlsRule] = await Promise.all([
updateStatusRulePromise,
updateTLSRulePromise,
]);
return {
statusRule: statusRule || null,
tlsRule: tlsRule || null,
};
} catch (e) {
server.logger.error(`Error updating default alerting rules: ${e}`);
return {
statusRule: null,
tlsRule: null,
};
}
},
});

View file

@ -593,25 +593,25 @@ describe('current status route', () => {
pendingConfigs: {
'id3-Asia/Pacific - Japan': {
configId: 'id3',
location: 'Asia/Pacific - Japan',
locationId: 'Asia/Pacific - Japan',
monitorQueryId: 'project-monitor-id',
status: 'unknown',
},
'id3-Europe - Germany': {
configId: 'id3',
location: 'Europe - Germany',
locationId: 'Europe - Germany',
monitorQueryId: 'project-monitor-id',
status: 'unknown',
},
'id4-Asia/Pacific - Japan': {
configId: 'id4',
location: 'Asia/Pacific - Japan',
locationId: 'Asia/Pacific - Japan',
monitorQueryId: 'id4',
status: 'unknown',
},
'id4-Europe - Germany': {
configId: 'id4',
location: 'Europe - Germany',
locationId: 'Europe - Germany',
monitorQueryId: 'id4',
status: 'unknown',
},

View file

@ -36,7 +36,7 @@ export function periodToMs(schedule: { number: string; unit: Unit }) {
* @returns The counts of up/down/disabled monitor by location, and a map of each monitor:location status.
*/
export async function getStatus(context: RouteContext, params: OverviewStatusQuery) {
const { syntheticsEsClient, syntheticsMonitorClient, savedObjectsClient, server } = context;
const { syntheticsEsClient, savedObjectsClient } = context;
const { query, scopeStatusByLocation = true } = params;
@ -77,13 +77,7 @@ export async function getStatus(context: RouteContext, params: OverviewStatusQue
disabledMonitorsCount,
projectMonitorsCount,
monitorQueryIdToConfigIdMap,
} = processMonitors(
allMonitors,
server,
savedObjectsClient,
syntheticsMonitorClient,
queryLocations
);
} = processMonitors(allMonitors, queryLocations);
// Account for locations filter
const listOfLocationAfterFilter =

View file

@ -22,7 +22,7 @@ export const createGetDynamicSettingsRoute: SyntheticsRestApiRouteFactory<
handler: async ({ savedObjectsClient }) => {
const dynamicSettingsAttributes: DynamicSettingsAttributes =
await savedObjectsAdapter.getSyntheticsDynamicSettings(savedObjectsClient);
return fromAttribute(dynamicSettingsAttributes);
return fromSettingsAttribute(dynamicSettingsAttributes);
},
});
@ -42,16 +42,20 @@ export const createPostDynamicSettingsRoute: SyntheticsRestApiRouteFactory = ()
...newSettings,
} as DynamicSettingsAttributes);
return fromAttribute(attr as DynamicSettingsAttributes);
return fromSettingsAttribute(attr as DynamicSettingsAttributes);
},
});
const fromAttribute = (attr: DynamicSettingsAttributes) => {
export const fromSettingsAttribute = (
attr: DynamicSettingsAttributes
): DynamicSettingsAttributes => {
return {
certExpirationThreshold: attr.certExpirationThreshold,
certAgeThreshold: attr.certAgeThreshold,
defaultConnectors: attr.defaultConnectors,
defaultEmail: attr.defaultEmail,
defaultStatusRuleEnabled: attr.defaultStatusRuleEnabled ?? true,
defaultTLSRuleEnabled: attr.defaultTLSRuleEnabled ?? true,
};
};
@ -72,6 +76,8 @@ export const DynamicSettingsSchema = schema.object({
certAgeThreshold: schema.maybe(schema.number({ min: 1, validate: validateInteger })),
certExpirationThreshold: schema.maybe(schema.number({ min: 1, validate: validateInteger })),
defaultConnectors: schema.maybe(schema.arrayOf(schema.string())),
defaultStatusRuleEnabled: schema.maybe(schema.boolean()),
defaultTLSRuleEnabled: schema.maybe(schema.boolean()),
defaultEmail: schema.maybe(
schema.object({
to: schema.arrayOf(schema.string()),

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { SyntheticsRestApiRouteFactory } from '../types';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { monitorAttributes, syntheticsMonitorType } from '../../../common/types/saved_objects';
import {
ConfigKey,
MonitorFiltersResult,
@ -30,7 +30,7 @@ interface AggsResponse {
projectsAggs: {
buckets: Buckets;
};
monitorTypeAggs: {
monitorTypesAggs: {
buckets: Buckets;
};
monitorIdsAggs: {
@ -85,7 +85,7 @@ export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory<
searchFields: SEARCH_FIELDS,
});
const { monitorTypeAggs, tagsAggs, locationsAggs, projectsAggs, monitorIdsAggs } =
const { monitorTypesAggs, tagsAggs, locationsAggs, projectsAggs, monitorIdsAggs } =
(data?.aggregations as AggsResponse) ?? {};
const allLocationsMap = new Map(allLocations.map((obj) => [obj.id, obj.label]));
@ -114,7 +114,7 @@ export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory<
count,
})) ?? [],
monitorTypes:
monitorTypeAggs?.buckets?.map(({ key, doc_count: count }) => ({
monitorTypesAggs?.buckets?.map(({ key, doc_count: count }) => ({
label: key,
value: key,
count,
@ -129,35 +129,42 @@ export const getSyntheticsSuggestionsRoute: SyntheticsRestApiRouteFactory<
const aggs = {
tagsAggs: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.TAGS}`,
field: `${monitorAttributes}.${ConfigKey.TAGS}`,
size: 10000,
exclude: [''],
},
},
monitorTypeAggs: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.MONITOR_TYPE}.keyword`,
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
locationsAggs: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.LOCATIONS}.id`,
field: `${monitorAttributes}.${ConfigKey.LOCATIONS}.id`,
size: 10000,
exclude: [''],
},
},
projectsAggs: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.PROJECT_ID}`,
field: `${monitorAttributes}.${ConfigKey.PROJECT_ID}`,
size: 10000,
exclude: [''],
},
},
monitorTypesAggs: {
terms: {
field: `${monitorAttributes}.${ConfigKey.MONITOR_TYPE}.keyword`,
size: 10000,
exclude: [''],
},
},
monitorIdsAggs: {
terms: {
field: `${syntheticsMonitorType}.attributes.${ConfigKey.MONITOR_QUERY_ID}`,
field: `${monitorAttributes}.${ConfigKey.MONITOR_QUERY_ID}`,
size: 10000,
exclude: [''],
},

View file

@ -25,6 +25,8 @@ export const DynamicSettingsAttributesCodec = t.intersection([
}),
t.partial({
defaultEmail: DefaultEmailCodec,
defaultStatusRuleEnabled: t.boolean,
defaultTLSRuleEnabled: t.boolean,
}),
]);

View file

@ -12,6 +12,7 @@ import {
} from '@kbn/core/server';
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
import { fromSettingsAttribute } from '../routes/settings/dynamic_settings';
import {
syntheticsSettings,
syntheticsSettingsObjectId,
@ -62,7 +63,7 @@ export const savedObjectsAdapter = {
syntheticsSettingsObjectType,
syntheticsSettingsObjectId
);
return obj?.attributes ?? DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES;
return fromSettingsAttribute(obj?.attributes ?? DYNAMIC_SETTINGS_DEFAULT_ATTRIBUTES);
} catch (getErr) {
if (SavedObjectsErrorHelpers.isNotFoundError(getErr)) {
// If the object doesn't exist, check to see if uptime settings exist

View file

@ -6,46 +6,11 @@
*/
import { processMonitors } from './get_all_monitors';
import { mockEncryptedSO } from '../../synthetics_service/utils/mocks';
import { loggerMock } from '@kbn/logging-mocks';
import { savedObjectsClientMock } from '@kbn/core-saved-objects-api-server-mocks';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
import { SyntheticsService } from '../../synthetics_service/synthetics_service';
import * as getLocations from '../../synthetics_service/get_all_locations';
import { SyntheticsServerSetup } from '../../types';
describe('processMonitors', () => {
const mockEsClient = {
search: jest.fn(),
};
const logger = loggerMock.create();
const soClient = savedObjectsClientMock.create();
const serverMock: SyntheticsServerSetup = {
logger,
syntheticsEsClient: mockEsClient,
authSavedObjectsClient: soClient,
config: {
service: {
username: 'dev',
password: '12345',
manifestUrl: 'http://localhost:8080/api/manifest',
},
},
spaces: {
spacesService: {
getSpaceId: jest.fn().mockReturnValue('test-space'),
},
},
encryptedSavedObjects: mockEncryptedSO(),
} as unknown as SyntheticsServerSetup;
const syntheticsService = new SyntheticsService(serverMock);
const monitorClient = new SyntheticsMonitorClient(syntheticsService, serverMock);
it('should return a processed data', async () => {
const result = processMonitors(testMonitors, serverMock, soClient, monitorClient);
const result = processMonitors(testMonitors);
expect(result).toEqual({
allIds: [
'aa925d91-40b0-4f8f-b695-bb9b53cd4e22',
@ -81,7 +46,7 @@ describe('processMonitors', () => {
it('should return a processed data where location label is missing', async () => {
testMonitors[0].attributes.locations[0].label = undefined;
const result = processMonitors(testMonitors, serverMock, soClient, monitorClient);
const result = processMonitors(testMonitors);
expect(result).toEqual({
allIds: [
'aa925d91-40b0-4f8f-b695-bb9b53cd4e22',
@ -155,7 +120,7 @@ describe('processMonitors', () => {
)
);
const result = processMonitors(testMonitors, serverMock, soClient, monitorClient);
const result = processMonitors(testMonitors);
expect(result).toEqual({
allIds: [
'aa925d91-40b0-4f8f-b695-bb9b53cd4e22',

View file

@ -11,7 +11,6 @@ import {
SavedObjectsFindResult,
} from '@kbn/core-saved-objects-api-server';
import { intersection } from 'lodash';
import { SyntheticsServerSetup } from '../../types';
import { syntheticsMonitorType } from '../../../common/types/saved_objects';
import { periodToMs } from '../../routes/overview_status/overview_status';
import {
@ -19,7 +18,6 @@ import {
EncryptedSyntheticsMonitorAttributes,
SourceType,
} from '../../../common/runtime_types';
import { SyntheticsMonitorClient } from '../../synthetics_service/synthetics_monitor/synthetics_monitor_client';
export const getAllMonitors = async ({
soClient,
@ -57,9 +55,6 @@ export const getAllMonitors = async ({
export const processMonitors = (
allMonitors: Array<SavedObjectsFindResult<EncryptedSyntheticsMonitorAttributes>>,
server: SyntheticsServerSetup,
soClient: SavedObjectsClientContract,
syntheticsMonitorClient: SyntheticsMonitorClient,
queryLocations?: string[] | string
) => {
/**

View file

@ -17,6 +17,7 @@ import {
Logger,
SavedObjectsClientContract,
} from '@kbn/core/server';
import { PluginStartContract as AlertingPluginStart } from '@kbn/alerting-plugin/server';
import { SharePluginSetup } from '@kbn/share-plugin/server';
import { ObservabilityPluginSetup } from '@kbn/observability-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
@ -85,6 +86,7 @@ export interface SyntheticsPluginsStartDependencies {
taskManager: TaskManagerStartContract;
telemetry: TelemetryPluginStart;
spaces?: SpacesPluginStart;
alerting: AlertingPluginStart;
}
export type UptimeRequestHandlerContext = CustomRequestHandlerContext<{

View file

@ -77,7 +77,7 @@
"@kbn/react-kibana-context-render",
"@kbn/react-kibana-context-theme",
"@kbn/react-kibana-mount",
"@kbn/deeplinks-observability"
"@kbn/deeplinks-observability",
],
"exclude": ["target/**/*"]
}

View file

@ -8,6 +8,7 @@ import expect from '@kbn/expect';
import { omit } from 'lodash';
import { HTTPFields } from '@kbn/synthetics-plugin/common/runtime_types';
import { SYNTHETICS_API_URLS } from '@kbn/synthetics-plugin/common/constants';
import { DYNAMIC_SETTINGS_DEFAULTS } from '@kbn/synthetics-plugin/common/constants/settings_defaults';
import { FtrProviderContext } from '../../ftr_provider_context';
import { getFixtureJson } from './helper/get_fixture_json';
@ -41,13 +42,19 @@ export default function ({ getService }: FtrProviderContext) {
beforeEach(async () => {
httpMonitorJson = _httpMonitorJson;
await kibanaServer.savedObjects.cleanStandardList();
await supertest
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
.set('kbn-xsrf', 'true')
.send(DYNAMIC_SETTINGS_DEFAULTS)
.expect(200);
});
it('returns the created alerted when called', async () => {
const apiResponse = await supertest
.post(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.send({});
.send()
.expect(200);
const omitFields = [
'id',
@ -76,6 +83,96 @@ export default function ({ getService }: FtrProviderContext) {
expect(apiResponse).eql(omitMonitorKeys(newMonitor));
await retry.tryForTime(30 * 1000, async () => {
const res = await supertest
.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.expect(200);
expect(res.body.statusRule.ruleTypeId).eql('xpack.synthetics.alerts.monitorStatus');
expect(res.body.tlsRule.ruleTypeId).eql('xpack.synthetics.alerts.tls');
});
});
it('deletes (and recreates) the default rule when settings are updated', async () => {
const newMonitor = httpMonitorJson;
const { body: apiResponse } = await addMonitorAPI(newMonitor);
expect(apiResponse).eql(omitMonitorKeys(newMonitor));
await retry.tryForTime(30 * 1000, async () => {
const res = await supertest
.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.expect(200);
expect(res.body.statusRule.ruleTypeId).eql('xpack.synthetics.alerts.monitorStatus');
expect(res.body.tlsRule.ruleTypeId).eql('xpack.synthetics.alerts.tls');
});
const settings = await supertest
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
.set('kbn-xsrf', 'true')
.send({
defaultStatusRuleEnabled: false,
defaultTLSRuleEnabled: false,
});
expect(settings.body.defaultStatusRuleEnabled).eql(false);
expect(settings.body.defaultTLSRuleEnabled).eql(false);
await supertest
.put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
await retry.tryForTime(30 * 1000, async () => {
const res = await supertest
.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.expect(200);
expect(res.body.statusRule).eql(null);
expect(res.body.tlsRule).eql(null);
});
const settings2 = await supertest
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
.set('kbn-xsrf', 'true')
.send({
defaultStatusRuleEnabled: true,
defaultTLSRuleEnabled: true,
})
.expect(200);
expect(settings2.body.defaultStatusRuleEnabled).eql(true);
expect(settings2.body.defaultTLSRuleEnabled).eql(true);
await supertest
.put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
await retry.tryForTime(30 * 1000, async () => {
const res = await supertest
.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.expect(200);
expect(res.body.statusRule.ruleTypeId).eql('xpack.synthetics.alerts.monitorStatus');
expect(res.body.tlsRule.ruleTypeId).eql('xpack.synthetics.alerts.tls');
});
});
it('doesnt throw errors when rule has already been deleted', async () => {
const newMonitor = httpMonitorJson;
const { body: apiResponse } = await addMonitorAPI(newMonitor);
expect(apiResponse).eql(omitMonitorKeys(newMonitor));
await retry.tryForTime(30 * 1000, async () => {
const res = await supertest
.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
@ -84,6 +181,49 @@ export default function ({ getService }: FtrProviderContext) {
expect(res.body.statusRule.ruleTypeId).eql('xpack.synthetics.alerts.monitorStatus');
expect(res.body.tlsRule.ruleTypeId).eql('xpack.synthetics.alerts.tls');
});
const settings = await supertest
.put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS)
.set('kbn-xsrf', 'true')
.send({
defaultStatusRuleEnabled: false,
defaultTLSRuleEnabled: false,
});
expect(settings.body.defaultStatusRuleEnabled).eql(false);
expect(settings.body.defaultTLSRuleEnabled).eql(false);
await supertest
.put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.send();
await retry.tryForTime(30 * 1000, async () => {
const res = await supertest
.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.expect(200);
expect(res.body.statusRule).eql(null);
expect(res.body.tlsRule).eql(null);
});
// call api again with the same settings, make sure its 200
await supertest
.put(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.send()
.expect(200);
await retry.tryForTime(30 * 1000, async () => {
const res = await supertest
.get(SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING)
.set('kbn-xsrf', 'true')
.expect(200);
expect(res.body.statusRule).eql(null);
expect(res.body.tlsRule).eql(null);
});
});
});
}