Skip to content

Commit f916c50

Browse files
committed
Review APM, move preValidate to bulk_create_rules for visibility in key steps.
1 parent 1fca692 commit f916c50

2 files changed

Lines changed: 150 additions & 145 deletions

File tree

x-pack/platform/plugins/shared/alerting/server/application/rule/methods/bulk_create/bulk_create_rules.ts

Lines changed: 150 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import { withSpan } from '@kbn/apm-utils';
1111
import type { SavedObjectsBulkCreateObject } from '@kbn/core/server';
1212
import { SavedObjectsUtils } from '@kbn/core/server';
1313
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
14-
import { getRuleCircuitBreakerErrorMessage } from '../../../../../common';
15-
import { updateMeta } from '../../../../rules_client/lib';
14+
import { getRuleCircuitBreakerErrorMessage, parseDuration } from '../../../../../common';
15+
import { addGeneratedActionValues, updateMeta } from '../../../../rules_client/lib';
16+
import { validateRuleTypeParams } from '../../../../lib';
17+
import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization';
1618
import {
1719
API_KEY_GENERATE_CONCURRENCY,
1820
DEFAULT_BULK_CREATE_BATCH_SIZE,
@@ -24,6 +26,7 @@ import { bulkCreateRulesSo } from '../../../../data/rule';
2426
import type { RawRule } from '../../../../types';
2527
import type { RulesClientContext } from '../../../../rules_client/types';
2628
import type { RuleParams } from '../../types';
29+
import { createRuleDataSchema } from '../create/schemas';
2730
import { validateScheduleLimit } from '../get_schedule_frequency';
2831
import type {
2932
ApiKeyEntry,
@@ -40,7 +43,6 @@ import {
4043
demotePreparedRules,
4144
flushKeysToInvalidate,
4245
prepareRule,
43-
preValidate,
4446
} from './utils';
4547

4648
export async function bulkCreateRules<Params extends RuleParams = never>(
@@ -131,6 +133,123 @@ export async function bulkCreateRules<Params extends RuleParams = never>(
131133
return { successfulIds, errors, total };
132134
}
133135

136+
async function preValidate<Params extends RuleParams>({
137+
context,
138+
inputs,
139+
}: {
140+
context: RulesClientContext;
141+
inputs: Array<{ id: string; rule: BulkCreateRulesItem<Params> }>;
142+
}): Promise<{
143+
validated: Array<{ id: string; rule: BulkCreateRulesItem<Params> }>;
144+
errors: BulkCreateOperationError[];
145+
}> {
146+
const validated = new Map<string, { id: string; rule: BulkCreateRulesItem<Params> }>();
147+
const errors: BulkCreateOperationError[] = [];
148+
const authPairs = new Map<
149+
string,
150+
{ ruleTypeId: string; consumer: string; ids: string[]; names: Map<string, string> }
151+
>();
152+
153+
// Phase A1: per-rule in-memory checks, sequential, cheapest-first.
154+
await withSpan({ name: 'preValidate.checkInMemory', type: 'rules' }, async () => {
155+
for (const { id, rule } of inputs) {
156+
try {
157+
const { actions: genActions, systemActions: genSystemActions } =
158+
await addGeneratedActionValues(rule.data.actions, rule.data.systemActions, context);
159+
const data = { ...rule.data, actions: genActions, systemActions: genSystemActions };
160+
161+
try {
162+
createRuleDataSchema.validate(data);
163+
} catch (err) {
164+
throw Boom.badRequest(`Error validating create data - ${err.message}`);
165+
}
166+
167+
// ruleTypeRegistry.get throws 400 if not registered.
168+
const ruleType = context.ruleTypeRegistry.get(data.alertTypeId);
169+
context.ruleTypeRegistry.ensureRuleTypeEnabled(data.alertTypeId);
170+
validateRuleTypeParams(data.params, ruleType.validate.params);
171+
172+
const intervalInMs = parseDuration(data.schedule.interval);
173+
if (
174+
intervalInMs < context.minimumScheduleIntervalInMs &&
175+
context.minimumScheduleInterval.enforce
176+
) {
177+
throw Boom.badRequest(
178+
`Error creating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}`
179+
);
180+
}
181+
if (
182+
intervalInMs < context.minimumScheduleIntervalInMs &&
183+
!context.minimumScheduleInterval.enforce
184+
) {
185+
context.logger.warn(
186+
`Rule schedule interval (${data.schedule.interval}) for "${ruleType.id}" rule type with ID "${id}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent creation of these rules.`
187+
);
188+
}
189+
190+
const authzKey = `${data.alertTypeId}::${data.consumer}`;
191+
const pair = authPairs.get(authzKey);
192+
if (pair) {
193+
pair.ids.push(id);
194+
pair.names.set(id, data.name);
195+
} else {
196+
authPairs.set(authzKey, {
197+
ruleTypeId: data.alertTypeId,
198+
consumer: data.consumer,
199+
ids: [id],
200+
names: new Map([[id, data.name]]),
201+
});
202+
}
203+
204+
validated.set(id, { id, rule });
205+
} catch (err) {
206+
errors.push({
207+
message: err.message,
208+
status: err.output?.statusCode,
209+
rule: { id, name: rule.data?.name ?? 'n/a' },
210+
});
211+
}
212+
}
213+
});
214+
215+
if (validated.size === 0) {
216+
return { validated: [], errors };
217+
}
218+
219+
// Phase A2: deduped per-pair authorization.
220+
await withSpan({ name: 'preValidate.ensureAuthorized', type: 'rules' }, async () => {
221+
for (const { ruleTypeId, consumer, ids, names } of authPairs.values()) {
222+
try {
223+
await context.authorization.ensureAuthorized({
224+
ruleTypeId,
225+
consumer,
226+
operation: WriteOperations.Create,
227+
entity: AlertingAuthorizationEntity.Rule,
228+
});
229+
} catch (authzError) {
230+
// One audit per rule in failing set. Following single rule `create()`.
231+
for (const ruleId of ids) {
232+
context.auditLogger?.log(
233+
ruleAuditEvent({
234+
action: RuleAuditAction.CREATE,
235+
savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: ruleId, name: names.get(ruleId)! },
236+
error: authzError,
237+
})
238+
);
239+
errors.push({
240+
message: authzError.message,
241+
status: authzError.output?.statusCode,
242+
rule: { id: ruleId, name: names.get(ruleId) ?? 'n/a' },
243+
});
244+
validated.delete(ruleId);
245+
}
246+
}
247+
}
248+
});
249+
250+
return { validated: [...validated.values()], errors };
251+
}
252+
134253
interface RunBatchArgs<Params extends RuleParams> {
135254
context: RulesClientContext;
136255
username: string | null;
@@ -154,22 +273,24 @@ async function runBatch<Params extends RuleParams>({
154273
const errors: BulkCreateOperationError[] = [];
155274

156275
// Phase B1: per-rule prepare (high latency validation + API key generation).
157-
await pMap(
158-
batch,
159-
async ({ id, rule }) => {
160-
const { prepared, error } = await prepareRule({
161-
context,
162-
actionsClient,
163-
username,
164-
id,
165-
rule,
166-
errors,
167-
apiKeysMap,
168-
});
169-
if (prepared) preparedRules.set(id, prepared);
170-
else if (error) errors.push(error);
171-
},
172-
{ concurrency: API_KEY_GENERATE_CONCURRENCY }
276+
await withSpan({ name: 'runBatch.pMap.prepareRule', type: 'rules' }, () =>
277+
pMap(
278+
batch,
279+
async ({ id, rule }) => {
280+
const { prepared, error } = await prepareRule({
281+
context,
282+
actionsClient,
283+
username,
284+
id,
285+
rule,
286+
errors,
287+
apiKeysMap,
288+
});
289+
if (prepared) preparedRules.set(id, prepared);
290+
else if (error) errors.push(error);
291+
},
292+
{ concurrency: API_KEY_GENERATE_CONCURRENCY }
293+
)
173294
);
174295

175296
// No survivors? Flush any keys created and return.
@@ -182,10 +303,14 @@ async function runBatch<Params extends RuleParams>({
182303
const enabled = [...preparedRules.values()].filter((p) => p.enabled);
183304

184305
if (enabled.length > 0) {
185-
const validationPayload = await validateScheduleLimit({
186-
context,
187-
updatedInterval: enabled.map((r) => r.schedule.interval),
188-
});
306+
const validationPayload = await withSpan(
307+
{ name: 'runBatch.validateScheduleLimit', type: 'rules' },
308+
() =>
309+
validateScheduleLimit({
310+
context,
311+
updatedInterval: enabled.map((r) => r.schedule.interval),
312+
})
313+
);
189314
if (validationPayload) {
190315
const enabledIds = enabled.map((p) => p.id);
191316
const reasonMessage = getRuleCircuitBreakerErrorMessage({
@@ -221,7 +346,7 @@ async function runBatch<Params extends RuleParams>({
221346
const survivingEnabledIds = survivingEnabled.map((p) => p.id);
222347
try {
223348
const scheduledTasks = await withSpan(
224-
{ name: 'taskManager.bulkSchedule', type: 'tasks' },
349+
{ name: 'runBatch.bulkSchedule', type: 'tasks' },
225350
() => context.taskManager.bulkSchedule(tasksToSchedule)
226351
);
227352
scheduledIds = scheduledTasks.map((task) => task.id);
@@ -291,7 +416,7 @@ async function runBatch<Params extends RuleParams>({
291416
let bulkResponse;
292417
try {
293418
bulkResponse = await withSpan(
294-
{ name: 'unsecuredSavedObjectsClient.bulkCreate', type: 'rules' },
419+
{ name: 'runBatch.bulkCreateRulesSo', type: 'rules' },
295420
() =>
296421
bulkCreateRulesSo({
297422
savedObjectsClient: context.unsecuredSavedObjectsClient,

x-pack/platform/plugins/shared/alerting/server/application/rule/methods/bulk_create/utils.ts

Lines changed: 0 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@
55
* 2.0.
66
*/
77

8-
import Boom from '@hapi/boom';
98
import Semver from 'semver';
109
import { i18n } from '@kbn/i18n';
1110
import type { TaskInstanceWithDeprecatedFields } from '@kbn/task-manager-plugin/server/task';
1211

1312
import { validateAndAuthorizeSystemActions } from '../../../../lib/validate_authorize_system_actions';
14-
import { RULE_SAVED_OBJECT_TYPE } from '../../../../saved_objects';
15-
import { parseDuration } from '../../../../../common';
16-
import { WriteOperations, AlertingAuthorizationEntity } from '../../../../authorization';
1713
import {
1814
validateRuleTypeParams,
1915
getRuleNotifyWhenType,
@@ -31,19 +27,16 @@ import {
3127
apiKeyAsAlertAttributes,
3228
apiKeyAsRuleDomainProperties,
3329
} from '../../../../rules_client/common';
34-
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
3530
import { bulkMarkApiKeysForInvalidation } from '../../../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
3631
import type { BulkOperationError, RulesClientContext } from '../../../../rules_client/types';
3732
import type { RuleDomain, RuleParams } from '../../types';
3833
import { transformRuleDomainToRuleAttributes } from '../../transforms';
39-
import { createRuleDataSchema } from '../create/schemas';
4034
import type {
4135
PreparedRule,
4236
PrepareRuleArgs,
4337
ApiKeyEntry,
4438
BulkCreateOperationError,
4539
BulkCreateDisabledReason,
46-
BulkCreateRulesItem,
4740
} from './types';
4841

4942
export const getBulkCreateAsDisabledMessage = (message: string): string =>
@@ -84,119 +77,6 @@ export const buildTaskInstance = (
8477
enabled: true,
8578
});
8679

87-
export const preValidate = async <Params extends RuleParams>({
88-
context,
89-
inputs,
90-
}: {
91-
context: RulesClientContext;
92-
inputs: Array<{ id: string; rule: BulkCreateRulesItem<Params> }>;
93-
}): Promise<{
94-
validated: Array<{ id: string; rule: BulkCreateRulesItem<Params> }>;
95-
errors: BulkCreateOperationError[];
96-
}> => {
97-
const validated = new Map<string, { id: string; rule: BulkCreateRulesItem<Params> }>();
98-
const errors: BulkCreateOperationError[] = [];
99-
const authPairs = new Map<
100-
string,
101-
{ ruleTypeId: string; consumer: string; ids: string[]; names: Map<string, string> }
102-
>();
103-
104-
// Phase A1: per-rule in-memory checks, sequential, cheapest-first.
105-
for (const { id, rule } of inputs) {
106-
try {
107-
const { actions: genActions, systemActions: genSystemActions } =
108-
await addGeneratedActionValues(rule.data.actions, rule.data.systemActions, context);
109-
const data = { ...rule.data, actions: genActions, systemActions: genSystemActions };
110-
111-
try {
112-
createRuleDataSchema.validate(data);
113-
} catch (err) {
114-
throw Boom.badRequest(`Error validating create data - ${err.message}`);
115-
}
116-
117-
// ruleTypeRegistry.get throws 400 if not registered.
118-
const ruleType = context.ruleTypeRegistry.get(data.alertTypeId);
119-
context.ruleTypeRegistry.ensureRuleTypeEnabled(data.alertTypeId);
120-
validateRuleTypeParams(data.params, ruleType.validate.params);
121-
122-
const intervalInMs = parseDuration(data.schedule.interval);
123-
if (
124-
intervalInMs < context.minimumScheduleIntervalInMs &&
125-
context.minimumScheduleInterval.enforce
126-
) {
127-
throw Boom.badRequest(
128-
`Error creating rule: the interval is less than the allowed minimum interval of ${context.minimumScheduleInterval.value}`
129-
);
130-
}
131-
if (
132-
intervalInMs < context.minimumScheduleIntervalInMs &&
133-
!context.minimumScheduleInterval.enforce
134-
) {
135-
context.logger.warn(
136-
`Rule schedule interval (${data.schedule.interval}) for "${ruleType.id}" rule type with ID "${id}" is less than the minimum value (${context.minimumScheduleInterval.value}). Running rules at this interval may impact alerting performance. Set "xpack.alerting.rules.minimumScheduleInterval.enforce" to true to prevent creation of these rules.`
137-
);
138-
}
139-
140-
const authzKey = `${data.alertTypeId}::${data.consumer}`;
141-
const pair = authPairs.get(authzKey);
142-
if (pair) {
143-
pair.ids.push(id);
144-
pair.names.set(id, data.name);
145-
} else {
146-
authPairs.set(authzKey, {
147-
ruleTypeId: data.alertTypeId,
148-
consumer: data.consumer,
149-
ids: [id],
150-
names: new Map([[id, data.name]]),
151-
});
152-
}
153-
154-
validated.set(id, { id, rule });
155-
} catch (err) {
156-
errors.push({
157-
message: err.message,
158-
status: err.output?.statusCode,
159-
rule: { id, name: rule.data?.name ?? 'n/a' },
160-
});
161-
}
162-
}
163-
164-
if (validated.size === 0) {
165-
return { validated: [], errors };
166-
}
167-
168-
// Phase A2: deduped per-pair authorization.
169-
for (const { ruleTypeId, consumer, ids, names } of authPairs.values()) {
170-
try {
171-
await context.authorization.ensureAuthorized({
172-
ruleTypeId,
173-
consumer,
174-
operation: WriteOperations.Create,
175-
entity: AlertingAuthorizationEntity.Rule,
176-
});
177-
} catch (authzError) {
178-
// One audit per rule in failing set. Following single rule `create()`.
179-
for (const ruleId of ids) {
180-
context.auditLogger?.log(
181-
ruleAuditEvent({
182-
action: RuleAuditAction.CREATE,
183-
savedObject: { type: RULE_SAVED_OBJECT_TYPE, id: ruleId, name: names.get(ruleId)! },
184-
error: authzError,
185-
})
186-
);
187-
errors.push({
188-
message: authzError.message,
189-
status: authzError.output?.statusCode,
190-
rule: { id: ruleId, name: names.get(ruleId) ?? 'n/a' },
191-
});
192-
validated.delete(ruleId);
193-
}
194-
}
195-
}
196-
197-
return { validated: [...validated.values()], errors };
198-
};
199-
20080
export const prepareRule = async <Params extends RuleParams>({
20181
context,
20282
actionsClient,

0 commit comments

Comments
 (0)