From cce5081720e55b773fe89c0c9ff563162f0608ca Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 3 Oct 2025 11:46:57 -0400 Subject: [PATCH 1/2] Do not create default alerts if connectors are not defined. --- .../default_alert_service.test.ts | 46 +++++++++---------- .../default_alerts/default_alert_service.ts | 18 ++++++-- .../default_alerts/enable_default_alert.ts | 6 +-- .../default_alerts/get_default_alert.ts | 4 +- .../default_alerts/update_default_alert.ts | 4 +- .../add_monitor/add_monitor_api.ts | 6 +-- 6 files changed, 46 insertions(+), 38 deletions(-) diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.test.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.test.ts index 07fa214479d6d..3a52ebaf72d3f 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.test.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.test.ts @@ -11,7 +11,7 @@ import { SYNTHETICS_STATUS_RULE, SYNTHETICS_TLS_RULE, } from '../../../common/constants/synthetics_alerts'; -import { DefaultAlertService } from './default_alert_service'; +import { DefaultRuleService } from './default_alert_service'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../constants/settings'; describe('DefaultAlertService', () => { @@ -24,7 +24,7 @@ describe('DefaultAlertService', () => { const soResponse = { attributes: { ...expectedSettings } }; it('returns settings if already set', async () => { const soClient = { get: jest.fn() } as any; - const service = new DefaultAlertService({} as any, {} as any, soClient); + const service = new DefaultRuleService({} as any, {} as any, soClient); service.settings = expectedSettings; const settings = await service.getSettings(); expect(settings).toEqual(expectedSettings); @@ -33,7 +33,7 @@ describe('DefaultAlertService', () => { it('fetches settings if not set', async () => { const soClient = { get: jest.fn() } as any; - const service = new DefaultAlertService({} as any, {} as any, soClient); + const service = new DefaultRuleService({} as any, {} as any, soClient); soClient.get.mockResolvedValueOnce(soResponse); const settings = await service.getSettings(); expect(settings).toEqual({ @@ -51,7 +51,7 @@ describe('DefaultAlertService', () => { it('sets up status and tls rules', async () => { const soClient = { get: jest.fn() } as any; - const service = new DefaultAlertService({} as any, {} as any, soClient); + const service = new DefaultRuleService({} as any, {} as any, soClient); service.getSettings = jest.fn().mockResolvedValue({ certAgeThreshold: 50, certExpirationThreshold: 10, @@ -66,7 +66,7 @@ describe('DefaultAlertService', () => { service.setupTlsRule = setupTlsRule; setupStatusRule.mockResolvedValueOnce({ status: 'fulfilled', value: {} }); setupTlsRule.mockResolvedValueOnce({ status: 'fulfilled', value: {} }); - const result = await service.setupDefaultAlerts(); + const result = await service.setupDefaultRules(); expect(setupStatusRule).toHaveBeenCalledTimes(1); expect(setupTlsRule).toHaveBeenCalledTimes(1); expect(result).toEqual({ @@ -76,7 +76,7 @@ describe('DefaultAlertService', () => { }); it('returns null rules if value is falsy', async () => { const soClient = { get: jest.fn() } as any; - const service = new DefaultAlertService({} as any, {} as any, soClient); + const service = new DefaultRuleService({} as any, {} as any, soClient); service.getSettings = jest.fn().mockResolvedValue({ certAgeThreshold: 50, certExpirationThreshold: 10, @@ -91,7 +91,7 @@ describe('DefaultAlertService', () => { service.setupTlsRule = setupTlsRule; setupStatusRule.mockResolvedValueOnce(undefined); setupTlsRule.mockResolvedValueOnce(undefined); - const result = await service.setupDefaultAlerts(); + const result = await service.setupDefaultRules(); expect(setupStatusRule).toHaveBeenCalledTimes(1); expect(setupTlsRule).toHaveBeenCalledTimes(1); expect(result).toEqual({ @@ -106,7 +106,7 @@ describe('DefaultAlertService', () => { const server = { alerting: { getConfig: () => ({ minimumScheduleInterval: { value: '30s' } }) }, } as any; - const service = new DefaultAlertService({} as any, server, {} as any); + const service = new DefaultRuleService({} as any, server, {} as any); expect(service.getMinimumRuleInterval()).toBe('1m'); }); @@ -114,14 +114,14 @@ describe('DefaultAlertService', () => { const server = { alerting: { getConfig: () => ({ minimumScheduleInterval: { value: '5m' } }) }, } as any; - const service = new DefaultAlertService({} as any, server, {} as any); + const service = new DefaultRuleService({} as any, server, {} as any); expect(service.getMinimumRuleInterval()).toBe('5m'); }); }); describe('setupStatusRule', () => { it('creates status rule if enabled', async () => { - const service = new DefaultAlertService({} as any, {} as any, {} as any); + const service = new DefaultRuleService({} as any, {} as any, {} as any); service.getMinimumRuleInterval = jest.fn().mockReturnValue('1m'); service.createDefaultRuleIfNotExist = jest.fn(); service.settings = { defaultStatusRuleEnabled: true } as any; @@ -137,7 +137,7 @@ describe('DefaultAlertService', () => { }); it('does not create status rule if disabled', async () => { - const service = new DefaultAlertService({} as any, {} as any, {} as any); + const service = new DefaultRuleService({} as any, {} as any, {} as any); service.getMinimumRuleInterval = jest.fn().mockReturnValue('1m'); service.createDefaultRuleIfNotExist = jest.fn(); service.settings = { defaultStatusRuleEnabled: false } as any; @@ -149,7 +149,7 @@ describe('DefaultAlertService', () => { describe('setupTlsRule', () => { it('creates tls rule if enabled', async () => { - const service = new DefaultAlertService({} as any, {} as any, {} as any); + const service = new DefaultRuleService({} as any, {} as any, {} as any); service.getMinimumRuleInterval = jest.fn().mockReturnValue('1m'); service.createDefaultRuleIfNotExist = jest.fn(); service.settings = { defaultTlsRuleEnabled: true } as any; @@ -165,7 +165,7 @@ describe('DefaultAlertService', () => { }); it('does not create tls rule if disabled', async () => { - const service = new DefaultAlertService({} as any, {} as any, {} as any); + const service = new DefaultRuleService({} as any, {} as any, {} as any); service.getMinimumRuleInterval = jest.fn().mockReturnValue('1m'); service.createDefaultRuleIfNotExist = jest.fn(); service.settings = { defaultTLSRuleEnabled: false } as any; @@ -209,7 +209,7 @@ describe('DefaultAlertService', () => { describe('getExistingAlert', () => { it('returns rule if exists', async () => { const { getRulesClient, mockRule } = setUpExistingRules(); - const service = new DefaultAlertService( + const service = new DefaultRuleService( { alerting: { getRulesClient } } as any, {} as any, {} as any @@ -222,7 +222,7 @@ describe('DefaultAlertService', () => { const find = jest.fn().mockResolvedValue({ data: [] }); const getRulesClient = jest.fn(); getRulesClient.mockReturnValue({ find }); - const service = new DefaultAlertService( + const service = new DefaultRuleService( { alerting: { getRulesClient } } as any, {} as any, {} as any @@ -234,7 +234,7 @@ describe('DefaultAlertService', () => { describe('createDefaultAlertIfNotExist', () => { it('returns rule if exists', async () => { const { getRulesClient, mockRule } = setUpExistingRules(); - const service = new DefaultAlertService( + const service = new DefaultRuleService( { alerting: { getRulesClient } } as any, {} as any, {} as any @@ -265,7 +265,7 @@ describe('DefaultAlertService', () => { }); const getRulesClient = jest.fn(); getRulesClient.mockReturnValue({ find, create }); - const service = new DefaultAlertService( + const service = new DefaultRuleService( { actions: { getActionsClient }, alerting: { getRulesClient } } as any, {} as any, {} as any @@ -330,7 +330,7 @@ describe('DefaultAlertService', () => { schedule: { interval: '1m' }, params: { param: 'value' }, }); - const service = new DefaultAlertService(context as any, server as any, {} as any); + const service = new DefaultRuleService(context as any, server as any, {} as any); service.settings = { defaultConnectors: ['slack', 'email'] } as any; const result = await service.updateStatusRule(true); expect(result).toEqual({ @@ -364,7 +364,7 @@ describe('DefaultAlertService', () => { } as any; const bulkDeleteRules = jest.fn(); const { getRulesClient } = setUpExistingRules(undefined, { bulkDeleteRules }); - const service = new DefaultAlertService( + const service = new DefaultRuleService( { alerting: { getRulesClient } } as any, server as any, {} as any @@ -381,7 +381,7 @@ describe('DefaultAlertService', () => { describe('updateTlsRule', () => { it('updates the rule if it is enabled', async () => { const { context, server } = setUpUpdateTest(); - const service = new DefaultAlertService(context as any, server as any, {} as any); + const service = new DefaultRuleService(context as any, server as any, {} as any); service.settings = { defaultConnectors: ['slack', 'email'] } as any; const result = await service.updateTlsRule(true); expect(result).toEqual({ @@ -397,7 +397,7 @@ describe('DefaultAlertService', () => { it('creates the rule if it does not exist', async () => { const { context, server } = setUpUpdateTest(); - const service = new DefaultAlertService(context as any, server as any, {} as any); + const service = new DefaultRuleService(context as any, server as any, {} as any); service.settings = { defaultConnectors: ['slack', 'email'] } as any; const getExistingAlertMock = jest.fn().mockResolvedValue(undefined); service.getExistingAlert = getExistingAlertMock; @@ -423,7 +423,7 @@ describe('DefaultAlertService', () => { } as any; const bulkDeleteRules = jest.fn(); const { getRulesClient } = setUpExistingRules(undefined, { bulkDeleteRules }); - const service = new DefaultAlertService( + const service = new DefaultRuleService( { alerting: { getRulesClient } } as any, server as any, {} as any @@ -445,7 +445,7 @@ describe('DefaultAlertService', () => { getActionsClient.mockReturnValue({ getAll, }); - const service = new DefaultAlertService( + const service = new DefaultRuleService( { actions: { getActionsClient } } as any, {} as any, { get: jest.fn() } as any diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.ts index c9ff8b46cde87..7e39bbe4c96c7 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/default_alert_service.ts @@ -8,6 +8,7 @@ import type { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server'; import { parseDuration } from '@kbn/alerting-plugin/server'; import type { FindActionResult } from '@kbn/actions-plugin/server'; +import { isEmpty } from 'lodash'; import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings'; import type { DynamicSettingsAttributes } from '../../runtime_types/settings'; import { populateAlertActions } from '../../../common/rules/alert_actions'; @@ -22,7 +23,7 @@ import { SYNTHETICS_TLS_RULE, } from '../../../common/constants/synthetics_alerts'; import type { DefaultRuleType } from '../../../common/types/default_alerts'; -export class DefaultAlertService { +export class DefaultRuleService { context: UptimeRequestHandlerContext; soClient: SavedObjectsClientContract; server: SyntheticsServerSetup; @@ -45,8 +46,15 @@ export class DefaultAlertService { return this.settings; } - async setupDefaultAlerts() { + async setupDefaultRules() { this.settings = await this.getSettings(); + if (isEmpty(this.settings?.defaultConnectors)) { + this.server.logger.debug(`Default connectors are not set. Skipping default rule setup.`); + return { + statusRule: null, + tlsRule: null, + }; + } const [statusRule, tlsRule] = await Promise.allSettled([ this.setupStatusRule(), @@ -152,7 +160,7 @@ export class DefaultAlertService { async updateStatusRule(enabled?: boolean) { const minimumRuleInterval = this.getMinimumRuleInterval(); if (enabled) { - return this.upsertDefaultAlert( + return this.upsertDefaultRule( SYNTHETICS_STATUS_RULE, `Synthetics status internal rule`, minimumRuleInterval @@ -168,7 +176,7 @@ export class DefaultAlertService { async updateTlsRule(enabled?: boolean) { const minimumRuleInterval = this.getMinimumRuleInterval(); if (enabled) { - return this.upsertDefaultAlert( + return this.upsertDefaultRule( SYNTHETICS_TLS_RULE, `Synthetics internal TLS rule`, minimumRuleInterval @@ -181,7 +189,7 @@ export class DefaultAlertService { } } - async upsertDefaultAlert(ruleType: DefaultRuleType, name: string, interval: string) { + async upsertDefaultRule(ruleType: DefaultRuleType, name: string, interval: string) { const rulesClient = await (await this.context.alerting)?.getRulesClient(); const alert = await this.getExistingAlert(ruleType); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/enable_default_alert.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/enable_default_alert.ts index 63567c4e82245..9b2a7c90d427b 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/enable_default_alert.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/enable_default_alert.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { DefaultAlertService } from './default_alert_service'; +import { DefaultRuleService } from './default_alert_service'; import type { SyntheticsRestApiRouteFactory } from '../types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import type { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts'; @@ -15,8 +15,8 @@ export const enableDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ( path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING, validate: {}, handler: async ({ context, server, savedObjectsClient }): Promise => { - const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient); + const defaultAlertService = new DefaultRuleService(context, server, savedObjectsClient); - return defaultAlertService.setupDefaultAlerts(); + return defaultAlertService.setupDefaultRules(); }, }); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/get_default_alert.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/get_default_alert.ts index c01096eb04742..718b1fb7c0866 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/get_default_alert.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/get_default_alert.ts @@ -9,7 +9,7 @@ import { SYNTHETICS_STATUS_RULE, SYNTHETICS_TLS_RULE, } from '../../../common/constants/synthetics_alerts'; -import { DefaultAlertService } from './default_alert_service'; +import { DefaultRuleService } from './default_alert_service'; import type { SyntheticsRestApiRouteFactory } from '../types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import type { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts'; @@ -19,7 +19,7 @@ export const getDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ({ path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING, validate: {}, handler: async ({ context, server, savedObjectsClient }): Promise => { - const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient); + const defaultAlertService = new DefaultRuleService(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]); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts index 641bd0f80adae..16d66030e3e1f 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/default_alerts/update_default_alert.ts @@ -6,7 +6,7 @@ */ import { getSyntheticsDynamicSettings } from '../../saved_objects/synthetics_settings'; -import { DefaultAlertService } from './default_alert_service'; +import { DefaultRuleService } from './default_alert_service'; import type { SyntheticsRestApiRouteFactory } from '../types'; import { SYNTHETICS_API_URLS } from '../../../common/constants'; import type { DEFAULT_ALERT_RESPONSE } from '../../../common/types/default_alerts'; @@ -16,7 +16,7 @@ export const updateDefaultAlertingRoute: SyntheticsRestApiRouteFactory = () => ( path: SYNTHETICS_API_URLS.ENABLE_DEFAULT_ALERTING, validate: {}, handler: async ({ context, server, savedObjectsClient }): Promise => { - const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient); + const defaultAlertService = new DefaultRuleService(context, server, savedObjectsClient); const { defaultTLSRuleEnabled, defaultStatusRuleEnabled } = await getSyntheticsDynamicSettings( savedObjectsClient ); diff --git a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts index 360d0f82223b0..7a20f0b7c9086 100644 --- a/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts +++ b/x-pack/solutions/observability/plugins/synthetics/server/routes/monitor_cruds/add_monitor/add_monitor_api.ts @@ -37,7 +37,7 @@ import { DEFAULT_NAMESPACE_STRING, } from '../../../../common/constants/monitor_defaults'; import { triggerTestNow } from '../../synthetics_service/test_now_monitor'; -import { DefaultAlertService } from '../../default_alerts/default_alert_service'; +import { DefaultRuleService } from '../../default_alerts/default_alert_service'; import type { RouteContext } from '../../types'; import { formatTelemetryEvent, sendTelemetryEvents } from '../../telemetry/monitor_upgrade_sender'; import { formatKibanaNamespace } from '../../../../common/formatters'; @@ -235,9 +235,9 @@ export class AddEditMonitorAPI { try { // we do this async, so we don't block the user, error handling will be done on the UI via separate api - const defaultAlertService = new DefaultAlertService(context, server, savedObjectsClient); + const defaultAlertService = new DefaultRuleService(context, server, savedObjectsClient); defaultAlertService - .setupDefaultAlerts() + .setupDefaultRules() .then(() => { server.logger.debug(`Successfully created default alert for monitor: ${name}`); }) From e1012fe00b2d62126da38660f1e6710eda6ef6eb Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Fri, 3 Oct 2025 16:40:24 -0400 Subject: [PATCH 2/2] Fix FTR. --- .../apis/synthetics/enable_default_alerting.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/enable_default_alerting.ts b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/enable_default_alerting.ts index 031fe3bb89e28..dc8fdd8ed7632 100644 --- a/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/enable_default_alerting.ts +++ b/x-pack/solutions/observability/test/api_integration_deployment_agnostic/apis/synthetics/enable_default_alerting.ts @@ -17,12 +17,15 @@ import { getFixtureJson } from './helpers/get_fixture_json'; import { addMonitorAPIHelper, omitMonitorKeys } from './create_monitor'; import { PrivateLocationTestService } from '../../services/synthetics_private_location'; +const TEST_INDEX_CONNECTOR_NAME = 'synthetics-default-alerting-test'; + export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { describe('EnableDefaultAlerting', function () { const supertest = getService('supertestWithoutAuth'); const kibanaServer = getService('kibanaServer'); const retry = getService('retry'); const samlAuth = getService('samlAuth'); + const alerting = getService('alertingApi'); let _httpMonitorJson: HTTPFields; let httpMonitorJson: HTTPFields; @@ -37,12 +40,18 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { after(async () => { await kibanaServer.savedObjects.cleanStandardList(); + await alerting.deleteAllActionConnectors({ roleAuthc: editorUser }); }); before(async () => { await kibanaServer.savedObjects.cleanStandardList(); _httpMonitorJson = getFixtureJson('http_monitor'); editorUser = await samlAuth.createM2mApiKeyWithRoleScope('editor'); + await alerting.createIndexConnector({ + roleAuthc: editorUser, + name: TEST_INDEX_CONNECTOR_NAME, + indexName: 'synthetics-*', + }); }); beforeEach(async () => { @@ -58,7 +67,10 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { .put(SYNTHETICS_API_URLS.DYNAMIC_SETTINGS) .set(editorUser.apiKeyHeader) .set(samlAuth.getInternalRequestHeader()) - .send(DYNAMIC_SETTINGS_DEFAULTS) + .send({ + ...DYNAMIC_SETTINGS_DEFAULTS, + defaultConnectors: [TEST_INDEX_CONNECTOR_NAME], + }) .expect(200); });