Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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: 1 addition & 1 deletion packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pageLoadAssetSize:
aiAssistantManagementSelection: 13590
aiops: 15227
alerting: 22371
alertingVTwo: 361715
alertingVTwo: 362170
apm: 38573
apmSourcesAccess: 2278
automaticImport: 12162
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"ad_hoc_run_params": "9c372f2a8f8b468e9b699a6df633c7f14fab7f13216c9ec160813e75bae56098",
"alert": "119624b6025ea6794d2c33e2b41c2e4730d10446430b285691f7638ee6787af5",
"alerting_notification_policy": "81fc65ed6652cd1196f83b4222f227e8c7a3f646a6e044f63a2d82f12dacbfb0",
"alerting_rule": "9e26ddcbca3edb3803dcfe3d6bf908341441fb32c67005cab71ae0f7b9841387",
"alerting_rule": "8f80d561d2b3caf07925be4a5fff52d8433ca3f3f204723f6f3e9e32dbce7f42",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an interesting one, you've changed a saved object definition (the validation schema) and so the hash changed.

Normally this would require a new model version for your type but in the case of going required -> conditional field I wonder if that's needed... Fairly sure it won't break anything, but I'll leave it up to you.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah wait, this is a feature branch. Please proceed 😄

"alerting_rule_template": "a26521005d8a51af336ec95a2097c4bd073980c050e3c675cec3851acff78fd9",
"api_key_pending_invalidation": "b5a0fe007bff147bbb0ef7d0393c976f777ccb470359090d79890a769baf3c68",
"api_key_to_invalidate": "5add5ee737ccc61cc16bbf68423d634d1354971f20926b5ff465a2a853d1723a",
Expand Down Expand Up @@ -303,7 +303,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"alerting_rule|global: e78adb1490c02adb4c705491c87e08332c0f668e",
"alerting_rule|mappings: 05a17ab7488d3b86ec25c724f21a19cd7ebb9778",
"alerting_rule|schemas: da39a3ee5e6b4b0d3255bfef95601890afd80709",
"alerting_rule|10.1.0: 8b54e0f9a45fff3fbb39dd35dde15784674293f7dfb199bc10218f3658266207",
"alerting_rule|10.1.0: 7e2e621ff8a85dd04d0b8e374a9a8b85ce23ba968dfbacef9dfd23a003dc4afb",
"======================================================================================",
"alerting_rule_template|global: a8ee387a4bc794ff6450017a92742b39b79e0446",
"alerting_rule_template|mappings: eccf889027b5ea2d292c1bf0f9280348deaec0ef",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ const validCreateData = {
kind: 'alert',
metadata: { name: 'test rule' },
schedule: { every: '5m' },
evaluation: { query: { base: 'FROM logs-* | LIMIT 1' } },
};

const validCreateDataWithCondition = {
...validCreateData,
evaluation: { query: { base: 'FROM logs-* | LIMIT 1', condition: 'count > 0' } },
};

Expand All @@ -19,6 +24,18 @@ describe('createRuleDataSchema', () => {
it('accepts a minimal valid payload and applies defaults', () => {
const result = createRuleDataSchema.parse(validCreateData);

expect(result).toEqual({
kind: 'alert',
metadata: { name: 'test rule' },
time_field: '@timestamp',
schedule: { every: '5m' },
evaluation: { query: { base: 'FROM logs-* | LIMIT 1' } },
});
});

it('accepts a minimal valid payload with condition', () => {
const result = createRuleDataSchema.parse(validCreateDataWithCondition);

expect(result).toEqual({
kind: 'alert',
metadata: { name: 'test rule' },
Expand All @@ -30,7 +47,7 @@ describe('createRuleDataSchema', () => {

it('accepts a full payload with all optional fields', () => {
const result = createRuleDataSchema.parse({
...validCreateData,
...validCreateDataWithCondition,
metadata: { name: 'test rule', owner: 'team-a', labels: ['label-1', 'label-2'] },
time_field: 'event.created',
schedule: { every: '5m', lookback: '10m' },
Expand Down Expand Up @@ -153,15 +170,71 @@ describe('createRuleDataSchema', () => {
it('rejects an empty query', () => {
const result = createRuleDataSchema.safeParse({
...validCreateData,
evaluation: { query: { base: '', condition: 'count > 0' } },
evaluation: { query: { base: '' } },
});
expect(result.success).toBe(false);
});

it('rejects an invalid ES|QL query', () => {
const result = createRuleDataSchema.safeParse({
...validCreateData,
evaluation: { query: { base: 'FROM |', condition: 'count > 0' } },
evaluation: { query: { base: 'FROM |' } },
});
expect(result.success).toBe(false);
});
});

describe('evaluation.query.condition', () => {
it('accepts an alert rule without condition', () => {
const result = createRuleDataSchema.safeParse(validCreateData);
expect(result.success).toBe(true);
expect(result.data?.evaluation.query.condition).toBeUndefined();
});

it('accepts an alert rule with condition', () => {
const result = createRuleDataSchema.safeParse(validCreateDataWithCondition);
expect(result.success).toBe(true);
expect(result.data?.evaluation.query.condition).toBe('count > 0');
});

it('accepts a signal rule without condition', () => {
const result = createRuleDataSchema.safeParse({
...validCreateData,
kind: 'signal',
});
expect(result.success).toBe(true);
});

it('rejects a signal rule with condition', () => {
const result = createRuleDataSchema.safeParse({
...validCreateDataWithCondition,
kind: 'signal',
});
expect(result.success).toBe(false);
expect(result.error?.issues[0].path).toEqual(['evaluation', 'query', 'condition']);
});

it('requires condition when no_data is configured', () => {
const result = createRuleDataSchema.safeParse({
...validCreateData,
no_data: { behavior: 'no_data', timeframe: '10m' },
});
expect(result.success).toBe(false);
expect(result.error?.issues[0].path).toEqual(['evaluation', 'query', 'condition']);
});

it('accepts no_data with condition present', () => {
const result = createRuleDataSchema.safeParse({
...validCreateDataWithCondition,
no_data: { behavior: 'no_data', timeframe: '10m' },
});
expect(result.success).toBe(true);
});

it('rejects an empty condition string', () => {
const result = createRuleDataSchema.safeParse({
...validCreateData,
evaluation: { query: { base: 'FROM logs-* | LIMIT 1', condition: '' } },
});
expect(result.success).toBe(false);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ const scheduleSchema = z
const evaluationQuerySchema = z
.object({
base: esqlQuerySchema.describe('Base ES|QL query.'),
condition: z.string().min(1).max(5000).describe('Trigger condition (WHERE clause).'),
condition: z
.string()
.min(1)
.max(5000)
.optional()
.describe('Trigger condition (WHERE clause). Required when no_data is configured.'),
})
.strict();

Expand Down Expand Up @@ -180,18 +185,17 @@ export const createRuleDataSchema = z
notification_policies: z.array(notificationPolicyRefSchema).optional(),
})
.strip()
/**
*
* The `.refine` method adds a custom validation to the schema.
* In this case, it enforces that the `state_transition` property is only allowed when `kind` is "alert".
* The predicate `data.kind === 'alert' || data.state_transition == null` means:
* - If the rule kind is "alert", `state_transition` may be present (or absent).
* - For any other `kind`, `state_transition` must be `null` or `undefined`.
* If validation fails, the specified error message will be associated with the `state_transition` field.
*/
.refine((data) => data.kind === 'alert' || data.state_transition == null, {
message: 'state_transition is only allowed when kind is "alert".',
path: ['state_transition'],
})
.refine((data) => data.kind === 'alert' || data.evaluation.query.condition == null, {
message: 'evaluation.query.condition is only allowed when kind is "alert".',
path: ['evaluation', 'query', 'condition'],
})
.refine((data) => !data.no_data || data.evaluation.query.condition != null, {
message: 'evaluation.query.condition is required when no_data is configured.',
path: ['evaluation', 'query', 'condition'],
});

export type CreateRuleData = z.infer<typeof createRuleDataSchema>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export interface RuleResponse {
evaluation: {
query: {
base: string;
condition: string;
condition?: string;
};
};
recovery_policy?: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,12 @@ export interface CompletionContext {
}

/**
* Default property names that should be treated as ES|QL queries
* YAML property names whose values contain ES|QL queries.
* The editor enables ES|QL syntax highlighting and auto-completion for these fields.
*
* TODO: with zod4 upgrade we can add a metadata tag to the schema to mark these fields as ES|QL queries.
*/
export const DEFAULT_ESQL_PROPERTY_NAMES = ['query'];
export const DEFAULT_ESQL_PROPERTY_NAMES = ['base'];

/**
* Props for the YamlRuleEditor component
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ export const CreateRulePage = () => {
evaluation: {
query: {
base: rule.evaluation?.query?.base ?? DEFAULT_RULE_VALUES.evaluation.query.base,
condition:
rule.evaluation?.query?.condition ?? DEFAULT_RULE_VALUES.evaluation.query.condition,
...(rule.evaluation?.query?.condition != null
? { condition: rule.evaluation.query.condition }
: {}),
},
},
recovery_policy: rule.recovery_policy,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const ruleSavedObjectAttributesSchema = schema.object({
evaluation: schema.object({
query: schema.object({
base: schema.string(),
condition: schema.string(),
condition: schema.maybe(schema.string()),
}),
}),
recovery_policy: schema.maybe(
Expand Down