Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
246ef0b
Fix wiring
kdelemme Feb 4, 2026
1ee1620
Fix bulk query after schema change
kdelemme Feb 4, 2026
770fafe
Fix bulk query after schema change
kdelemme Feb 4, 2026
3069894
wip
kdelemme Feb 6, 2026
9ce04f7
add agent
kdelemme Feb 6, 2026
d8cf2bb
Suppress alert
kdelemme Feb 6, 2026
a154115
Add apm span
kdelemme Feb 6, 2026
2216998
use partition
kdelemme Feb 6, 2026
fd6c77b
debug log
kdelemme Feb 6, 2026
72e7c75
Update agent docs
kdelemme Feb 6, 2026
933adba
Update tests
kdelemme Feb 6, 2026
01c86fa
Add comprehensive test scenario
kdelemme Feb 6, 2026
b3ecb51
Remove llm docs
kdelemme Feb 9, 2026
5f7099c
Fix dispatcher
kdelemme Feb 9, 2026
7d41c1d
Add integration tests
kdelemme Feb 9, 2026
48b971b
Merge branch 'alerting_v2' into alertingv2/dispatcher-suppression
kdelemme Feb 9, 2026
2815d8e
Merge branch 'alerting_v2' into alertingv2/dispatcher-suppression
kdelemme Feb 10, 2026
9eba0ea
Improve suppression algo
kdelemme Feb 10, 2026
ec2ae3d
Merge branch 'alerting_v2' into alertingv2/dispatcher-suppression
kdelemme Feb 10, 2026
fd4fbd0
remove import
kdelemme Feb 10, 2026
b3570f3
provide actionType
kdelemme Feb 10, 2026
7878155
Update mocks
kdelemme Feb 10, 2026
bbc6fc0
POC notificationm policy
kdelemme Feb 10, 2026
0b24ee5
poc
kdelemme Feb 11, 2026
3187391
make matcher optional
kdelemme Feb 11, 2026
3a276be
Add reason of suppression
kdelemme Feb 11, 2026
e04bc72
Add reason for notified group action
kdelemme Feb 11, 2026
93c64bb
Fix throttling
kdelemme Feb 11, 2026
bf95941
Return early
kdelemme Feb 11, 2026
7e56d93
Remove log
kdelemme Feb 11, 2026
07e674b
trigger workflow (break task tho)
kdelemme Feb 11, 2026
b83c1c1
Merge branch 'alerting_v2' into alertingv2/poc-dispatcher-notificatio…
kdelemme Feb 16, 2026
35687bd
Merge branch 'alerting_v2' into alertingv2/poc-dispatcher-notificatio…
kdelemme Feb 19, 2026
8b70bc1
Fix merge conflicts
kdelemme Feb 19, 2026
51a3758
Store api key in notification policy
kdelemme Feb 19, 2026
dc0f87d
Use a new internal rules so client for fetching rules by ids from dis…
kdelemme Feb 19, 2026
101a90d
Introduce notification policy internal so for usage in the dispatcher
kdelemme Feb 20, 2026
6cf679a
Refactor dispatcher with step pipeline
kdelemme Feb 20, 2026
e7265e1
Add missing test
kdelemme Feb 20, 2026
431e88c
fix integration test
kdelemme Feb 20, 2026
5cb4afc
Changes from node scripts/lint_ts_projects --fix
kibanamachine Feb 20, 2026
603bfe2
Merge branch 'alerting_v2' into alertingv2/poc-dispatcher-notificatio…
kdelemme Feb 20, 2026
d2b811a
Merge branch 'alerting_v2' into alertingv2/poc-dispatcher-notificatio…
kdelemme Feb 20, 2026
98ae1c4
evaluate kql in matcher step
kdelemme Feb 20, 2026
5f1e0fe
use debug logger
kdelemme Feb 20, 2026
af4f64f
Rename active to dispatchable
kdelemme Feb 20, 2026
4c52825
Add tests for record actions step
kdelemme Feb 20, 2026
b0530d2
Remove inject from dispatcher service
kdelemme Feb 20, 2026
59b20b8
Fix dispatcher step tests
kdelemme Feb 20, 2026
b3cfd89
remove comment
kdelemme Feb 20, 2026
f1c1af1
remove as unknown type cast
kdelemme Feb 20, 2026
34b2e23
Remove workflow and apikey in notificaiton policy for now
kdelemme Feb 23, 2026
f4eb7ba
Merge branch 'alerting_v2' into alertingv2/poc-dispatcher-notificatio…
kdelemme Feb 23, 2026
f6e255a
Changes from node scripts/lint_ts_projects --fix
kibanamachine Feb 23, 2026
a8f4287
Update integration tests
kdelemme Feb 23, 2026
01369e5
Merge branch 'alerting_v2' into alertingv2/poc-dispatcher-notificatio…
kdelemme Feb 26, 2026
43d8446
Move to injectable()
kdelemme Feb 26, 2026
d951a95
reuse test_util
kdelemme Feb 26, 2026
aae8266
reuse mocks
kdelemme Feb 26, 2026
dd22e98
move tokens into their service folder
kdelemme Feb 26, 2026
5702779
use correct token names
kdelemme Feb 26, 2026
92e1a9d
Remove raw function
kdelemme Feb 26, 2026
f63d6f8
Address comments
kdelemme Feb 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions x-pack/platform/plugins/shared/alerting_v2/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { bindOnStart } from './setup/bind_on_start';
import { bindRoutes } from './setup/bind_routes';
import { bindServices } from './setup/bind_services';
import { bindRuleExecutionServices } from './setup/bind_rule_executor';
import { bindDispatcherExecutionServices } from './setup/bind_dispatcher_executor';
import { bindTasks } from './setup/bind_tasks';

export const config: PluginConfigDescriptor<PluginConfig> = {
Expand All @@ -31,6 +32,7 @@ export const module = new ContainerModule((options) => {
bindRoutes(options);
bindServices(options);
bindRuleExecutionServices(options);
bindDispatcherExecutionServices(options);
bindTasks(options);
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,134 @@

import type { BulkResponse } from '@elastic/elasticsearch/lib/api/types';
import type { DeeplyMockedApi } from '@kbn/core-elasticsearch-client-server-mocks';
import type { ElasticsearchClient } from '@kbn/core/server';
import type { ElasticsearchClient, SavedObjectsClientContract } from '@kbn/core/server';
import moment from 'moment';
import { ALERT_ACTIONS_DATA_STREAM, type AlertAction } from '../../resources/alert_actions';
import { RULE_SAVED_OBJECT_TYPE, NOTIFICATION_POLICY_SAVED_OBJECT_TYPE } from '../../saved_objects';
import type { RuleSavedObjectAttributes } from '../../saved_objects';
import { createRuleSoAttributes } from '../test_utils';
import { createLoggerService } from '../services/logger_service/logger_service.mock';
import type { NotificationPolicySavedObjectServiceContract } from '../services/notification_policy_saved_object_service/notification_policy_saved_object_service';
import { createNotificationPolicySavedObjectService } from '../services/notification_policy_saved_object_service/notification_policy_saved_object_service.mock';
import type { QueryServiceContract } from '../services/query_service/query_service';
import { createQueryService } from '../services/query_service/query_service.mock';
import type { RulesSavedObjectServiceContract } from '../services/rules_saved_object_service/rules_saved_object_service';
import { createRulesSavedObjectService } from '../services/rules_saved_object_service/rules_saved_object_service.mock';
import type { StorageServiceContract } from '../services/storage_service/storage_service';
import { createStorageService } from '../services/storage_service/storage_service.mock';
import { LOOKBACK_WINDOW_MINUTES } from './constants';
import { DispatcherService } from './dispatcher';
import { DispatcherPipeline } from './execution_pipeline';
import {
createAlertEpisodeSuppressionsResponse,
createDispatchableAlertEventsResponse,
createLastNotifiedTimestampsResponse,
} from './fixtures/dispatcher';
import { getDispatchableAlertEventsQuery } from './queries';
import {
FetchEpisodesStep,
FetchSuppressionsStep,
ApplySuppressionStep,
FetchRulesStep,
FetchPoliciesStep,
EvaluateMatchersStep,
BuildGroupsStep,
ApplyThrottlingStep,
DispatchStep,
StoreActionsStep,
} from './steps';
import type { AlertEpisode, AlertEpisodeSuppression } from './types';

function mockRulesBulkGet(
mockSoClient: jest.Mocked<SavedObjectsClientContract>,
ruleIds: string[],
overrides?: Partial<RuleSavedObjectAttributes>
) {
mockSoClient.bulkGet.mockResolvedValue({
saved_objects: ruleIds.map((id) => ({
id,
type: RULE_SAVED_OBJECT_TYPE,
attributes: createRuleSoAttributes({
notification_policies: [{ ref: 'policy_456' }],
...overrides,
}),
references: [],
})),
});
}

function mockNpBulkGet(mockSoClient: jest.Mocked<SavedObjectsClientContract>, policyIds: string[]) {
mockSoClient.bulkGet.mockResolvedValue({
saved_objects: policyIds.map((id) => ({
id,
type: NOTIFICATION_POLICY_SAVED_OBJECT_TYPE,
attributes: {
name: `Policy ${id}`,
description: `Description for ${id}`,
destinations: [{ type: 'workflow', id: 'workflow-test-id' }],
createdBy: null,
updatedBy: null,
createdAt: '2026-01-01T00:00:00.000Z',
updatedAt: '2026-01-01T00:00:00.000Z',
},
references: [],
})),
});
}

function buildDispatcherService(deps: {
queryService: QueryServiceContract;
storageService: StorageServiceContract;
rulesSoService: RulesSavedObjectServiceContract;
npSoService: NotificationPolicySavedObjectServiceContract;
}): DispatcherService {
const { loggerService } = createLoggerService();
const pipeline = new DispatcherPipeline(loggerService, [
new FetchEpisodesStep(deps.queryService),
new FetchSuppressionsStep(deps.queryService),
new ApplySuppressionStep(),
new FetchRulesStep(deps.rulesSoService),
new FetchPoliciesStep(deps.npSoService),
new EvaluateMatchersStep(),
new BuildGroupsStep(),
new ApplyThrottlingStep(deps.queryService, loggerService),
new DispatchStep(loggerService),
new StoreActionsStep(deps.storageService),
]);
return new DispatcherService(pipeline);
}

describe('DispatcherService', () => {
let dispatcherService: DispatcherService;
let queryService: QueryServiceContract;
let storageService: StorageServiceContract;
let queryEsClient: DeeplyMockedApi<ElasticsearchClient>;
let storageEsClient: jest.Mocked<ElasticsearchClient>;
let rulesSoService: RulesSavedObjectServiceContract;
let npSoService: NotificationPolicySavedObjectServiceContract;
let rulesMockSoClient: jest.Mocked<SavedObjectsClientContract>;
let npMockSoClient: jest.Mocked<SavedObjectsClientContract>;

beforeEach(() => {
({ queryService, mockEsClient: queryEsClient } = createQueryService());
({ storageService, mockEsClient: storageEsClient } = createStorageService());
const { loggerService } = createLoggerService();
dispatcherService = new DispatcherService(queryService, loggerService, storageService);

const rulesMock = createRulesSavedObjectService();
rulesSoService = rulesMock.rulesSavedObjectService;
rulesMockSoClient = rulesMock.mockSavedObjectsClient;
mockRulesBulkGet(rulesMockSoClient, ['rule-1', 'rule-2']);

const npMock = createNotificationPolicySavedObjectService();
npSoService = npMock.notificationPolicySavedObjectService;
npMockSoClient = npMock.mockSavedObjectsClient;
mockNpBulkGet(npMockSoClient, ['policy_456']);

dispatcherService = buildDispatcherService({
queryService,
storageService,
rulesSoService,
npSoService,
});
});

afterEach(() => {
Expand Down Expand Up @@ -78,7 +177,8 @@ describe('DispatcherService', () => {

queryEsClient.esql.query
.mockResolvedValueOnce(createDispatchableAlertEventsResponse(alertEpisodes))
.mockResolvedValueOnce(createAlertEpisodeSuppressionsResponse(suppressions));
.mockResolvedValueOnce(createAlertEpisodeSuppressionsResponse(suppressions))
.mockResolvedValueOnce(createLastNotifiedTimestampsResponse());

storageEsClient.bulk.mockResolvedValue({
items: [{ create: { _id: '1', status: 201 } }, { create: { _id: '2', status: 201 } }],
Expand All @@ -97,7 +197,7 @@ describe('DispatcherService', () => {
.subtract(LOOKBACK_WINDOW_MINUTES, 'minutes')
.toISOString();

expect(queryEsClient.esql.query).toHaveBeenCalledTimes(2);
expect(queryEsClient.esql.query).toHaveBeenCalledTimes(3);
expect(queryEsClient.esql.query).toHaveBeenCalledWith(
{
query: getDispatchableAlertEventsQuery().query,
Expand Down Expand Up @@ -126,7 +226,11 @@ describe('DispatcherService', () => {
expect(createOperations).toEqual(
expect.arrayContaining([{ create: { _index: ALERT_ACTIONS_DATA_STREAM } }])
);
expect(docs).toHaveLength(alertEpisodes.length);

const fireActions = docs.filter((d: any) => d.action_type === 'fire');
const notifiedActions = docs.filter((d: any) => d.action_type === 'notified');
expect(fireActions).toHaveLength(alertEpisodes.length);
expect(notifiedActions.length).toBeGreaterThan(0);

expect(docs).toEqual(
expect.arrayContaining([
Expand Down Expand Up @@ -185,7 +289,8 @@ describe('DispatcherService', () => {

queryEsClient.esql.query
.mockResolvedValueOnce(createDispatchableAlertEventsResponse(alertEpisodes))
.mockResolvedValueOnce(createAlertEpisodeSuppressionsResponse(suppressions));
.mockResolvedValueOnce(createAlertEpisodeSuppressionsResponse(suppressions))
.mockResolvedValueOnce(createLastNotifiedTimestampsResponse());

storageEsClient.bulk.mockResolvedValue({
items: [{ create: { _id: '1', status: 201 } }, { create: { _id: '2', status: 201 } }],
Expand All @@ -201,7 +306,11 @@ describe('DispatcherService', () => {
const [{ operations }] = storageEsClient.bulk.mock.calls[0];
const safeOperations = operations ?? [];
const docs = safeOperations.filter((_, index) => index % 2 === 1);
expect(docs).toHaveLength(2);

const suppressDocs = docs.filter((d: any) => d.action_type === 'suppress');
const fireDocs = docs.filter((d: any) => d.action_type === 'fire');
expect(suppressDocs).toHaveLength(1);
expect(fireDocs).toHaveLength(1);

expect(docs).toEqual(
expect.arrayContaining([
Expand Down Expand Up @@ -238,6 +347,29 @@ describe('DispatcherService', () => {
});

it('dispatches correct fire/suppress actions across 5 rules with ack, unack, snooze, and deactivate suppressions', async () => {
const rulesMock = createRulesSavedObjectService();
rulesSoService = rulesMock.rulesSavedObjectService;
rulesMockSoClient = rulesMock.mockSavedObjectsClient;
mockRulesBulkGet(rulesMockSoClient, [
'rule-001',
'rule-002',
'rule-003',
'rule-004',
'rule-005',
]);

const npMock = createNotificationPolicySavedObjectService();
npSoService = npMock.notificationPolicySavedObjectService;
npMockSoClient = npMock.mockSavedObjectsClient;
mockNpBulkGet(npMockSoClient, ['policy_456']);

dispatcherService = buildDispatcherService({
queryService,
storageService,
rulesSoService,
npSoService,
});

// Dataset: 5 rules, 9 episodes total
// rule-001: single series, ack then unack → fire
// rule-002: single series, ack with no unack → suppress
Expand Down Expand Up @@ -359,7 +491,8 @@ describe('DispatcherService', () => {

queryEsClient.esql.query
.mockResolvedValueOnce(createDispatchableAlertEventsResponse(alertEpisodes))
.mockResolvedValueOnce(createAlertEpisodeSuppressionsResponse(suppressions));
.mockResolvedValueOnce(createAlertEpisodeSuppressionsResponse(suppressions))
.mockResolvedValueOnce(createLastNotifiedTimestampsResponse());

storageEsClient.bulk.mockResolvedValue({
items: Array.from({ length: 10 }, (_, i) => ({
Expand All @@ -373,17 +506,18 @@ describe('DispatcherService', () => {
});

expect(result.startedAt).toBeInstanceOf(Date);
expect(queryEsClient.esql.query).toHaveBeenCalledTimes(2);
expect(queryEsClient.esql.query).toHaveBeenCalledTimes(3);

const [{ operations }] = storageEsClient.bulk.mock.calls[0];

const docs = (operations ?? []).filter((_, index) => index % 2 === 1) as AlertAction[];
expect(docs).toHaveLength(10);

const fireActions = docs.filter((doc) => doc.action_type === 'fire');
const suppressActions = docs.filter((doc) => doc.action_type === 'suppress');
const notifiedActions = docs.filter((doc) => doc.action_type === 'notified');
expect(fireActions).toHaveLength(6);
expect(suppressActions).toHaveLength(4);
expect(notifiedActions.length).toBeGreaterThan(0);

// rule-001: fire (ack then unack cancels suppression)
expect(docs).toEqual(
Expand Down
Loading