diff --git a/README.md b/README.md index ad5b71199..e5a532806 100644 --- a/README.md +++ b/README.md @@ -122,12 +122,19 @@ Workflow triggers can either be executed manually when an alert is activated or            -            +

+

                      +            + +            + +            +

Databases and data warehouses

diff --git a/docs/api-ref/mapping/create-mapping.mdx b/docs/api-ref/mapping/create-mapping.mdx new file mode 100644 index 000000000..6994f6aab --- /dev/null +++ b/docs/api-ref/mapping/create-mapping.mdx @@ -0,0 +1,3 @@ +--- +openapi: post /mapping +--- \ No newline at end of file diff --git a/docs/api-ref/mapping/delete-mapping-by-id.mdx b/docs/api-ref/mapping/delete-mapping-by-id.mdx new file mode 100644 index 000000000..52645c5dd --- /dev/null +++ b/docs/api-ref/mapping/delete-mapping-by-id.mdx @@ -0,0 +1,3 @@ +--- +openapi: delete /mapping/{mapping_id} +--- \ No newline at end of file diff --git a/docs/api-ref/mapping/get-mappings.mdx b/docs/api-ref/mapping/get-mappings.mdx new file mode 100644 index 000000000..b1ac11c0b --- /dev/null +++ b/docs/api-ref/mapping/get-mappings.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /mapping +--- \ No newline at end of file diff --git a/docs/cli/commands/cli-config-new.mdx b/docs/cli/commands/cli-config-new.mdx new file mode 100644 index 000000000..cee753e18 --- /dev/null +++ b/docs/cli/commands/cli-config-new.mdx @@ -0,0 +1,56 @@ +--- +sidebarTitle: "keep config new" +--- + +Create new config. + +## Usage + +``` +Usage: keep config new [OPTIONS]... +``` + +## Options +* `interactive`: + * Type: BOOL + * Default: `True` + * Usage: `--interactive` + + Create config interactively. + +* `url`: + * Type: STRING + * Default: `http://localhost:8080` + * Usage: `--url` + + The URL of the Keep backend server. + +* `api-key`: + * Type: STRING + * Default: `` + * Usage: `--api-key` + + The api key for authenticating over keep. + +* `help`: + * Type: BOOL + * Default: `false` + * Usage: `--help` + + Show this message and exit. + + + +## CLI Help + +``` +Usage: keep config new [OPTIONS] + + create new config. + +Options: + -u, --url TEXT The url of the keep api + -a, --api-key TEXT The api key for keep + -i, --interactive Interactive mode creating keep config (default True) + --help Show this message and exit. +``` diff --git a/docs/cli/commands/cli-config-provider.mdx b/docs/cli/commands/cli-config-provider.mdx deleted file mode 100644 index dc4d8ba1d..000000000 --- a/docs/cli/commands/cli-config-provider.mdx +++ /dev/null @@ -1,68 +0,0 @@ ---- -title: "keep config provider" -sidebarTitle: "keep config provider" ---- - -Set the provider configuration. - -## Usage - -``` -Usage: keep config provider [OPTIONS] -``` - -## Options -* `provider_type` (REQUIRED): - * Type: STRING - * Default: `none` - * Usage: `--provider-type --p` - - The provider to configure [e.g. elastic] - - -* `provider_id` (REQUIRED): - * Type: STRING - * Default: `none` - * Usage: `--provider-id --i` - - The provider unique identifier [e.g. elastic-prod] - - -* `provider_config_file`: - * Type: STRING - * Default: `providers.yaml` - * Usage: `--provider-config-file --c` - - The provider config - - -* `help`: - * Type: BOOL - * Default: `false` - * Usage: `--help` - - Show this message and exit. - - - -## CLI Help - -``` -Usage: keep config provider [OPTIONS] - - Set the provider configuration. - -Options: - -p, --provider-type TEXT The provider to configure [e.g. elastic] - [required] - - -i, --provider-id TEXT The provider unique identifier [e.g. - elastic-prod] [required] - - -c, --provider-config-file TEXT - The provider config - --help Show this message and exit. -``` diff --git a/docs/cli/commands/cli-config-show.mdx b/docs/cli/commands/cli-config-show.mdx new file mode 100644 index 000000000..a217484b6 --- /dev/null +++ b/docs/cli/commands/cli-config-show.mdx @@ -0,0 +1,32 @@ +--- +sidebarTitle: "keep config show" +--- + +Show keep configuration. + +## Usage + +``` +Usage: keep config show [OPTIONS]... +``` + +## Options +* `help`: + * Type: BOOL + * Default: `false` + * Usage: `--help` + + Show this message and exit. + + + +## CLI Help + +``` +Usage: keep config show [OPTIONS] + + show the current config. + +Options: + --help Show this message and exit. +``` diff --git a/docs/cli/commands/cli-config.mdx b/docs/cli/commands/cli-config.mdx index 485611984..c5c7d8cf9 100644 --- a/docs/cli/commands/cli-config.mdx +++ b/docs/cli/commands/cli-config.mdx @@ -26,11 +26,12 @@ Usage: keep config [OPTIONS] COMMAND [ARGS]... ``` Usage: keep config [OPTIONS] COMMAND [ARGS]... - Set keep configuration. + Manage the config. Options: --help Show this message and exit. Commands: - provider Set the provider configuration. + new create new config. + show show the current config. ``` diff --git a/docs/cli/commands/mappings-create.mdx b/docs/cli/commands/mappings-create.mdx new file mode 100644 index 000000000..7cb711795 --- /dev/null +++ b/docs/cli/commands/mappings-create.mdx @@ -0,0 +1,75 @@ +--- +sidebarTitle: "keep mappings create" +--- + +Create a mapping rule. + +## Usage + +``` +Usage: keep mappings delete [OPTIONS] +``` + +## Options + +* `name` + * Type: STRING + * Default: `` + * Usage: `--name ` + + The name of the mapping. + +* `description` + * Type: STRING + * Default: `` + * Usage: `--description ` + + The description of the mapping. + +* `file` + * Type: STRING + * Default: `` + * Usage: `--file ` + + The mapping file. Must be a CSV file. + +* `matchers` + * Type: STRING + * Default: `` + * Usage: `--matchers ` + + The matchers of the mapping, as a comma-separated list of strings. + +* `priority` + * Type: INTEGER RANGE + * Default: `0` + * Usage: `--priority ` + + The priority of the mapping, higher priority means this rule will execute first. [0<=x<=100]. + +* `help`: + * Type: BOOL + * Default: `false` + * Usage: `--help` + + Show this message and exit. + +## CLI Help + +``` +Usage: keep mappings create [OPTIONS] + + Create a mapping rule. + +Options: + -n, --name TEXT The name of the mapping. [required] + -d, --description TEXT The description of the mapping. + -f, --file PATH The mapping file. Must be a CSV file. + [required] + -m, --matchers TEXT The matchers of the mapping, as a comma- + separated list of strings. [required] + -p, --priority INTEGER RANGE The priority of the mapping, higher priority + means this rule will execute first. + [0<=x<=100] + --help Show this message and exit. +``` diff --git a/docs/cli/commands/mappings-delete.mdx b/docs/cli/commands/mappings-delete.mdx new file mode 100644 index 000000000..fc54d57d7 --- /dev/null +++ b/docs/cli/commands/mappings-delete.mdx @@ -0,0 +1,41 @@ +--- +sidebarTitle: "keep mappings delete" +--- + +Delete a mapping with a specified ID. + +## Usage + +``` +Usage: keep mappings delete [OPTIONS] +``` + +## Options + +* `mapping-id` + * Type: STRING + * Default: `` + * Usage: `--mapping-id ` + + The ID of the mapping to delete. + +* `help`: + * Type: BOOL + * Default: `false` + * Usage: `--help` + + Show this message and exit. + + + +## CLI Help + +``` +Usage: keep mappings delete [OPTIONS] + + Delete a mapping with a specified ID + +Options: + --mapping-id INTEGER The ID of the mapping to delete. [required] + --help Show this message and exit. +``` diff --git a/docs/cli/commands/mappings-list.mdx b/docs/cli/commands/mappings-list.mdx new file mode 100644 index 000000000..79558047d --- /dev/null +++ b/docs/cli/commands/mappings-list.mdx @@ -0,0 +1,33 @@ +--- +sidebarTitle: "keep mappings list" +--- + +List mappings. + +## Usage + +``` +Usage: keep mappings [OPTIONS] +``` + +List mappings. + +## Options + +* `help`: + * Type: BOOL + * Default: `false` + * Usage: `--help` + + Show this message and exit. + +## CLI Help + +``` +Usage: keep mappings list [OPTIONS] + + List mappings. + +Options: + --help Show this message and exit. +``` diff --git a/docs/cli/installation.mdx b/docs/cli/installation.mdx index 469c46178..3020dd471 100644 --- a/docs/cli/installation.mdx +++ b/docs/cli/installation.mdx @@ -2,6 +2,14 @@ title: "Installation" --- Missing an installation? submit a new installation request and we will add it as soon as we can. + + +We recommend to install Keep CLI with Python version 3.11 for optimal compatibility and performance. +This choice ensures seamless integration with all dependencies, including pyarrow, which currently does not support Python 3.12 + + +Need Keep CLI on other versions? Feel free to contact us! + ## Clone and install (Option 1) ### Install diff --git a/docs/deployment/ecs.mdx b/docs/deployment/ecs.mdx new file mode 100644 index 000000000..097092e1a --- /dev/null +++ b/docs/deployment/ecs.mdx @@ -0,0 +1,154 @@ +--- +title: "AWS ECS" +sidebarTitle: "AWS ECS" +--- + +## Step 1: Login to AWS Console +- Open your web browser and navigate to the AWS Management Console. +- Log in using your AWS account credentials. + +## Step 2: Navigate to ECS +- Click on the "Services" dropdown menu in the top left corner. +- Select "ECS" from the list of services. + +## Step 3: Create 3 Task Definitions +- In the ECS dashboard, navigate to the "Task Definitions" section in the left sidebar. + Task Definition +- Click on "Create new Task Definition". + ![Create new task definition](/images/ecs-task-def-create-new.png) + + ### Task Definition 1 (Frontend - KeepUI): + + - Task Definition Family: keep-frontend + ![Task Definition Family](/images/ecs-task-def-frontend1.png) + - Configure your container definitions as below: + - Infrastructure Requirements: + - Launch Type: AWS Fargate + - OS, Architecture, Network mode: Linux/X86_64 + - Task Size: + - CPU: 1 vCPU + - Memory: 2 GB + - Task Role and Task Execution Role are optional if you plan on using secrets manager for example then create a task execution role to allow access to the secret manager you created. + ![Infrastructure Requirements](/images/ecs-task-def-frontend2.png) + - Container Details: + - Name: keep-frontend + - Image URI: us-central1-docker.pkg.dev/keephq/keep/keep-api:latest + - Ports Mapping: + - Container Port: 3000 + - Protocol: TCP + ![Container Details](/images/ecs-task-def-frontend3.png) + - Environment Variables: (This can be static or you can use parameter store or secrets manager) + - DATABASE_CONNECTION_STRING + - AUTH_TYPE + - KEEP_JWT_SECRET + - KEEP_DEFAULT_USERNAME + - KEEP_DEFAULT_PASSWORD + - SECRET_MANAGER_TYPE + - SECRET_MANAGER_DIRECTORY + - USE_NGROK + - KEEP_API_URL + (The below variable is optional if you don't want to use websocket) + - PUSHER_DISABLED + (The below variables are optional if you want to use websocket) + - PUSHER_APP_ID + - PUSHER_APP_KEY + - PUSHER_APP_SECRET + - PUSHER_HOST + - PUSHER_PORT + ![Environment Variables](/images/ecs-task-def-frontend4.png) + - Review and create your task definition. + + ### Task Definition 2 (Backend - keepAPI): + + - Configure your container definitions as below: + - Task Definition Family: keep-frontend + ![Task Definition Family](/images/ecs-task-def-backend1.png) + - Infrastructure Requirements: + - Launch Type: AWS Fargate + - OS, Architecture, Network mode: Linux/X86_64 + - Task Size: + - CPU: 1 vCPU + - Memory: 2 GB + - Task Role and Task Execution Role are optional if you plan on using secrets manager for example then create a task execution role to allow access to the secret manager you created. + ![Infrastructure Requirements](/images/ecs-task-def-backend2.png) + - Container Details: + - Name: keep-backend + - Image URI: us-central1-docker.pkg.dev/keephq/keep/keep-api:latest + - Ports Mapping: + - Container Port: 8080 + - Protocol: TCP + ![Container Details](/images/ecs-task-def-backend3.png) + - Environment Variables: (This can be static or you can use parameter store or secrets manager) + - DATABASE_CONNECTION_STRING + - AUTH_TYPE + - KEEP_JWT_SECRET + - KEEP_DEFAULT_USERNAME + - KEEP_DEFAULT_PASSWORD + - SECRET_MANAGER_TYPE + - SECRET_MANAGER_DIRECTORY + - USE_NGROK + - KEEP_API_URL + (The below variable is optional if you don't want to use websocket) + - PUSHER_DISABLED + (The below variables are optional if you want to use websocket) + - PUSHER_APP_ID + - PUSHER_APP_KEY + - PUSHER_APP_SECRET + - PUSHER_HOST + - PUSHER_PORT + ![Environment Variables](/images/ecs-task-def-backend4.png) + - Storage: + - Volume Name: keep-efs + - Configuration Type: Configure at task definition creation + - Volume type: EFS + - Storage configurations: + - File system ID: Select an exisiting EFS filesystem or create a new one + - Root Directory: / + ![Volume Configuration](/images/ecs-task-def-backend5.png) + - Container mount points: + - Container: select the container you just created + - Source volume: keep-efs + - Container path: /app + - Make sure that Readonly is not selected + ![Container Mount](/images/ecs-task-def-backend6.png) + - Review and create your task definition. + + ### Task Definition 3 (Websocket): (This step is optional if you want to have automatic refresh of the alerts feed) + + - Configure your container definitions as below: + - Task Definition Family: keep-frontend + ![Task Definition Family](/images/ecs-task-def-websocket1.png) + - Infrastructure Requirements: + - Launch Type: AWS Fargate + - OS, Architecture, Network mode: Linux/X86_64 + - Task Size: + - CPU: 0.25 vCPU + - Memory: 1 GB + - Task Role and Task Execution Role are optional if you plan on using secrets manager for example then create a task execution role to allow access to the secret manager you created. + ![Infrastructure Requirements](/images/ecs-task-def-websocket2.png) + - Container Details: + - Name: keep-websocket + - Image URI: quay.io/soketi/soketi:1.4-16-debian + - Ports Mapping: + - Container Port: 6001 + - Protocol: TCP + ![Container Details](/images/ecs-task-def-websocket3.png) + - Environment Variables: (This can be static or you can use parameter store or secrets manager) + - SOKETI_DEBUG + - SOKETI_DEFAULT_APP_ID + - SOKETI_DEFAULT_APP_KEY + - SOKETI_DEFAULT_APP_SECRET + - SOKETI_USER_AUTHENTICATION_TIMEOUT + ![Environment Variables](/images/ecs-task-def-websocket4.png) + - Review and create your task definition. + +## Step 4: Create Keep Service +- In the ECS dashboard, navigate to the "Clusters" section in the left sidebar. +- Select the cluster you want to deploy your service to. +- Click on the "Create" button next to "Services". +- Configure your service settings. +- Review and create your service. + +## Step 5: Monitor Your Service +- Once your service is created, monitor its status in the ECS dashboard. +- You can view task status, service events, and other metrics to ensure your service is running correctly. diff --git a/docs/images/azuremonitoring_1.png b/docs/images/azuremonitoring_1.png new file mode 100644 index 000000000..b9636b27f Binary files /dev/null and b/docs/images/azuremonitoring_1.png differ diff --git a/docs/images/azuremonitoring_2.png b/docs/images/azuremonitoring_2.png new file mode 100644 index 000000000..58b26d436 Binary files /dev/null and b/docs/images/azuremonitoring_2.png differ diff --git a/docs/images/azuremonitoring_3.png b/docs/images/azuremonitoring_3.png new file mode 100644 index 000000000..c76740efd Binary files /dev/null and b/docs/images/azuremonitoring_3.png differ diff --git a/docs/images/azuremonitoring_4.png b/docs/images/azuremonitoring_4.png new file mode 100644 index 000000000..b068c8a81 Binary files /dev/null and b/docs/images/azuremonitoring_4.png differ diff --git a/docs/images/azuremonitoring_5.png b/docs/images/azuremonitoring_5.png new file mode 100644 index 000000000..0935013c6 Binary files /dev/null and b/docs/images/azuremonitoring_5.png differ diff --git a/docs/images/azuremonitoring_6.png b/docs/images/azuremonitoring_6.png new file mode 100644 index 000000000..3ee7ab07b Binary files /dev/null and b/docs/images/azuremonitoring_6.png differ diff --git a/docs/images/azuremonitoring_7.png b/docs/images/azuremonitoring_7.png new file mode 100644 index 000000000..546a5a327 Binary files /dev/null and b/docs/images/azuremonitoring_7.png differ diff --git a/docs/images/ecs-task-def-backend1.png b/docs/images/ecs-task-def-backend1.png new file mode 100644 index 000000000..cd79c1429 Binary files /dev/null and b/docs/images/ecs-task-def-backend1.png differ diff --git a/docs/images/ecs-task-def-backend2.png b/docs/images/ecs-task-def-backend2.png new file mode 100644 index 000000000..e6ff04309 Binary files /dev/null and b/docs/images/ecs-task-def-backend2.png differ diff --git a/docs/images/ecs-task-def-backend3.png b/docs/images/ecs-task-def-backend3.png new file mode 100644 index 000000000..917fd2f1a Binary files /dev/null and b/docs/images/ecs-task-def-backend3.png differ diff --git a/docs/images/ecs-task-def-backend4.png b/docs/images/ecs-task-def-backend4.png new file mode 100644 index 000000000..ba7b8750b Binary files /dev/null and b/docs/images/ecs-task-def-backend4.png differ diff --git a/docs/images/ecs-task-def-backend5.png b/docs/images/ecs-task-def-backend5.png new file mode 100644 index 000000000..eaef8d56c Binary files /dev/null and b/docs/images/ecs-task-def-backend5.png differ diff --git a/docs/images/ecs-task-def-backend6.png b/docs/images/ecs-task-def-backend6.png new file mode 100644 index 000000000..e47b91ca7 Binary files /dev/null and b/docs/images/ecs-task-def-backend6.png differ diff --git a/docs/images/ecs-task-def-create-new.png b/docs/images/ecs-task-def-create-new.png new file mode 100644 index 000000000..9cfd0904a Binary files /dev/null and b/docs/images/ecs-task-def-create-new.png differ diff --git a/docs/images/ecs-task-def-create.png b/docs/images/ecs-task-def-create.png new file mode 100644 index 000000000..ef309433b Binary files /dev/null and b/docs/images/ecs-task-def-create.png differ diff --git a/docs/images/ecs-task-def-frontend1.png b/docs/images/ecs-task-def-frontend1.png new file mode 100644 index 000000000..2744431c4 Binary files /dev/null and b/docs/images/ecs-task-def-frontend1.png differ diff --git a/docs/images/ecs-task-def-frontend2.png b/docs/images/ecs-task-def-frontend2.png new file mode 100644 index 000000000..135d8b8ea Binary files /dev/null and b/docs/images/ecs-task-def-frontend2.png differ diff --git a/docs/images/ecs-task-def-frontend3.png b/docs/images/ecs-task-def-frontend3.png new file mode 100644 index 000000000..8500e73b4 Binary files /dev/null and b/docs/images/ecs-task-def-frontend3.png differ diff --git a/docs/images/ecs-task-def-frontend4.png b/docs/images/ecs-task-def-frontend4.png new file mode 100644 index 000000000..3402d3d66 Binary files /dev/null and b/docs/images/ecs-task-def-frontend4.png differ diff --git a/docs/images/ecs-task-def-websocket1.png b/docs/images/ecs-task-def-websocket1.png new file mode 100644 index 000000000..3e0fd65c9 Binary files /dev/null and b/docs/images/ecs-task-def-websocket1.png differ diff --git a/docs/images/ecs-task-def-websocket2.png b/docs/images/ecs-task-def-websocket2.png new file mode 100644 index 000000000..daaf65566 Binary files /dev/null and b/docs/images/ecs-task-def-websocket2.png differ diff --git a/docs/images/ecs-task-def-websocket3.png b/docs/images/ecs-task-def-websocket3.png new file mode 100644 index 000000000..a1c4e32aa Binary files /dev/null and b/docs/images/ecs-task-def-websocket3.png differ diff --git a/docs/images/ecs-task-def-websocket4.png b/docs/images/ecs-task-def-websocket4.png new file mode 100644 index 000000000..84fb54e1b Binary files /dev/null and b/docs/images/ecs-task-def-websocket4.png differ diff --git a/docs/images/gcpmonitoring_1.png b/docs/images/gcpmonitoring_1.png new file mode 100644 index 000000000..4bd027e11 Binary files /dev/null and b/docs/images/gcpmonitoring_1.png differ diff --git a/docs/images/gcpmonitoring_2.png b/docs/images/gcpmonitoring_2.png new file mode 100644 index 000000000..561213c40 Binary files /dev/null and b/docs/images/gcpmonitoring_2.png differ diff --git a/docs/images/gcpmonitoring_3.png b/docs/images/gcpmonitoring_3.png new file mode 100644 index 000000000..f56b62400 Binary files /dev/null and b/docs/images/gcpmonitoring_3.png differ diff --git a/docs/images/gcpmonitoring_4.png b/docs/images/gcpmonitoring_4.png new file mode 100644 index 000000000..b9bf21d51 Binary files /dev/null and b/docs/images/gcpmonitoring_4.png differ diff --git a/docs/images/gcpmonitoring_5.png b/docs/images/gcpmonitoring_5.png new file mode 100644 index 000000000..dc984d0bc Binary files /dev/null and b/docs/images/gcpmonitoring_5.png differ diff --git a/docs/images/gcpmonitoring_6.png b/docs/images/gcpmonitoring_6.png new file mode 100644 index 000000000..25dcbea49 Binary files /dev/null and b/docs/images/gcpmonitoring_6.png differ diff --git a/docs/images/presets/convert-to-cel.png b/docs/images/presets/convert-to-cel.png new file mode 100644 index 000000000..3d15417a1 Binary files /dev/null and b/docs/images/presets/convert-to-cel.png differ diff --git a/docs/images/presets/converted-sql-to-cel.png b/docs/images/presets/converted-sql-to-cel.png new file mode 100644 index 000000000..15d54b213 Binary files /dev/null and b/docs/images/presets/converted-sql-to-cel.png differ diff --git a/docs/images/presets/import-from-sql.png b/docs/images/presets/import-from-sql.png new file mode 100644 index 000000000..7ac2002b5 Binary files /dev/null and b/docs/images/presets/import-from-sql.png differ diff --git a/docs/images/presets/invalid-sentry-cel.png b/docs/images/presets/invalid-sentry-cel.png new file mode 100644 index 000000000..5803ac556 Binary files /dev/null and b/docs/images/presets/invalid-sentry-cel.png differ diff --git a/docs/images/presets/preset-created.png b/docs/images/presets/preset-created.png new file mode 100644 index 000000000..5a053239d Binary files /dev/null and b/docs/images/presets/preset-created.png differ diff --git a/docs/images/presets/save-preset-modal.png b/docs/images/presets/save-preset-modal.png new file mode 100644 index 000000000..6e85c1498 Binary files /dev/null and b/docs/images/presets/save-preset-modal.png differ diff --git a/docs/images/presets/save-preset.png b/docs/images/presets/save-preset.png new file mode 100644 index 000000000..6a83825a1 Binary files /dev/null and b/docs/images/presets/save-preset.png differ diff --git a/docs/images/presets/valid-sentry-cel.png b/docs/images/presets/valid-sentry-cel.png new file mode 100644 index 000000000..6e0e9a9a9 Binary files /dev/null and b/docs/images/presets/valid-sentry-cel.png differ diff --git a/docs/mint.json b/docs/mint.json index 877f3f58a..3268264a7 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -30,6 +30,7 @@ "overview/keyconcepts", "overview/usecases", "overview/ruleengine", + "overview/presets", { "group": "Enrichments", "pages": ["overview/enrichment/mapping"] @@ -53,7 +54,8 @@ "deployment/secret-manager", "deployment/docker", "deployment/kubernetes", - "deployment/openshift" + "deployment/openshift", + "deployment/ecs" ] }, { @@ -79,6 +81,7 @@ "pages": [ "providers/documentation/aks-provider", "providers/documentation/axiom-provider", + "providers/documentation/azuremonitoring-provider", "providers/documentation/cloudwatch-logs", "providers/documentation/cloudwatch-metrics", "providers/documentation/console-provider", @@ -87,6 +90,7 @@ "providers/documentation/kibana-provider", "providers/documentation/discord-provider", "providers/documentation/elastic-provider", + "providers/documentation/gcpmonitoring-provider", "providers/documentation/grafana-provider", "providers/documentation/grafana-oncall-provider", "providers/documentation/http-provider", @@ -255,6 +259,14 @@ } ] }, + { + "group": "keep mappings", + "pages": [ + "cli/commands/mappings-list", + "cli/commands/mappings-create", + "cli/commands/mappings-delete" + ] + }, "cli/commands/cli-api", "cli/commands/cli-config", "cli/commands/cli-version", diff --git a/docs/overview/presets.mdx b/docs/overview/presets.mdx new file mode 100644 index 000000000..3480ef040 --- /dev/null +++ b/docs/overview/presets.mdx @@ -0,0 +1,81 @@ +--- +description: "CEL-Based Alert Filtering" +title: "Presets" +--- + +With Keep's introduction of CEL (Common Expression Language) for alert filtering, users gain the flexibility to define more complex and precise alert filtering logic. This feature allows the creation of customizable filters using CEL expressions to refine alert visibility based on specific criteria. + +## Introduction + +CEL-based filtering offers a powerful method for users to specify conditions under which alerts should be shown. Through a combination of logical, comparison, and string operations, alerts can be filtered to meet the exact needs of the user, improving the focus and efficiency of alert management. + +## How It Works + +1. **CEL Expression Creation**: Users craft CEL expressions that define the filtering criteria for alerts. +2. **Preset Definition**: These expressions can be saved as presets for easy application to different alert streams. +3. **Alert Filtering**: When applied, the CEL expressions evaluate each alert against the defined criteria, filtering the alert stream in real-time. + +## Practical Example + +For instance, a user could create a CEL expression to filter alerts by severity and source, such as `severity == 'critical' && service.contains('database')`, ensuring only critical alerts from database services are displayed. + +## Core Concepts + +- **CEL Expressions**: The CEL language syntax used to define alert filtering logic. +- **Presets**: Saved CEL expressions that can be reused across different alert streams. +- **Real-Time Filtering**: The dynamic application of CEL expressions to incoming alerts. + +## Creating a CEL Expression + +There is generally two ways of creating a CEL expression in Keep +### Importing from an SQL query + +1. Click on the "Import from SQL" button + + + +2. Write/Paste your SQL query and hit the "Convert to CEL" button + + + +Which in turn will generate and apply a valid CEL query: + + + + +### Manually creating CEL query + +Use the [CEL Language Definition](https://github.com/google/cel-spec/blob/master/doc/langdef.md) documentation to better understand the capabilities of the Common Expression Language +This is an example of how to query all the alerts that came from `Sentry` + + + +If the CEL syntax you typed in is invalid, an error message will show up (in this case, we used invalid `''` instead of `""`): + + + + +## Save Presets + +You can save your CEL queries into a `Preset` using the "Save current filter as a view" button + + + +You can name your `Preset` and configure whether it is "Private" (only the creating user will see this Preset) or account-wide available. + + + +The `Preset` will then be created and available for you to quickly navigate and used + + + + +## Best Practices + +- **Specificity in Expressions**: Craft expressions that precisely target the desired alerts to avoid filtering out relevant alerts. +- **Presets Management**: Regularly review and update your presets to align with evolving alerting needs. +- **Testing Expressions**: Before applying, test CEL expressions to ensure they correctly filter the desired alerts. + +## Useful Links +- [Common Expression Language](https://github.com/google/cel-spec?tab=readme-ov-file) +- [CEL Language Definition](https://github.com/google/cel-spec/blob/master/doc/langdef.md) diff --git a/docs/providers/documentation/azuremonitoring-provider.mdx b/docs/providers/documentation/azuremonitoring-provider.mdx new file mode 100644 index 000000000..f47a54be8 --- /dev/null +++ b/docs/providers/documentation/azuremonitoring-provider.mdx @@ -0,0 +1,78 @@ +--- +title: "Azure Monitoring" +sidebarTitle: "Azure Monitoring Provider" +description: "Azure Monitoring provider allows you to get alerts from Azure Monitoring via webhooks." +--- + +## Overview + +The Azure Monitoring Provider integrates Keep with Azure Monitoring, allowing you to receive alerts within Keep's platform. By setting up a webhook in Azure, you can ensure that critical alerts are sent to Keep, allowing for efficient monitoring and response. + +## Connecting Azure Monitoring to Keep + +Connecting Azure Monitoring to Keep involves creating an Action Group in Azure, adding a webhook action, and configuring the Alert Rule to use the new Action Group. + +### Step 1: Navigate an Action Group +1. Log in to your Azure portal. +2. Navigate to **Monitor** > **Alerts** > **Action groups**. + + + + + +### Step 2: Create new Action Group +1. Click on **+ Create**. + + + + + + +### Step 3: Fill Action Group details +1. Choose the Subscription and Resource Group. +2. Give the Action Group an indicative name. + + + + + +### Step 4: Go to "Action" and add Keep as a Webhook + + + + + +### Step 5: Test Keep Webhook action + + + + + + + + + +### Step 6: View the alert in Keep + + + + + +## Useful Links +- [Azure Monitor alert webhook](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-webhooks) +- [Azure Monitor alert payload](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-payload-samples) +- [Azure Monitor action groups](https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/action-groups) diff --git a/docs/providers/documentation/gcpmonitoring-provider.mdx b/docs/providers/documentation/gcpmonitoring-provider.mdx new file mode 100644 index 000000000..8a31e79c0 --- /dev/null +++ b/docs/providers/documentation/gcpmonitoring-provider.mdx @@ -0,0 +1,78 @@ +--- +title: "GCP Monitoring" +sidebarTitle: "GCP Monitoring Provider" +description: "GCP Monitoringing provider allows you to get alerts from Azure Monitoring via webhooks." +--- + +## Overview +The GCP Monitoring Provider enables seamless integration between Keep and GCP Monitoring, allowing alerts from GCP Monitoring to be directly sent to Keep through webhook configurations. This integration ensures that critical alerts are efficiently managed and responded to within Keep's platform. + +## Connecting GCP Monitoring to Keep +To connect GCP Monitoring to Keep, you'll need to configure a webhook as a notification channel in GCP Monitoring and then link it to the desired alert policy. + +### Step 1: Access Notification Channels +Log in to the Google Cloud Platform console. +Navigate to **Monitoring > Alerting > Notification channels**. + + + + + +### Step 2: Add a New Webhook +Within the Webhooks section, click on **ADD NEW**. + + + + + +### Step 3: Configure the Webhook +In the Endpoint URL field, enter the webhook URL provided by Keep. +- For Display Name, use keep-gcpmonitoring-webhook-integration. +- Enable Use HTTP Basic Auth and input the following credentials: + - Auth Username: **api_key** + - Auth Password: **%YOURAPIKEY%** + + + + + +### Step 4: Save the Webhook Configuration +- Click on Save to store the webhook configuration. + +### Step 5: Associate the Webhook with an Alert Policy + +Navigate to the alert policy you wish to send notifications from to Keep. +- Click on Edit. +- Under "Notifications and name," find the Notification Channels section and select the keep-gcpmonitoring-webhook-integration channel you created. +- Save the changes by clicking on SAVE POLICY. + + + + + + + + + + +### Step 6: Review the alert in Keep + + + + + +### Useful Links + - [GCP Monitoring Notification Channels](https://cloud.google.com/monitoring/support/notification-options) + - [GCP Monitoring Alerting](https://cloud.google.com/monitoring/alerts) diff --git a/docs/providers/documentation/sentry-provider.mdx b/docs/providers/documentation/sentry-provider.mdx index 09bfd7575..cf1f80fcd 100644 --- a/docs/providers/documentation/sentry-provider.mdx +++ b/docs/providers/documentation/sentry-provider.mdx @@ -15,6 +15,10 @@ The `api_key` and `organization_slug` are required for connecting to the Sentry `project_slug` is if you want to connect Sentry to a specific project within an organization. + +To connect self hosted Sentry, you need to set the `api_url` parameter. Default value is `https://sentry.io/api/0/`. + + ## Connecting with the Provider ### API Key diff --git a/docs/providers/documentation/splunk-provider.mdx b/docs/providers/documentation/splunk-provider.mdx new file mode 100644 index 000000000..88956cd8b --- /dev/null +++ b/docs/providers/documentation/splunk-provider.mdx @@ -0,0 +1,37 @@ +--- +title: "Splunk" +sidebarTitle: "Splunk Provider" +description: "Splunk provider allows you to get Splunk `saved searches` via webhook installation" +--- + +## Authentication Parameters +The Splunk provider requires the following authentication parameter: + +- `Splunk UseAPI Key`: Required. This is your Splunk account username, which you use to log in to the Splunk platform. +- `Host`: This is the hostname or IP address of the Splunk instance you wish to connect to. It identifies the Splunk server that the API will interact with. +- `Port`: This is the network port on the Splunk server that is listening for API connections. The default port for Splunk's management API is typically 8089. +- `` + +## Connecting with the Provider + +Obtain Splunk API Token: +1. Ensure you have a Splunk account with the necessary [permissions](https://docs.splunk.com/Documentation/Splunk/9.2.0/Security/Rolesandcapabilities). The basic permissions required are `list_all_objects` & `edit_own_objects`. +2. Get an API token for authenticating API requests. [Read More](https://docs.splunk.com/Documentation/Splunk/9.2.0/Security/Setupauthenticationwithtokens) on how to set up and get API Keys. + +Identify Your Splunk Instance Details: +1. Determine the Host (IP address or hostname) and Port (default is 8089 for Splunk's management API) of the Splunk instance you wish to connect to. + +--- +**NOTE** +Make sure to follow this [Guide](https://docs.splunk.com/Documentation/Splunk/9.2.0/Alert/ConfigureWebhookAllowList) to configure your webhook allow list to allow your `keep` deployment. +--- + + +## Useful Links + +- [Splunk Python SDK](https://dev.splunk.com/view/python-sdk/SP-CAAAEBB) +- [Splunk Webhook](https://docs.splunk.com/Documentation/Splunk/9.2.0/Alert/Webhooks) +- [Splunk Webhook Allow List](https://docs.splunk.com/Documentation/Splunk/9.2.0/Alert/ConfigureWebhookAllowList) +- [Splunk Permissions and Roles](https://docs.splunk.com/Documentation/Splunk/9.2.0/Security/Rolesandcapabilities) +- [Splunk API tokens](https://docs.splunk.com/Documentation/Splunk/9.2.0/Security/Setupauthenticationwithtokens) + diff --git a/docs/providers/documentation/squadcast-provider.mdx b/docs/providers/documentation/squadcast-provider.mdx index 35b7190c9..26cc7e2c6 100644 --- a/docs/providers/documentation/squadcast-provider.mdx +++ b/docs/providers/documentation/squadcast-provider.mdx @@ -15,6 +15,7 @@ The `notify` function take following parameters as inputs: - `priority` (optional): Priority of the incident. - `status` (optional): Status of the event. - `event_id` (optional): event_id is used to resolve an incident + - `additional_json` (optional): Additional JSON data to be sent with the incident. 2. ##### parametres for `notes` - `message` (required): The message of the note. - `incident_id` (required): Id of the incident where the Note has to be created. diff --git a/docs/workflows/functions/last.mdx b/docs/workflows/functions/last.mdx new file mode 100644 index 000000000..a3ec04cee --- /dev/null +++ b/docs/workflows/functions/last.mdx @@ -0,0 +1,27 @@ +--- +title: "last(iterable)" +sidebarTitle: "last" +--- + +### Input + +An iterable. + +### Output + +The last item of the iterable. + +### Example + +```yaml +actions: + - name: keep-slack + foreach: "{{steps.this.results}}" + condition: + - type: threshold + value: "keep.last(keep.split({{ foreach.value }}, ' '))" + # each line looks like: + # '2023-02-09 20:08:16,773 INFO: uvicorn.access -: 127.0.0.1:53948 - "GET /test2 HTTP/1.1" 503' + # where the "503" is the number of the + compare_to: 200 +``` diff --git a/docs/workflows/functions/lowercase.mdx b/docs/workflows/functions/lowercase.mdx new file mode 100644 index 000000000..e945f4b62 --- /dev/null +++ b/docs/workflows/functions/lowercase.mdx @@ -0,0 +1,24 @@ +--- +title: "string(string)" +sidebarTitle: "lowercase" +--- + +### Input + +A string. + +### Output + +Returns the string which is lowercased. + +### Example + +```yaml +actions: + - name: trigger-slack + condition: + - type: equals + value: keep.lowercase('ABC DEF') + compare_to: "abc def" + compare_type: eq +``` diff --git a/docs/workflows/functions/uppercase.mdx b/docs/workflows/functions/uppercase.mdx new file mode 100644 index 000000000..45f3f6672 --- /dev/null +++ b/docs/workflows/functions/uppercase.mdx @@ -0,0 +1,24 @@ +--- +title: "string(string)" +sidebarTitle: "uppercase" +--- + +### Input + +A string. + +### Output + +Returns the string which is uppercased. + +### Example + +```yaml +actions: + - name: trigger-slack + condition: + - type: equals + value: keep.uppercase('abc def') + compare_to: "ABC DEF" + compare_type: eq +``` diff --git a/examples/workflows/autosupress.yml b/examples/workflows/autosupress.yml new file mode 100644 index 000000000..d9952098e --- /dev/null +++ b/examples/workflows/autosupress.yml @@ -0,0 +1,16 @@ +workflow: + id: autosupress + description: demonstrates how to automatically suppress alerts + triggers: + - type: alert + filters: + - key: name + value: r"(somename)" + actions: + - name: dismiss-alert + provider: + type: mock + with: + enrich_alert: + - key: dismissed + value: "true" diff --git a/examples/workflows/squadcast_example.yml b/examples/workflows/squadcast_example.yml new file mode 100644 index 000000000..5849c5e3e --- /dev/null +++ b/examples/workflows/squadcast_example.yml @@ -0,0 +1,15 @@ +workflow: + id: squadcast + description: squadcast + triggers: + - type: alert + actions: + - name: create-incident + provider: + config: "{{ providers.squadcast }}" + type: squadcast + with: + additional_json: '{{ alert }}' + description: TEST + message: '{{ alert.name }}-test' + notify_type: incident diff --git a/keep-ui/app/alerts/alert-actions.tsx b/keep-ui/app/alerts/alert-actions.tsx index 81a1b53a2..071aff98d 100644 --- a/keep-ui/app/alerts/alert-actions.tsx +++ b/keep-ui/app/alerts/alert-actions.tsx @@ -7,7 +7,7 @@ import { useAlerts } from "utils/hooks/useAlerts"; import { PlusIcon } from "@radix-ui/react-icons"; import { toast } from "react-toastify"; import { usePresets } from "utils/hooks/usePresets"; -import { usePathname, useRouter } from "next/navigation"; +import { useRouter } from "next/navigation"; interface Props { selectedRowIds: string[]; @@ -20,7 +20,6 @@ export default function AlertActions({ alerts, clearRowSelection, }: Props) { - const pathname = usePathname(); const router = useRouter(); const { useAllAlerts } = useAlerts(); const { mutate } = useAllAlerts({ revalidateOnFocus: false }); @@ -96,9 +95,17 @@ export default function AlertActions({ const distinctAlertNames = Array.from( new Set(selectedAlerts.map((alert) => alert.name)) ); - const options = distinctAlertNames.map((name) => { - return { value: `name=${name}`, label: `name=${name}` }; - }); + const formattedCel = distinctAlertNames.reduce( + (accumulator, currentValue, currentIndex) => { + return ( + accumulator + + (currentIndex > 0 ? " || " : "") + + `name == "${currentValue}"` + ); + }, + "" + ); + const options = [{ value: formattedCel, label: "CEL" }]; const session = await getSession(); const apiUrl = getApiURL(); const response = await fetch(`${apiUrl}/preset`, { diff --git a/keep-ui/app/alerts/alert-history.tsx b/keep-ui/app/alerts/alert-history.tsx index d4c33ed8d..331e2d152 100644 --- a/keep-ui/app/alerts/alert-history.tsx +++ b/keep-ui/app/alerts/alert-history.tsx @@ -36,7 +36,8 @@ const AlertHistoryPanel = ({ return (

loading>; isLoading: boolean; + table: Table; } -export default function AlertPresets({ - preset, - alerts, - selectedOptions, - setSelectedOptions, - isLoading, -}: Props) { +export default function AlertPresets({ preset, isLoading, table }: Props) { const apiUrl = getApiURL(); const { useAllPresets } = usePresets(); - const { mutate: presetsMutator } = useAllPresets({ + const { mutate: presetsMutator, data: savedPresets = [] } = useAllPresets({ revalidateOnFocus: false, }); const { data: session } = useSession(); const router = useRouter(); - const selectRef = useRef(null); - const [options, setOptions] = useState([]); - const [inputValue, setInputValue] = useState(""); - const [isMenuOpen, setIsMenuOpen] = useState(false); - const uniqueValuesMap = useMemo(() => { - const newUniqueValuesMap = new Map>(); - if (alerts) { - // Populating the map with keys and values - alerts.forEach((alert) => { - Object.entries(alert).forEach(([key, value]) => { - if (typeof value !== "string" && key !== "source") return; - if (!newUniqueValuesMap.has(key)) { - newUniqueValuesMap.set(key, new Set()); - } - if (key === "source") { - value = value?.join(","); - } - if (!newUniqueValuesMap.get(key)?.has(value?.trim())) - newUniqueValuesMap.get(key)?.add(value?.toString().trim()); - }); - }); - } - return newUniqueValuesMap; - }, [alerts]); - - // Initially, set options to keys - useEffect(() => { - setOptions( - Array.from(uniqueValuesMap.keys()).map((key) => ({ - label: key, - value: key, - })) - ); - }, [uniqueValuesMap]); - - const isValidNewOption = () => { - // Only allow creating new options if the input includes '=' - return inputValue.includes("="); - }; - - // Handler for key down events - const handleKeyDown = (event: React.KeyboardEvent) => { - const inputElement = event.target as HTMLInputElement; // Cast to HTMLInputElement - - if (event.key === "Enter") { - if (!inputElement.value.includes("=")) { - event.preventDefault(); - } - } - - if (event.key === "Tab") { - event.preventDefault(); - // Only add to selectedOptions if focusedOption is not null - const select = selectRef.current as any; - if (select?.state.focusedOption) { - const value = select.state.focusedOption.value; - if (value.includes("=")) { - handleInputChange(select.state.focusedOption.value); - } else { - handleInputChange(`${value}=`); - } - } - } - }; - - const handleChange = (selected: any, actionMeta: any) => { - if ( - actionMeta.action === "select-option" && - selected.some((option: any) => !option.value.includes("=")) - ) { - // Handle invalid option selection - handleInputChange(`${actionMeta.option.value}=`); - // Optionally, you can prevent the selection or handle it differently - } else { - setSelectedOptions(selected); - setIsMenuOpen(false); - } - }; - - const handleInputChange = (inputValue: string) => { - setInputValue(inputValue); - if (inputValue.includes("=")) { - const [inputKey, inputValuePart] = inputValue.split("="); - if (uniqueValuesMap.has(inputKey)) { - const filteredValues = Array.from( - uniqueValuesMap.get(inputKey) || [] - ).filter((value) => value?.startsWith(inputValuePart)); - const newOptions = filteredValues.map((value) => ({ - label: `${inputKey}=${value}`, - value: `${inputKey}=${value}`, - })); - setOptions(newOptions); - } else { - setOptions([]); - } - } else { - setOptions( - Array.from(uniqueValuesMap.keys()).map((key) => ({ - label: key, - value: key, - })) - ); - } - }; + const [isModalOpen, setIsModalOpen] = useState(false); + const [presetName, setPresetName] = useState( + preset?.name === "feed" || preset?.name === "deleted" ? "" : preset?.name + ); + const [isPrivate, setIsPrivate] = useState(preset?.is_private); + const [presetCEL, setPresetCEL] = useState(""); - const filterOption = ({ label }: Option, input: string) => { - return label.toLowerCase().includes(input.toLowerCase()); - }; + const selectedPreset = savedPresets.find( + (savedPreset) => + savedPreset.name.toLowerCase() === + decodeURIComponent(preset!.name).toLowerCase() + ) as Preset | undefined; async function deletePreset(presetId: string) { if ( @@ -163,111 +54,121 @@ export default function AlertPresets({ type: "success", }); presetsMutator(); + router.push("/alerts/feed"); } } } async function addOrUpdatePreset() { - const newPresetName = prompt( - `${preset?.name ? "Update preset name?" : "Enter new preset name"}`, - preset?.name === "feed" || preset?.name === "deleted" ? "" : preset?.name - ); - if (newPresetName) { - const options = selectedOptions.map((option) => { - return { - value: option.value, - label: option.label, - }; - }); + if (presetName) { const response = await fetch( - preset?.id ? `${apiUrl}/preset/${preset?.id}` : `${apiUrl}/preset`, + selectedPreset?.id + ? `${apiUrl}/preset/${selectedPreset?.id}` + : `${apiUrl}/preset`, { - method: preset?.id ? "PUT" : "POST", + method: selectedPreset?.id ? "PUT" : "POST", headers: { Authorization: `Bearer ${session?.accessToken}`, "Content-Type": "application/json", }, - body: JSON.stringify({ name: newPresetName, options: options }), + body: JSON.stringify({ + name: presetName, + options: [ + { + label: "CEL", + value: presetCEL, + }, + ], + is_private: isPrivate, + }), } ); if (response.ok) { + setIsModalOpen(false); toast( - preset?.name - ? `Preset ${newPresetName} updated!` - : `Preset ${newPresetName} created!`, + selectedPreset?.name + ? `Preset ${presetName} updated!` + : `Preset ${presetName} created!`, { position: "top-left", type: "success", } ); await presetsMutator(); - router.push(`/alerts/${newPresetName.toLowerCase()}`); + router.push(`/alerts/${presetName.toLowerCase()}`); } } } return ( <> - Filters -
- setIsMenuOpen(true)} - onBlur={() => setIsMenuOpen(false)} - isClearable={false} - isDisabled={isLoading} - /> - {preset?.name === "feed" && ( - - )} - {preset?.name !== "deleted" && preset?.name !== "feed" && ( -
+ setIsModalOpen(false)} + className="w-[30%] max-w-screen-2xl max-h-[710px] transform overflow-auto ring-tremor bg-white p-6 text-left align-middle shadow-tremor transition-all rounded-xl" + > +
+
+

+ {presetName ? "Update preset name?" : "Enter new preset name"} +

+
+ +
+ setPresetName(e.target.value)} + className="w-full" + /> +
+ +
+ setIsPrivate(!isPrivate)} + /> + +
+ +
- )} +
+
+
+
); diff --git a/keep-ui/app/alerts/alert-table-tab-panel.tsx b/keep-ui/app/alerts/alert-table-tab-panel.tsx index 72d0ea6d7..1db678c6c 100644 --- a/keep-ui/app/alerts/alert-table-tab-panel.tsx +++ b/keep-ui/app/alerts/alert-table-tab-panel.tsx @@ -1,10 +1,6 @@ -import { useState } from "react"; -import { RowSelectionState } from "@tanstack/react-table"; -import AlertPresets, { Option } from "./alert-presets"; import { AlertTable } from "./alert-table"; import { useAlertTableCols } from "./alert-table-utils"; import { AlertDto, AlertKnownKeys, Preset } from "./models"; -import AlertActions from "./alert-actions"; const getPresetAlerts = (alert: AlertDto, presetName: string): boolean => { if (presetName === "deleted") { @@ -26,40 +22,6 @@ const getPresetAlerts = (alert: AlertDto, presetName: string): boolean => { return true; }; -const getOptionAlerts = (alert: AlertDto, options: Option[]): boolean => - options.length > 0 - ? options.some((option) => { - const [key, value] = option.value.split("="); - - if (key && value) { - const attribute = key.toLowerCase() as keyof AlertDto; - const lowercaseAttributeValue = value.toLowerCase(); - - const alertAttributeValue = alert[attribute]; - - if (Array.isArray(alertAttributeValue)) { - return alertAttributeValue.every((v) => - lowercaseAttributeValue.split(",").includes(v) - ); - } - - if (typeof alertAttributeValue === "string") { - return alertAttributeValue - .toLowerCase() - .includes(lowercaseAttributeValue); - } - } - - return false; - }) - : true; - -const getPresetAndOptionsAlerts = ( - alert: AlertDto, - options: Option[], - presetName: string -) => getPresetAlerts(alert, presetName) && getOptionAlerts(alert, options); - interface Props { alerts: AlertDto[]; preset: Preset; @@ -79,25 +41,8 @@ export default function AlertTableTabPanel({ setRunWorkflowModalAlert, setDismissModalAlert, }: Props) { - const [selectedOptions, setSelectedOptions] = useState( - preset.options - ); - - const [rowSelection, setRowSelection] = useState({}); - const selectedRowIds = Object.entries(rowSelection).reduce( - (acc, [alertId, isSelected]) => { - if (isSelected) { - return acc.concat(alertId); - } - return acc; - }, - [] - ); - const sortedPresetAlerts = alerts - .filter((alert) => - getPresetAndOptionsAlerts(alert, selectedOptions, preset.name) - ) + .filter((alert) => getPresetAlerts(alert, preset.name)) .sort((a, b) => b.lastReceived.getTime() - a.lastReceived.getTime()); const additionalColsToGenerate = [ @@ -121,29 +66,11 @@ export default function AlertTableTabPanel({ }); return ( - <> - {selectedRowIds.length ? ( - setRowSelection({})} - /> - ) : ( - - )} - - + ); } diff --git a/keep-ui/app/alerts/alert-table-utils.tsx b/keep-ui/app/alerts/alert-table-utils.tsx index ccfb73843..19dd5a761 100644 --- a/keep-ui/app/alerts/alert-table-utils.tsx +++ b/keep-ui/app/alerts/alert-table-utils.tsx @@ -258,7 +258,7 @@ export const useAlertTableCols = ( columnHelper.display({ id: "alertMenu", meta: { - tdClassName: "flex justify-end", + tdClassName: "sticky right-0", }, size: 50, cell: (context) => ( diff --git a/keep-ui/app/alerts/alert-table.tsx b/keep-ui/app/alerts/alert-table.tsx index d5fb290e3..5dfe1ceff 100644 --- a/keep-ui/app/alerts/alert-table.tsx +++ b/keep-ui/app/alerts/alert-table.tsx @@ -3,8 +3,6 @@ import { AlertsTableBody } from "./alerts-table-body"; import { AlertDto } from "./models"; import { CircleStackIcon } from "@heroicons/react/24/outline"; import { - OnChangeFn, - RowSelectionState, getCoreRowModel, useReactTable, getPaginationRowModel, @@ -14,6 +12,7 @@ import { ColumnSizingState, getFilteredRowModel, } from "@tanstack/react-table"; + import AlertPagination from "./alert-pagination"; import AlertColumnsSelect from "./alert-columns-select"; import AlertsTableHeaders from "./alert-table-headers"; @@ -24,6 +23,9 @@ import { DEFAULT_COLS_VISIBILITY, DEFAULT_COLS, } from "./alert-table-utils"; +import AlertActions from "./alert-actions"; +import AlertPresets from "./alert-presets"; +import { evalWithContext } from "./alerts-rules-builder"; interface Props { alerts: AlertDto[]; @@ -32,10 +34,6 @@ interface Props { presetName: string; isMenuColDisplayed?: boolean; isRefreshAllowed?: boolean; - rowSelection?: { - state: RowSelectionState; - onChange: OnChangeFn; - }; } export function AlertTable({ @@ -43,7 +41,6 @@ export function AlertTable({ columns, isAsyncLoading = false, presetName, - rowSelection, isRefreshAllowed = true, }: Props) { const columnsIds = getColumnsIds(columns); @@ -70,7 +67,6 @@ export function AlertTable({ columnVisibility: getOnlyVisibleCols(columnVisibility, columnsIds), columnOrder: columnOrder, columnSizing: columnSizing, - rowSelection: rowSelection?.state, columnPinning: { left: ["checkbox"], right: ["alertMenu"], @@ -79,19 +75,40 @@ export function AlertTable({ initialState: { pagination: { pageSize: 10 }, }, + globalFilterFn: ({ original }, _id, value) => { + return evalWithContext(original, value); + }, getCoreRowModel: getCoreRowModel(), getFilteredRowModel: getFilteredRowModel(), getPaginationRowModel: getPaginationRowModel(), - enableRowSelection: rowSelection !== undefined, - onRowSelectionChange: rowSelection?.onChange, onColumnSizingChange: setColumnSizing, enableColumnPinning: true, columnResizeMode: "onChange", autoResetPageIndex: false, + enableGlobalFilter: true, }); + const selectedRowIds = Object.entries( + table.getSelectedRowModel().rowsById + ).reduce((acc, [alertId]) => { + return acc.concat(alertId); + }, []); + return ( <> + {selectedRowIds.length ? ( + + ) : ( + + )} {isAsyncLoading && ( + // make sure string is a String, and make sure pattern has the /g flag + String(string).match(new RegExp(pattern, "g")); + +const sanitizeCELIntoJS = (celExpression: string): string => { + // First, replace "contains" with "includes" + let jsExpression = celExpression.replace(/contains/g, "includes"); + // Replace severity comparisons with mapped values + jsExpression = jsExpression.replace( + /severity\s*([<>=]+)\s*(\d)/g, + (match, operator, number) => { + const severityValue = severityMapping[number]; + if (!severityValue) { + return match; // If no mapping found, return the original match + } + + // For equality, directly replace with the severity level + if (operator === "==") { + return `severity == "${severityValue}"`; + } + + // For greater than or less than, include multiple levels based on the mapping + const levels = Object.entries(severityMapping); + let replacement = ""; + if (operator === ">") { + const filteredLevels = levels + .filter(([key]) => key > number) + .map(([, value]) => `severity == "${value}"`); + replacement = filteredLevels.join(" || "); + } else if (operator === "<") { + const filteredLevels = levels + .filter(([key]) => key < number) + .map(([, value]) => `severity == "${value}"`); + replacement = filteredLevels.join(" || "); + } + + return `(${replacement})`; + } + ); + + // Convert 'in' syntax to '.includes()' + jsExpression = jsExpression.replace( + /(\w+)\s+in\s+\[([^\]]+)\]/g, + (match, variable, list) => { + // Split the list by commas, trim spaces, and wrap items in quotes if not already done + const items = list + .split(",") + .map((item: string) => item.trim().replace(/^([^"]*)$/, '"$1"')); + return `[${items.join(", ")}].includes(${variable})`; + } + ); + + return jsExpression; +}; + +// this pattern is far from robust +const variablePattern = /[a-zA-Z$_][0-9a-zA-Z$_]*/; + +export const evalWithContext = (context: AlertDto, celExpression: string) => { + try { + if (celExpression.length === 0) { + return new Function(); + } + + const jsExpression = sanitizeCELIntoJS(celExpression); + const variables = ( + getAllMatches(variablePattern, jsExpression) ?? [] + ).filter((variable) => variable !== "true" && variable !== "false"); + + const func = new Function(...variables, `return (${jsExpression})`); + + const args = variables.map((arg) => + Object.hasOwnProperty.call(context, arg) + ? context[arg as keyof AlertDto] + : undefined + ); + + return func(...args); + } catch (error) { + return; + } +}; + +const getOperators = (id: string): Operator[] => { + if (id === "source") { + return [ + { name: "contains", label: "contains" }, + { name: "null", label: "null" }, + ]; + } + + return defaultOperators; +}; + +type AlertsRulesBuilderProps = { + table: Table; + selectedPreset?: Preset; + defaultQuery: string | undefined; + setIsModalOpen: React.Dispatch>; + deletePreset: (presetId: string) => Promise; + setPresetCEL: React.Dispatch>; +}; + +const SQL_QUERY_PLACEHOLDER = `SELECT * +FROM alerts +WHERE severity = 'critical' and status = 'firing'`; + +export const AlertsRulesBuilder = ({ + table, + selectedPreset, + defaultQuery = "", + setIsModalOpen, + deletePreset, + setPresetCEL, +}: AlertsRulesBuilderProps) => { + const [isGUIOpen, setIsGUIOpen] = useState(false); + const [isImportSQLOpen, setImportSQLOpen] = useState(false); + const [sqlQuery, setSQLQuery] = useState(""); + const [celRules, setCELRules] = useState(defaultQuery); + + const parsedCELRulesToQuery = parseCEL(celRules); + const [query, setQuery] = useState(parsedCELRulesToQuery); + const [isValidCEL, setIsValidCEL] = useState(true); + const [sqlError, setSqlError] = useState(null); + + const textAreaRef = useRef(null); + + const isFirstRender = useRef(true); + + const constructCELRules = (preset?: Preset) => { + // Check if selectedPreset is defined and has options + if (preset && preset.options) { + // New version: single "CEL" key + const celOption = preset.options.find((option) => option.label === "CEL"); + if (celOption) { + return celOption.value; + } + // Older version: Concatenate multiple fields + else { + return preset.options + .map((option) => { + // Assuming the older format is exactly "x='y'" (x equals y) + // We split the string by '=', then trim and quote the value part + let [key, value] = option.value.split("="); + // Trim spaces and single quotes (if any) from the value + value = value.trim().replace(/^'(.*)'$/, "$1"); + // Return the correctly formatted CEL expression + return `${key.trim()}=="${value}"`; + }) + .join(" && "); + } + } + return ""; // Default to empty string if no preset or options are found + }; + + useEffect(() => { + // Use the constructCELRules function to set the initial value of celRules + const initialCELRules = constructCELRules(selectedPreset); + setCELRules(initialCELRules); + }, [selectedPreset]); + + useEffect(() => { + // This effect waits for celRules to update and applies the filter only on the initial render + if (isFirstRender.current && celRules.length > 0) { + onApplyFilter(); + isFirstRender.current = false; + } else if (!selectedPreset) { + isFirstRender.current = false; + } + // This effect should only run when celRules updates and on initial render + }, [celRules]); + + // Adjust the height of the textarea based on its content + const adjustTextAreaHeight = () => { + const textArea = textAreaRef.current; + if (textArea) { + textArea.style.height = "auto"; + textArea.style.height = `${textArea.scrollHeight}px`; + } + }; + // Adjust the height whenever the content changes + useEffect(() => { + adjustTextAreaHeight(); + }, [celRules]); + + const handleClearInput = () => { + setCELRules(""); + table.resetGlobalFilter(); + setIsValidCEL(true); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); // Prevents the default action of Enter key in a form + // You can now use `target` which is asserted to be an HTMLTextAreaElement + + // check if the CEL is valid by comparing the parsed query with the original CEL + // remove spaces so that "a && b" is the same as "a&&b" + const celQuery = formatQuery(parsedCELRulesToQuery, "cel"); + const isValidCEL = + celQuery.replace(/\s+/g, "") === celRules.replace(/\s+/g, "") || + celRules === ""; + setIsValidCEL(isValidCEL); + if (isValidCEL) { + onApplyFilter(); + } + } + }; + + const onApplyFilter = () => { + if (celRules.length === 0) { + return table.resetGlobalFilter(); + } + + return table.setGlobalFilter(celRules); + }; + + const onGenerateQuery = () => { + setCELRules(formatQuery(query, "cel")); + setIsGUIOpen(false); + }; + + const fields: Field[] = table + .getAllColumns() + .filter(({ getIsPinned }) => getIsPinned() === false) + .map(({ id, columnDef }) => ({ + name: id, + label: columnDef.header as string, + operators: getOperators(id), + })); + + const onImportSQL = () => { + setImportSQLOpen(true); + }; + + const convertSQLToCEL = (sql: string): string | null => { + try { + const query = parseSQL(sql); + const formattedCel = formatQuery(query, "cel"); + return formatQuery(parseCEL(formattedCel), "cel"); + } catch (error) { + // If the caught error is an instance of Error, use its message + if (error instanceof Error) { + setSqlError(error.message); + } else { + setSqlError("An unknown error occurred while parsing SQL."); + } + return null; + } + }; + + const onImportSQLSubmit = () => { + const convertedCEL = convertSQLToCEL(sqlQuery); + if (convertedCEL) { + setCELRules(convertedCEL); // Set the converted CEL as the new CEL rules + setImportSQLOpen(false); // Close the modal + setSqlError(null); // Clear any previous errors + } + }; + + const onValueChange = (value: string) => { + setCELRules(value); + if (value.length === 0) { + setIsValidCEL(true); + } + }; + + const validateAndOpenSaveModal = (celExpression: string) => { + // Use existing validation logic + const celQuery = formatQuery(parseCEL(celExpression), "cel"); + const isValidCEL = + celQuery.replace(/\s+/g, "") === celExpression.replace(/\s+/g, "") || + celExpression === ""; + + if (isValidCEL && celExpression.length) { + // If CEL is valid and not empty, set the CEL rules for the preset and open the modal + setPresetCEL(celExpression); + setIsModalOpen(true); + } else { + // If CEL is invalid or empty, inform the user + alert("You can only save a valid CEL expression."); + setIsValidCEL(isValidCEL); + } + }; + + return ( +
+ setIsGUIOpen(false)} + className="w-[50%] max-w-screen-2xl max-h-[710px] transform overflow-auto ring-tremor bg-white p-6 text-left align-middle shadow-tremor transition-all rounded-xl" + title="Query Builder" + > +
+
+ setQuery(newQuery)} + fields={fields} + addRuleToNewGroups + showCombinatorsBetweenRules={false} + /> +
+
+ +
+
+
+ { + setImportSQLOpen(false); + setSqlError(null); + }} // Clear the error when closing the modal + title="Import from SQL" + > +
+