diff --git a/docs/snippets/providers/cloudwatch-snippet-autogenerated.mdx b/docs/snippets/providers/cloudwatch-snippet-autogenerated.mdx index c5f44499c3..a9a314eacc 100644 --- a/docs/snippets/providers/cloudwatch-snippet-autogenerated.mdx +++ b/docs/snippets/providers/cloudwatch-snippet-autogenerated.mdx @@ -1,4 +1,4 @@ -{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py +{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py Do not edit it manually, as it will be overwritten */} ## Authentication @@ -33,11 +33,11 @@ steps: provider: cloudwatch config: "{{ provider.my_provider_name }}" with: - log_group: {value} - log_groups: {value} - remove_ptr_from_results: {value} - query: {value} - hours: {value} + log_group: {value} + log_groups: {value} + remove_ptr_from_results: {value} + query: {value} + hours: {value} ``` @@ -47,3 +47,4 @@ steps: Check the following workflow examples: - [retrieve_cloudwatch_logs.yaml](https://github.com/keephq/keep/blob/main/examples/workflows/retrieve_cloudwatch_logs.yaml) - [slack_basic.yml](https://github.com/keephq/keep/blob/main/examples/workflows/slack_basic.yml) +- [slack_basic_cel.yml](https://github.com/keephq/keep/blob/main/examples/workflows/slack_basic_cel.yml) diff --git a/docs/snippets/providers/console-snippet-autogenerated.mdx b/docs/snippets/providers/console-snippet-autogenerated.mdx index c61b0f0562..63a690a8d3 100644 --- a/docs/snippets/providers/console-snippet-autogenerated.mdx +++ b/docs/snippets/providers/console-snippet-autogenerated.mdx @@ -35,6 +35,7 @@ actions: Check the following workflow examples: - [aks_basic.yml](https://github.com/keephq/keep/blob/main/examples/workflows/aks_basic.yml) - [change.yml](https://github.com/keephq/keep/blob/main/examples/workflows/change.yml) +- [complex-conditions-cel.yml](https://github.com/keephq/keep/blob/main/examples/workflows/complex-conditions-cel.yml) - [console_example.yml](https://github.com/keephq/keep/blob/main/examples/workflows/console_example.yml) - [consts_and_dict.yml](https://github.com/keephq/keep/blob/main/examples/workflows/consts_and_dict.yml) - [cron-digest-alerts.yml](https://github.com/keephq/keep/blob/main/examples/workflows/cron-digest-alerts.yml) @@ -45,7 +46,9 @@ Check the following workflow examples: - [incident-enrich.yaml](https://github.com/keephq/keep/blob/main/examples/workflows/incident-enrich.yaml) - [incident_example.yml](https://github.com/keephq/keep/blob/main/examples/workflows/incident_example.yml) - [inputs_example.yml](https://github.com/keephq/keep/blob/main/examples/workflows/inputs_example.yml) +- [multi-condition-cel.yml](https://github.com/keephq/keep/blob/main/examples/workflows/multi-condition-cel.yml) - [mustache-paths-example.yml](https://github.com/keephq/keep/blob/main/examples/workflows/mustache-paths-example.yml) +- [pattern-matching-cel.yml](https://github.com/keephq/keep/blob/main/examples/workflows/pattern-matching-cel.yml) - [severity_changed.yml](https://github.com/keephq/keep/blob/main/examples/workflows/severity_changed.yml) - [webhook_example.yml](https://github.com/keephq/keep/blob/main/examples/workflows/webhook_example.yml) - [webhook_example_foreach.yml](https://github.com/keephq/keep/blob/main/examples/workflows/webhook_example_foreach.yml) diff --git a/docs/snippets/providers/datadog-snippet-autogenerated.mdx b/docs/snippets/providers/datadog-snippet-autogenerated.mdx index 85e9404e0c..3418ad5981 100644 --- a/docs/snippets/providers/datadog-snippet-autogenerated.mdx +++ b/docs/snippets/providers/datadog-snippet-autogenerated.mdx @@ -1,4 +1,4 @@ -{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py +{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py Do not edit it manually, as it will be overwritten */} ## Authentication @@ -10,14 +10,14 @@ This provider requires authentication. - **oauth_token**: For OAuth flow (required: False, sensitive: True) Certain scopes may be required to perform specific actions or queries via the provider. Below is a summary of relevant scopes and their use cases: -- **events_read**: Read events data. (mandatory) +- **events_read**: Read events data. (mandatory) - **monitors_read**: Read monitors (mandatory) ([Documentation](https://docs.datadoghq.com/account_management/rbac/permissions/#monitors)) - **monitors_write**: Write monitors ([Documentation](https://docs.datadoghq.com/account_management/rbac/permissions/#monitors)) -- **create_webhooks**: Create webhooks integrations -- **metrics_read**: View custom metrics. -- **logs_read**: Read log data. -- **apm_read**: Read APM data for Topology creation. -- **apm_service_catalog_read**: Read APM service catalog for Topology creation. +- **create_webhooks**: Create webhooks integrations +- **metrics_read**: View custom metrics. +- **logs_read**: Read log data. +- **apm_read**: Read APM data for Topology creation. +- **apm_service_catalog_read**: Read APM service catalog for Topology creation. @@ -33,9 +33,9 @@ steps: provider: datadog config: "{{ provider.my_provider_name }}" with: - query: {value} - timeframe: {value} - query_type: {value} + query: {value} + timeframe: {value} + query_type: {value} ``` @@ -43,14 +43,15 @@ steps: Check the following workflow examples: +- [complex-conditions-cel.yml](https://github.com/keephq/keep/blob/main/examples/workflows/complex-conditions-cel.yml) - [db_disk_space.yml](https://github.com/keephq/keep/blob/main/examples/workflows/db_disk_space.yml) - [dd.yml](https://github.com/keephq/keep/blob/main/examples/workflows/dd.yml) - [keep_semantic_alert_example_datadog.yml](https://github.com/keephq/keep/blob/main/examples/workflows/keep_semantic_alert_example_datadog.yml) ## Topology -This provider pulls [topology](/overview/servicetopology) to Keep. It could be used in [correlations](/overview/correlation-topology) -and [mapping](/overview/enrichment/mapping#mapping-with-topology-data), and as a context +This provider pulls [topology](/overview/servicetopology) to Keep. It could be used in [correlations](/overview/correlation-topology) +and [mapping](/overview/enrichment/mapping#mapping-with-topology-data), and as a context for [alerts](/alerts/sidebar#7-alert-topology-view) and [incidents](/overview#17-incident-topology). ## Provider Methods @@ -69,4 +70,3 @@ The provider exposes the following [Provider Methods](/providers/provider-method - **resolve_incident** Resolve an active incident (action, scopes: incidents_write) - **add_incident_timeline_note** Add a note to an incident timeline (action, scopes: incidents_write) - diff --git a/docs/snippets/providers/newrelic-snippet-autogenerated.mdx b/docs/snippets/providers/newrelic-snippet-autogenerated.mdx index 671c8fa850..2ded3e487d 100644 --- a/docs/snippets/providers/newrelic-snippet-autogenerated.mdx +++ b/docs/snippets/providers/newrelic-snippet-autogenerated.mdx @@ -1,4 +1,4 @@ -{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py +{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py Do not edit it manually, as it will be overwritten */} ## Authentication @@ -28,11 +28,13 @@ steps: provider: newrelic config: "{{ provider.my_provider_name }}" with: - nrql: {value} + nrql: {value} query: {value} # query to execute ``` -If you need workflow examples with this provider, please raise a [GitHub issue](https://github.com/keephq/keep/issues). + +Check the following workflow example: +- [complex-conditions-cel.yml](https://github.com/keephq/keep/blob/main/examples/workflows/complex-conditions-cel.yml) diff --git a/docs/snippets/providers/opsgenie-snippet-autogenerated.mdx b/docs/snippets/providers/opsgenie-snippet-autogenerated.mdx index 35400efc42..07d4ac8629 100644 --- a/docs/snippets/providers/opsgenie-snippet-autogenerated.mdx +++ b/docs/snippets/providers/opsgenie-snippet-autogenerated.mdx @@ -1,4 +1,4 @@ -{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py +{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py Do not edit it manually, as it will be overwritten */} ## Authentication @@ -7,7 +7,7 @@ This provider requires authentication. - **integration_name**: OpsGenie integration name (required: True, sensitive: False) Certain scopes may be required to perform specific actions or queries via the provider. Below is a summary of relevant scopes and their use cases: -- **opsgenie:create**: Create OpsGenie alerts (mandatory) +- **opsgenie:create**: Create OpsGenie alerts (mandatory) @@ -23,8 +23,8 @@ steps: provider: opsgenie config: "{{ provider.my_provider_name }}" with: - query_type: {value} - query: {value} + query_type: {value} + query: {value} ``` @@ -58,6 +58,7 @@ actions: Check the following workflow examples: - [failed-to-login-workflow.yml](https://github.com/keephq/keep/blob/main/examples/workflows/failed-to-login-workflow.yml) - [opsgenie-close-alert.yml](https://github.com/keephq/keep/blob/main/examples/workflows/opsgenie-close-alert.yml) +- [opsgenie-create-alert-cel.yml](https://github.com/keephq/keep/blob/main/examples/workflows/opsgenie-create-alert-cel.yml) - [opsgenie-create-alert.yml](https://github.com/keephq/keep/blob/main/examples/workflows/opsgenie-create-alert.yml) - [opsgenie_open_alerts.yml](https://github.com/keephq/keep/blob/main/examples/workflows/opsgenie_open_alerts.yml) @@ -68,4 +69,3 @@ The provider exposes the following [Provider Methods](/providers/provider-method - **close_alert** Close an alert (action, scopes: opsgenie:create) - **comment_alert** Comment an alert (action, scopes: opsgenie:create) - diff --git a/docs/snippets/providers/prometheus-snippet-autogenerated.mdx b/docs/snippets/providers/prometheus-snippet-autogenerated.mdx index 40e091cfcb..1e4744a33d 100644 --- a/docs/snippets/providers/prometheus-snippet-autogenerated.mdx +++ b/docs/snippets/providers/prometheus-snippet-autogenerated.mdx @@ -39,6 +39,7 @@ Check the following workflow examples: - [enrich_using_structured_output_from_vllm_qwen.yaml](https://github.com/keephq/keep/blob/main/examples/workflows/enrich_using_structured_output_from_vllm_qwen.yaml) - [http_enrich.yml](https://github.com/keephq/keep/blob/main/examples/workflows/http_enrich.yml) - [kubernetes.yml](https://github.com/keephq/keep/blob/main/examples/workflows/kubernetes.yml) +- [multi-condition-cel.yml](https://github.com/keephq/keep/blob/main/examples/workflows/multi-condition-cel.yml) ## Connecting via Webhook (omnidirectional) diff --git a/docs/snippets/providers/slack-snippet-autogenerated.mdx b/docs/snippets/providers/slack-snippet-autogenerated.mdx index 3dfaf77652..da2f772961 100644 --- a/docs/snippets/providers/slack-snippet-autogenerated.mdx +++ b/docs/snippets/providers/slack-snippet-autogenerated.mdx @@ -56,6 +56,7 @@ Check the following workflow examples: - [raw_sql_query_datetime.yml](https://github.com/keephq/keep/blob/main/examples/workflows/raw_sql_query_datetime.yml) - [slack-message-reaction.yml](https://github.com/keephq/keep/blob/main/examples/workflows/slack-message-reaction.yml) - [slack_basic.yml](https://github.com/keephq/keep/blob/main/examples/workflows/slack_basic.yml) +- [slack_basic_cel.yml](https://github.com/keephq/keep/blob/main/examples/workflows/slack_basic_cel.yml) - [slack_basic_interval.yml](https://github.com/keephq/keep/blob/main/examples/workflows/slack_basic_interval.yml) - [trello_new_card_alert.yml](https://github.com/keephq/keep/blob/main/examples/workflows/trello_new_card_alert.yml) - [workflow_only_first_time_example.yml](https://github.com/keephq/keep/blob/main/examples/workflows/workflow_only_first_time_example.yml) diff --git a/docs/workflows/examples/create-servicenow-tickets.mdx b/docs/workflows/examples/create-servicenow-tickets.mdx index b556181db8..5c7d47f20e 100644 --- a/docs/workflows/examples/create-servicenow-tickets.mdx +++ b/docs/workflows/examples/create-servicenow-tickets.mdx @@ -22,9 +22,7 @@ workflow: description: create a ticket in servicenow when an alert is triggered triggers: - type: alert - filters: - - key: source - value: r"(grafana|prometheus)" + cel: source.contains("grafana") || source.contains("prometheus") actions: - name: create-service-now-ticket if: "not '{{ alert.ticket_id }}' and {{ alert.annotations.ticket_type }}" diff --git a/docs/workflows/examples/highsev.mdx b/docs/workflows/examples/highsev.mdx index 8714a7f2d5..6916cc3d16 100644 --- a/docs/workflows/examples/highsev.mdx +++ b/docs/workflows/examples/highsev.mdx @@ -27,13 +27,7 @@ workflow: description: handle alerts triggers: - type: alert - filters: - - key: source - value: sentry - - key: severity - value: critical - - key: service - value: r"(payments|ftp)" + cel: source.contains("sentry") && severity == "critical" && (service == "payments" || service == "ftp") actions: - name: send-slack-message-team-payments if: "'{{ alert.service }}' == 'payments'" diff --git a/docs/workflows/examples/update-servicenow-tickets.mdx b/docs/workflows/examples/update-servicenow-tickets.mdx index 7289fc3f40..e4587ca427 100644 --- a/docs/workflows/examples/update-servicenow-tickets.mdx +++ b/docs/workflows/examples/update-servicenow-tickets.mdx @@ -27,9 +27,7 @@ workflow: provider: type: keep with: - filters: - - key: ticket_type - value: servicenow + cel: ticket_type == "servicenow" actions: - name: update-ticket foreach: "{{ steps.get-alerts.results }}" diff --git a/docs/workflows/syntax/triggers.mdx b/docs/workflows/syntax/triggers.mdx index 7ed5497a57..d12a617d82 100644 --- a/docs/workflows/syntax/triggers.mdx +++ b/docs/workflows/syntax/triggers.mdx @@ -6,7 +6,6 @@ title: "Triggers" Triggers in Keep Workflow Engine define **when a workflow is executed**. Triggers are the starting point for workflows and can be configured to respond to a variety of events, conditions, or schedules. - A workflow can have one or multiple triggers, and these triggers determine the specific circumstances under which the workflow is initiated. Examples include manual invocation, time-based schedules, or event-driven actions like alerts or incident updates. Triggers are defined under the `triggers` section of a workflow YAML file. Each trigger has a `type` and optional additional configurations or filters. @@ -35,16 +34,45 @@ triggers: ### Alert Trigger -Executes a workflow when an alert is received, with optional filters for alert properties. +Executes a workflow when an alert is received. ```yaml triggers: - type: alert ``` -### Filtering Alert + + If no filters or CEL expressions are specified, the workflow will be executed + for every alert that comes in. + + +### Filtering Alerts + +There are two ways to filter alerts in Keep: -You can filter alerts by specific properties like `severity`, `source`, or use regex to match specific `service`. +#### 1. CEL-based Filtering (Recommended) + +Keep uses [Common Expression Language (CEL)](https://github.com/google/cel-spec/blob/master/doc/langdef.md) for filtering alerts. CEL provides a powerful and flexible way to express conditions using a simple expression language. + +```yaml +triggers: + - type: alert + cel: source.contains("datadog") && severity == "critical" +``` + +Common CEL patterns: + +- String matching: `source.contains("prometheus")` +- Exact matching: `severity == "critical"` +- Multiple conditions: `source.contains("datadog") && severity == "critical"` +- Pattern matching: `name.contains("error") || name.contains("failure")` +- Complex conditions: `(source.contains("datadog") && severity == "critical") || (source.contains("newrelic") && severity == "error")` + +You can test and experiment with CEL expressions using the [CEL Playground](https://playcel.undistro.io/). + +#### 2. Legacy Filtering (Deprecated) + +The old filtering mechanism is deprecated but still supported for backward compatibility. It uses a list of key-value pairs with optional regex patterns. ```yaml triggers: @@ -63,7 +91,6 @@ triggers: Runs workflows when an incident is created, updated, or resolved. ```yaml - triggers: - type: incident on: @@ -80,9 +107,10 @@ triggers: - type: alert only_on_change: - status - ``` ## Summary -Triggers are a powerful way to control the execution of workflows, ensuring that they respond appropriately to manual actions, schedules, or events. By leveraging filters and configurations, workflows can be fine-tuned to execute only under specific conditions. +Triggers are a powerful way to control the execution of workflows, ensuring that they respond appropriately to manual actions, schedules, or events. By leveraging CEL expressions or filters, workflows can be fine-tuned to execute only under specific conditions. + +For more information about CEL expressions, refer to the [CEL Language Definition](https://github.com/google/cel-spec/blob/master/doc/langdef.md) and experiment with expressions in the [CEL Playground](https://playcel.undistro.io/). diff --git a/examples/workflows/complex-conditions-cel.yml b/examples/workflows/complex-conditions-cel.yml new file mode 100644 index 0000000000..5daef5cae1 --- /dev/null +++ b/examples/workflows/complex-conditions-cel.yml @@ -0,0 +1,13 @@ +workflow: + id: complex-conditions-monitor-cel + name: Complex Conditions Monitor (CEL) + description: Monitors alerts with complex conditions using CEL filters. + triggers: + - type: alert + cel: (source.contains("datadog") && severity == "critical") || (source.contains("newrelic") && severity == "error") + actions: + - name: notify + provider: + type: console + with: + message: "Critical Datadog or error NewRelic alert: {{ alert.name }}" diff --git a/examples/workflows/multi-condition-cel.yml b/examples/workflows/multi-condition-cel.yml new file mode 100644 index 0000000000..23c636e256 --- /dev/null +++ b/examples/workflows/multi-condition-cel.yml @@ -0,0 +1,13 @@ +workflow: + id: multi-condition-monitor-cel + name: Multi-Condition Monitor (CEL) + description: Monitors alerts with multiple conditions using CEL filters. + triggers: + - type: alert + cel: source.contains("prometheus") && severity == "critical" && environment == "production" + actions: + - name: notify + provider: + type: console + with: + message: "Critical production alert from Prometheus: {{ alert.name }}" diff --git a/examples/workflows/opsgenie-create-alert-cel.yml b/examples/workflows/opsgenie-create-alert-cel.yml new file mode 100644 index 0000000000..8a1b5928b0 --- /dev/null +++ b/examples/workflows/opsgenie-create-alert-cel.yml @@ -0,0 +1,22 @@ +workflow: + id: opsgenie-critical-alert-creator-cel + name: OpsGenie Critical Alert Creator (CEL) + description: Creates OpsGenie alerts for critical Coralogix issues with team assignment and alert enrichment tracking using CEL filters. + triggers: + - type: manual + - type: alert + cel: source.contains("coralogix") && severity == "critical" + actions: + - name: create-alert + provider: + config: "{{ providers.opsgenie }}" + type: opsgenie + if: "not '{{ alert.opsgenie_alert_id }}'" + with: + message: "{{ alert.name }}" + responders: + - name: "{{ alert.team }}" + type: team + enrich_alert: + - key: opsgenie_alert_id + value: results.alertId diff --git a/examples/workflows/pattern-matching-cel.yml b/examples/workflows/pattern-matching-cel.yml new file mode 100644 index 0000000000..8c2f6d505e --- /dev/null +++ b/examples/workflows/pattern-matching-cel.yml @@ -0,0 +1,13 @@ +workflow: + id: pattern-matching-monitor-cel + name: Pattern Matching Monitor (CEL) + description: Monitors alerts with pattern matching using CEL filters. + triggers: + - type: alert + cel: name.contains("error") || name.contains("failure") + actions: + - name: notify + provider: + type: console + with: + message: "Error or failure detected: {{ alert.name }}" diff --git a/examples/workflows/slack_basic_cel.yml b/examples/workflows/slack_basic_cel.yml new file mode 100644 index 0000000000..01fb6b825e --- /dev/null +++ b/examples/workflows/slack_basic_cel.yml @@ -0,0 +1,15 @@ +workflow: + id: cloudwatch-slack-notifier-cel + name: CloudWatch Slack Notifier (CEL) + description: Forwards AWS CloudWatch alarms to Slack channels with customized alert messages using CEL filters. + triggers: + - type: alert + cel: source.contains("cloudwatch") + - type: manual + actions: + - name: trigger-slack + provider: + type: slack + config: " {{ providers.slack-prod }} " + with: + message: "Got alarm from aws cloudwatch! {{ alert.name }}" diff --git a/keep-ui/app/(keep)/workflows/workflow-tile.tsx b/keep-ui/app/(keep)/workflows/workflow-tile.tsx index 0d5e70efd1..3619b75dfb 100644 --- a/keep-ui/app/(keep)/workflows/workflow-tile.tsx +++ b/keep-ui/app/(keep)/workflows/workflow-tile.tsx @@ -41,6 +41,7 @@ function TriggerTile({ trigger }: { trigger: Trigger }) { {trigger.type === "interval" && {trigger.value} seconds} {trigger.type === "alert" && ( + {trigger.cel && CEL = {trigger.cel}} {trigger.filters && trigger.filters.map((filter) => ( diff --git a/keep-ui/entities/workflows/lib/__tests__/validation.test.ts b/keep-ui/entities/workflows/lib/__tests__/validation.test.ts index 5934bf19d5..46105263f3 100644 --- a/keep-ui/entities/workflows/lib/__tests__/validation.test.ts +++ b/keep-ui/entities/workflows/lib/__tests__/validation.test.ts @@ -496,27 +496,6 @@ describe("validateGlobalPure", () => { ]); }); - it("should detect empty alert trigger", () => { - const definition: Definition = { - properties: { - id: "test-workflow", - name: "Test Workflow", - description: "Test Description", - disabled: false, - isLocked: false, - consts: {}, - alert: {}, - }, - sequence: [], - }; - - const result = validateGlobalPure(definition); - expect(result).toContainEqual([ - "alert", - "Alert trigger should have at least one filter.", - ]); - }); - it("should detect empty incident trigger", () => { const definition: Definition = { properties: { diff --git a/keep-ui/entities/workflows/lib/getTriggerDescription.ts b/keep-ui/entities/workflows/lib/getTriggerDescription.ts index 194263714f..6e13287253 100644 --- a/keep-ui/entities/workflows/lib/getTriggerDescription.ts +++ b/keep-ui/entities/workflows/lib/getTriggerDescription.ts @@ -37,10 +37,13 @@ export function getTriggerDescriptionFromStep(trigger: V2StepTrigger) { return `Every ${getHumanReadableInterval(trigger.properties.interval)} (${trigger.properties.interval} seconds)`; } case "alert": { - const alertProperties = trigger.properties?.alert - ? trigger.properties.alert - : trigger.properties; - return `${Object.entries(alertProperties) + if (trigger.properties?.cel) { + return `CEL: ${trigger.properties.cel}`; + } + const alertFilters = trigger.properties?.filters + ? trigger.properties.filters + : {}; + return `${Object.entries(alertFilters) .map(([key, value]) => `${key}=${value}`) .join(", ")}`; } diff --git a/keep-ui/entities/workflows/lib/parser.ts b/keep-ui/entities/workflows/lib/parser.ts index 03b4ba4e33..dbd4ef0fb8 100644 --- a/keep-ui/entities/workflows/lib/parser.ts +++ b/keep-ui/entities/workflows/lib/parser.ts @@ -219,13 +219,19 @@ export function parseWorkflow( const currType = curr.type; let value = curr.value; if (currType === "alert") { + value = {}; if (curr.filters) { - value = curr.filters.reduce((prev: any, curr: any) => { + const filters = curr.filters.reduce((prev: any, curr: any) => { prev[curr.key] = curr.value; return prev; }, {}); - } else { - value = {}; + value["filters"] = filters; + } + if (curr.cel) { + value["cel"] = curr.cel; + } + if (curr.only_on_change) { + value["only_on_change"] = curr.only_on_change; } } else if (currType === "manual") { value = "true"; @@ -487,20 +493,24 @@ export function getYamlWorkflowDefinition( const triggers = []; if (alert.properties.manual === "true") triggers.push({ type: "manual" }); - if ( - alert.properties.alert && - Object.keys(alert.properties.alert).length > 0 - ) { - const filters = Object.keys(alert.properties.alert).map((key) => { - return { - key: key, - value: (alert.properties.alert as any)[key], - }; - }); - triggers.push({ - type: "alert", - filters: filters, - }); + if (alert.properties.alert) { + const alertTrigger: any = { type: "alert" }; + if (alert.properties.alert.filters) { + const filters = Object.keys(alert.properties.alert.filters).map((key) => { + return { + key: key, + value: (alert.properties.alert as any)[key], + }; + }); + alertTrigger["filters"] = filters; + } + if (alert.properties.alert.cel) { + alertTrigger["cel"] = alert.properties.alert.cel; + } + if (alert.properties.alert.only_on_change) { + alertTrigger["only_on_change"] = alert.properties.alert.only_on_change; + } + triggers.push(alertTrigger); } if (alert.properties.interval) { triggers.push({ diff --git a/keep-ui/entities/workflows/lib/validation.ts b/keep-ui/entities/workflows/lib/validation.ts index 5a67849a5e..3e79628ab3 100644 --- a/keep-ui/entities/workflows/lib/validation.ts +++ b/keep-ui/entities/workflows/lib/validation.ts @@ -171,17 +171,6 @@ export function validateGlobalPure(definition: Definition): ValidationResult[] { errors.push(["interval", "Workflow interval cannot be empty."]); } - const alertSources = Object.values(definition.properties.alert || {}).filter( - Boolean - ); - if ( - definition?.properties && - definition.properties["alert"] && - alertSources.length == 0 - ) { - errors.push(["alert", "Alert trigger should have at least one filter."]); - } - const incidentEvents = definition.properties.incident?.events; if ( definition?.properties && diff --git a/keep-ui/entities/workflows/model/schema.ts b/keep-ui/entities/workflows/model/schema.ts index 48f94ac62b..1abcb9803c 100644 --- a/keep-ui/entities/workflows/model/schema.ts +++ b/keep-ui/entities/workflows/model/schema.ts @@ -2,10 +2,13 @@ import { z } from "zod"; const ManualTriggerValueSchema = z.literal("true"); -export const V2StepManualTriggerSchema = z.object({ +const TriggerSchemaBase = z.object({ id: z.string(), name: z.string(), componentType: z.literal("trigger"), +}); + +export const V2StepManualTriggerSchema = TriggerSchemaBase.extend({ type: z.literal("manual"), properties: z.object({ manual: ManualTriggerValueSchema, @@ -14,10 +17,7 @@ export const V2StepManualTriggerSchema = z.object({ const IntervalTriggerValueSchema = z.union([z.string(), z.number()]); -export const V2StepIntervalTriggerSchema = z.object({ - id: z.string(), - name: z.string(), - componentType: z.literal("trigger"), +export const V2StepIntervalTriggerSchema = TriggerSchemaBase.extend({ type: z.literal("interval"), properties: z.object({ interval: IntervalTriggerValueSchema, @@ -25,16 +25,15 @@ export const V2StepIntervalTriggerSchema = z.object({ }); const AlertTriggerValueSchema = z.record(z.string(), z.string()); -export const V2StepAlertTriggerSchema = z.object({ - id: z.string(), - name: z.string(), - componentType: z.literal("trigger"), +export const V2StepAlertTriggerSchema = TriggerSchemaBase.extend({ type: z.literal("alert"), - properties: z.object({ - alert: AlertTriggerValueSchema, - source: z.string().optional(), - }), - only_on_change: z.array(z.string()).optional(), + properties: z + .object({ + filters: z.record(z.string(), z.string()).optional(), + cel: z.string().optional(), + only_on_change: z.array(z.string()).optional(), + }) + .optional(), }); export const IncidentEventEnum = z.enum(["created", "updated", "deleted"]); @@ -42,10 +41,8 @@ export const IncidentEventEnum = z.enum(["created", "updated", "deleted"]); const IncidentTriggerValueSchema = z.object({ events: z.array(IncidentEventEnum), }); -export const V2StepIncidentTriggerSchema = z.object({ - id: z.string(), - name: z.string(), - componentType: z.literal("trigger"), + +export const V2StepIncidentTriggerSchema = TriggerSchemaBase.extend({ type: z.literal("incident"), properties: z.object({ incident: IncidentTriggerValueSchema, diff --git a/keep-ui/entities/workflows/model/yaml.schema.ts b/keep-ui/entities/workflows/model/yaml.schema.ts index e40c72448c..439c6bbac6 100644 --- a/keep-ui/entities/workflows/model/yaml.schema.ts +++ b/keep-ui/entities/workflows/model/yaml.schema.ts @@ -50,7 +50,9 @@ const ManualTriggerSchema = z.object({ const AlertTriggerSchema = z.object({ type: z.literal("alert"), - filters: z.array(z.object({ key: z.string(), value: z.string() })), + filters: z.array(z.object({ key: z.string(), value: z.string() })).optional(), + cel: z.string().optional(), + only_on_change: z.array(z.string()).optional(), }); const IntervalTriggerSchema = z.object({ diff --git a/keep-ui/entities/workflows/ui/NodeTriggerIcon.tsx b/keep-ui/entities/workflows/ui/NodeTriggerIcon.tsx index f908dcf2d9..845b6ab59f 100644 --- a/keep-ui/entities/workflows/ui/NodeTriggerIcon.tsx +++ b/keep-ui/entities/workflows/ui/NodeTriggerIcon.tsx @@ -16,7 +16,7 @@ export function NodeTriggerIcon({ nodeData }: { nodeData: NodeData }) { case "interval": return ; case "alert": { - const alertSource = nodeData.properties?.source; + const alertSource = nodeData.properties?.filters?.source; if (alertSource) { return ( void; @@ -19,6 +20,7 @@ interface CelInputProps { const CelInput: FC = ({ id, + staticPositionForSuggestions, value = "", fieldsForSuggestions = [], onValueChange, @@ -30,10 +32,12 @@ const CelInput: FC = ({ disabled = false, }) => { return ( -
+
{})} @@ -44,7 +48,7 @@ const CelInput: FC = ({ {placeholder && !value && ( -
+
{placeholder}
)} diff --git a/keep-ui/features/presets/create-or-update-preset/ui/alerts-count-badge.tsx b/keep-ui/features/presets/create-or-update-preset/ui/alerts-count-badge.tsx index 7a4624e512..7b9f5edb6b 100644 --- a/keep-ui/features/presets/create-or-update-preset/ui/alerts-count-badge.tsx +++ b/keep-ui/features/presets/create-or-update-preset/ui/alerts-count-badge.tsx @@ -6,12 +6,14 @@ interface AlertsCountBadgeProps { presetCEL: string; isDebouncing: boolean; vertical?: boolean; + description?: string; } export const AlertsCountBadge: React.FC = ({ presetCEL, isDebouncing, vertical = false, + description, }) => { console.log("AlertsCountBadge::presetCEL", presetCEL); const { useLastAlerts } = useAlerts(); @@ -26,7 +28,7 @@ export const AlertsCountBadge: React.FC = ({ // Show loading state when searching or debouncing if (isSearching || isDebouncing) { return ( - +
= ({ ... - Searching... + Searching...
@@ -49,7 +51,7 @@ export const AlertsCountBadge: React.FC = ({ } return ( - +
= ({ {totalCount} - + {totalCount === 1 ? "Alert" : "Alerts"} found
- - These are the alerts that would match your preset - + {description && ( + {description} + )}
); }; diff --git a/keep-ui/features/presets/create-or-update-preset/ui/create-or-update-preset-form.tsx b/keep-ui/features/presets/create-or-update-preset/ui/create-or-update-preset-form.tsx index d44bb3fed5..fa6913eba0 100644 --- a/keep-ui/features/presets/create-or-update-preset/ui/create-or-update-preset-form.tsx +++ b/keep-ui/features/presets/create-or-update-preset/ui/create-or-update-preset-form.tsx @@ -233,13 +233,12 @@ export function CreateOrUpdatePresetForm({ {/* Add alerts count card before the save buttons */} {presetData.CEL && ( -
- -
+ )}
diff --git a/keep-ui/features/presets/presets-manager/ui/alerts-rules-builder.tsx b/keep-ui/features/presets/presets-manager/ui/alerts-rules-builder.tsx index fbae12675c..a742ae595d 100644 --- a/keep-ui/features/presets/presets-manager/ui/alerts-rules-builder.tsx +++ b/keep-ui/features/presets/presets-manager/ui/alerts-rules-builder.tsx @@ -14,10 +14,10 @@ import "react-querybuilder/dist/query-builder.scss"; import { Table } from "@tanstack/react-table"; import { FiExternalLink, FiSave } from "react-icons/fi"; import { AlertDto } from "@/entities/alerts/model"; -import { TrashIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { TrashIcon } from "@heroicons/react/24/outline"; import { TbDatabaseImport } from "react-icons/tb"; import { components, GroupBase, MenuListProps } from "react-select"; -import { MonacoEditor, Select } from "@/shared/ui"; +import { Select } from "@/shared/ui"; import { useConfig } from "@/utils/hooks/useConfig"; import { IoSearchOutline } from "react-icons/io5"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; @@ -360,8 +360,8 @@ export const AlertsRulesBuilder = ({ operators: getOperators(id), })) : customFields - ? customFields - : []; + ? customFields + : []; const onImportSQL = () => { setImportSQLOpen(true); diff --git a/keep-ui/features/workflows/ai-assistant/ui/AddTriggerUI.tsx b/keep-ui/features/workflows/ai-assistant/ui/AddTriggerUI.tsx index ca5deb3f2a..a3969abb83 100644 --- a/keep-ui/features/workflows/ai-assistant/ui/AddTriggerUI.tsx +++ b/keep-ui/features/workflows/ai-assistant/ui/AddTriggerUI.tsx @@ -1,12 +1,12 @@ import { useState, useCallback, useEffect } from "react"; import { useWorkflowStore } from "@/entities/workflows"; -import { WF_DEBUG_INFO } from "../../builder/ui/debug-settings"; import { Button } from "@/components/ui"; import { JsonCard } from "@/shared/ui"; import { StepPreview } from "./StepPreview"; import { SuggestionResult, SuggestionStatus } from "./SuggestionStatus"; import { getErrorMessage } from "../lib/utils"; import { V2StepTrigger } from "@/entities/workflows/model/types"; +import { useConfig } from "@/utils/hooks/useConfig"; type AddTriggerUIPropsCommon = { trigger: V2StepTrigger; @@ -34,6 +34,7 @@ export const AddTriggerUI = ({ }: AddTriggerUIProps) => { const [isAddingTrigger, setIsAddingTrigger] = useState(false); const { addNodeBetween, getNextEdge } = useWorkflowStore(); + const { data: config } = useConfig(); const handleAddTrigger = useCallback(() => { if (isAddingTrigger) { @@ -85,7 +86,9 @@ export const AddTriggerUI = ({ if (status === "complete") { return (
- {WF_DEBUG_INFO && } + {config?.KEEP_WORKFLOW_DEBUG && ( + + )}

Do you want to add this trigger to the workflow?

@@ -94,7 +97,9 @@ export const AddTriggerUI = ({ } return (
- {WF_DEBUG_INFO && } + {config?.KEEP_WORKFLOW_DEBUG && ( + + )}

Do you want to add this trigger to the workflow?

diff --git a/keep-ui/features/workflows/ai-assistant/ui/StepPreview.tsx b/keep-ui/features/workflows/ai-assistant/ui/StepPreview.tsx index 6b951209b8..87d3cda7d1 100644 --- a/keep-ui/features/workflows/ai-assistant/ui/StepPreview.tsx +++ b/keep-ui/features/workflows/ai-assistant/ui/StepPreview.tsx @@ -6,8 +6,8 @@ import { normalizeStepType } from "../../builder/lib/utils"; import { stringify } from "yaml"; import { getTriggerDescriptionFromStep } from "@/entities/workflows/lib/getTriggerDescription"; import { getYamlFromStep } from "../lib/utils"; -import { WF_DEBUG_INFO } from "../../builder/ui/debug-settings"; import { JsonCard, MonacoEditor } from "@/shared/ui"; +import { useConfig } from "@/utils/hooks/useConfig"; function getStepIconUrl(data: V2Step | V2StepTrigger) { const { type } = data || {}; @@ -25,6 +25,7 @@ export const StepPreview = ({ step: V2Step | V2StepTrigger; className?: string; }) => { + const { data: config } = useConfig(); const yamlDefinition = getYamlFromStep(step); const yaml = yamlDefinition ? stringify(yamlDefinition) : null; @@ -33,7 +34,7 @@ export const StepPreview = ({ return (
- {WF_DEBUG_INFO && } + {config?.KEEP_WORKFLOW_DEBUG && }
{ - acc[filter.attribute] = filter.value; - return acc; - }, - {} as Record - ), + cel: args.args.alertFilters, }; const trigger = getTriggerDefinitionFromCopilotAction( @@ -480,7 +470,9 @@ export function WorkflowBuilderChat({ parameters: [ { name: "incidentEvents", - description: `The events of the incident trigger, one of: ${IncidentEventEnum.options.map((o) => `"${o}"`).join(", ")}`, + description: `The events of the incident trigger, one of: ${IncidentEventEnum.options + .map((o) => `"${o}"`) + .join(", ")}`, type: "string[]", required: true, }, @@ -1063,7 +1055,7 @@ Example: 'node_123__empty_true'`, } > {/* Debug info */} - {WF_DEBUG_INFO && ( + {config?.KEEP_WORKFLOW_DEBUG && (
+ {error && ( + + {Array.isArray(error) ? error[0] : error} + + )} +
+
+ CEL Expression + { + window.open(`${docsUrl}/overview/cel`, "_blank"); + }} + tooltip="Read more about CEL expressions" + /> +
+
+ updateAlertCel(value)} + onClearValue={() => updateAlertCel("")} + fieldsForSuggestions={alertFields} + /> +
+
+ +
- {properties.alert && - Object.keys(properties.alert ?? {}).map((filter) => ( -
- {filter} -
- - updateAlertFilter(filter, e.target.value) - } - value={ - (properties.alert as any)[filter] || ("" as string) - } - /> - deleteFilter(filter)} - /> +
+ Alert filter (deprecated) + + Please convert your alert filters to CEL expressions to ensure + stability and performance. + +
+ +
+ {properties.alert.filters && + Object.keys(properties.alert.filters ?? {}).map((filter) => ( +
+ {filter} +
+ + updateAlertFilter(filter, e.target.value) + } + value={ + (properties.alert.filters as any)[filter] || + ("" as string) + } + /> + deleteFilter(filter)} + /> +
-
- ))} + ))} +
); diff --git a/keep-ui/features/workflows/builder/ui/WorkflowEdge.tsx b/keep-ui/features/workflows/builder/ui/WorkflowEdge.tsx index 01d2d1d327..a7ecb31376 100644 --- a/keep-ui/features/workflows/builder/ui/WorkflowEdge.tsx +++ b/keep-ui/features/workflows/builder/ui/WorkflowEdge.tsx @@ -6,8 +6,8 @@ import { Button } from "@tremor/react"; import "@xyflow/react/dist/style.css"; import { PlusIcon } from "@heroicons/react/24/outline"; import clsx from "clsx"; -import { WF_DEBUG_INFO } from "./debug-settings"; import { edgeCanHaveAddButton } from "../lib/utils"; +import { useConfig } from "@/utils/hooks/useConfig"; export function DebugEdgeInfo({ id, @@ -21,7 +21,8 @@ export function DebugEdgeInfo({ labelY: number; isLayouted: boolean; }) { - if (!WF_DEBUG_INFO) { + const { data: config } = useConfig(); + if (!config?.KEEP_WORKFLOW_DEBUG) { return null; } return ( diff --git a/keep-ui/features/workflows/builder/ui/WorkflowNode.tsx b/keep-ui/features/workflows/builder/ui/WorkflowNode.tsx index c4a6c948c1..a57d985b77 100644 --- a/keep-ui/features/workflows/builder/ui/WorkflowNode.tsx +++ b/keep-ui/features/workflows/builder/ui/WorkflowNode.tsx @@ -11,7 +11,6 @@ import { toast } from "react-toastify"; import { FlowNode } from "@/entities/workflows/model/types"; import { DynamicImageProviderIcon } from "@/components/ui"; import clsx from "clsx"; -import { WF_DEBUG_INFO } from "./debug-settings"; import { ExclamationCircleIcon, ExclamationTriangleIcon, @@ -21,9 +20,11 @@ import { NodeTriggerIcon } from "@/entities/workflows/ui/NodeTriggerIcon"; import { normalizeStepType, triggerTypes } from "../lib/utils"; import { getTriggerDescriptionFromStep } from "@/entities/workflows/lib/getTriggerDescription"; import { ValidationError } from "@/entities/workflows/lib/validation"; +import { useConfig } from "@/utils/hooks/useConfig"; export function DebugNodeInfo({ id, data }: Pick) { - if (!WF_DEBUG_INFO) { + const { data: config } = useConfig(); + if (!config?.KEEP_WORKFLOW_DEBUG) { return null; } return ( @@ -190,7 +191,7 @@ function WorkflowNode({ id, data }: FlowNode) {