Repository: keephq/keep
Pull request: fix: Use integer severities for zabbix provider
<!-- 
Thanks for creating this pull request 🤗

Please make sure that the pull request is limited to one type (docs, feature, etc.) and keep it as small as possible. You can open multiple prs instead of opening a huge one.
-->

<!-- If this pull request closes an issue, please mention the issue number below -->
Closes #5139 

## 📑 Description
Keep uses a severity map to translate zabbix severity to Keep severity. Currently it references zabbix severities using names. When the severity names are updated in zabbix, keep is unable to map the new names correctly. Hence, it classifies the alerts with changed severities as INFO level alerts. 

Solution: Add support for integer severities to Zabbix provider.


## ✅ Checks
<!-- Make sure your pr passes the CI checks and do check the following fields as needed - -->
- [ ] My pull request adheres to the code style of this project
- [ ] My code requires changes to the documentation
- [ ] I have updated the documentation as required
- [ ] All the tests have passed

## ℹ Additional Information
Also, updated the change_severity method to accept integers in addition to severity names for better usability.



Problem statement:
[🐛 Bug]: Zabbix provider is misclassifying the alert severity
**Describe the bug**
Whenever Zabbix sends a critical alert, Keep does not display it with the correct severity.

**To Reproduce**
Steps to reproduce the behavior:

1. Login to Zabbix as an admin and edit the severity names.
2. Save the changes and let the new alerts forward to keep.

**Expected behavior**
Updating the severity name in Zabbix should appropriately reflect in Keep.

Hints from issue discussion:
<!-- Answer -->
The Zabbix provider in Keep uses a static mapping for severity names and only recognizes the default Zabbix severities: "not_classified", "information", "warning", "average", "high", and "disaster". If you customize severity names in Zabbix, Keep won't recognize them and will default to displaying those alerts as INFO severity. This behavior is hardcoded in the provider and isn't configurable or dynamic—there's no current support for mapping custom severity names from Zabbix to Keep severities. You can see the mapping and its limitations in the [docs](https://docs.keephq.dev/overview/alertseverityandstatus) and the [provider code](https://github.com/keephq/keep/blob/9aa3b8720cfe0d5da3c5f8d96aefbbdeff64f11b/keep/providers/zabbix_provider/zabbix_provider.py).

As a workaround, you can either revert to the default Zabbix severity names or, if you're self-hosting, modify the SEVERITIES_MAP in the provider code to include your custom names. There is currently no built-in way to configure or extend this mapping through the UI or config files.


<!-- Dosu Comment Footer -->
*To reply, just mention [@dosu](https://go.dosu.dev/dosubot).*

---
<sup>How did I do? [Good](https://app.dosu.dev/response-feedback/a7b2bd76-357e-44be-90ea-13a6694f1764?feedback_type=great_response) | [Irrelevant](https://app.dosu.dev/response-feedback/a7b2bd76-357e-44be-90ea-13a6694f1764?feedback_type=irrelevant_answer) | [Incorrect](https://app.dosu.dev/response-feedback/a7b2bd76-357e-44be-90ea-13a6694f1764?feedback_type=incorrect_sources) | [Verbose](https://app.dosu.dev/response-feedback/a7b2bd76-357e-44be-90ea-13a6694f1764?feedback_type=too_verbose) | [Hallucination](https://app.dosu.dev/response-feedback/a7b2bd76-357e-44be-90ea-13a6694f1764?feedback_type=hallucination) | [Report 🐛](https://app.dosu.dev/response-feedback/a7b2bd76-357e-44be-90ea-13a6694f1764?feedback_type=bug_report) | [Other](https://app.dosu.dev/response-feedback/a7b2bd76-357e-44be-90ea-13a6694f1764?feedback_type=other)</sup>&nbsp;&nbsp;[![Join Discord](https://img.shields.io/badge/join-5865F2?logo=discord&logoColor=white&label=)](https://go.dosu.dev/discord-bot)&nbsp;[![Share on X](https://img.shields.io/badge/X-share-black)](https://twitter.com/intent/tweet?text=%40dosu_ai%20helped%20me%20solve%20this%20issue!&url=https%3A//github.com/keephq/keep/issues/5139)

Commit message: Merge branch 'main' into 5139-fix-alert-severity-classification-for-zabbix

Patch to review:
diff --git a/docs/snippets/providers/zabbix-snippet-autogenerated.mdx b/docs/snippets/providers/zabbix-snippet-autogenerated.mdx
index 3a077e4f21..d26b20b8fd 100644
--- a/docs/snippets/providers/zabbix-snippet-autogenerated.mdx
+++ b/docs/snippets/providers/zabbix-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
@@ -39,7 +39,13 @@ The provider exposes the following [Provider Methods](/providers/provider-method
 - **change_severity** No description. (action, scopes: event.acknowledge)
 
     - `id`: The problem id.
-    - `new_severity`: The new severity, can be one of the following: Not classified, Information, Warning, Average, High, Disaster
+    - `new_severity`: The new severity. Can be an integer (0-5) or string:
+- 0 or "Not classified"
+- 1 or "Information"
+- 2 or "Warning"
+- 3 or "Average"
+- 4 or "High"
+- 5 or "Disaster"
 - **surrpress_problem** No description. (action, scopes: event.acknowledge)
 
     - `id`: The problem id.
diff --git a/keep/providers/zabbix_provider/zabbix_provider.py b/keep/providers/zabbix_provider/zabbix_provider.py
index 8dd1614d64..5243bcf00d 100644
--- a/keep/providers/zabbix_provider/zabbix_provider.py
+++ b/keep/providers/zabbix_provider/zabbix_provider.py
@@ -8,7 +8,7 @@
 import logging
 import os
 import random
-from typing import Literal
+from typing import Literal, Union
 
 import pydantic
 import requests
@@ -205,12 +205,21 @@ class ZabbixProvider(BaseProvider):
     ]
 
     SEVERITIES_MAP = {
-        "not_classified": AlertSeverity.LOW,
-        "information": AlertSeverity.INFO,
-        "warning": AlertSeverity.WARNING,
-        "average": AlertSeverity.WARNING,
-        "high": AlertSeverity.HIGH,
-        "disaster": AlertSeverity.CRITICAL,
+        0: AlertSeverity.LOW,
+        1: AlertSeverity.INFO,
+        2: AlertSeverity.WARNING,
+        3: AlertSeverity.WARNING,
+        4: AlertSeverity.HIGH,
+        5: AlertSeverity.CRITICAL,
+    }
+
+    SEVERITY_NAME_TO_ID_MAP = {
+        "not classified": 0,
+        "information": 1,
+        "warning": 2,
+        "average": 3,
+        "high": 4,
+        "disaster": 5,
     }
 
     STATUS_MAP = {
@@ -342,27 +351,33 @@ def get_problem_messages(self, id: str):
     def change_severity(
         self,
         id: str,
-        new_severity: Literal[
-            "Not classified", "Information", "Warning", "Average", "High", "Disaster"
+        new_severity: Union[
+            int,
+            Literal[
+                "Not classified", "Information", "Warning", "Average", "High", "Disaster"
+            ],
         ],
     ):
         """
         Change the severity of a problem.
         Args:
             id (str): The problem id.
-            new_severity (str): The new severity, can be one of the following: Not classified, Information, Warning, Average, High, Disaster
+            new_severity (int | str): The new severity. Can be an integer (0-5) or string:
+                - 0 or "Not classified"
+                - 1 or "Information"
+                - 2 or "Warning"
+                - 3 or "Average"
+                - 4 or "High"
+                - 5 or "Disaster"
         """
+        # Handle integer input
         severity = 0
-        if new_severity.lower() == "information":
-            severity = 1
-        elif new_severity.lower() == "warning":
-            severity = 2
-        elif new_severity.lower() == "average":
-            severity = 3
-        elif new_severity.lower() == "high":
-            severity = 4
-        elif new_severity.lower() == "disaster":
-            severity = 5
+        if isinstance(new_severity, int):
+            if 0 <= new_severity <= 5:
+                severity = new_severity
+        else:
+            # Handle string input
+            severity = ZabbixProvider.SEVERITY_NAME_TO_ID_MAP.get(new_severity.lower(), 0)
         self.__send_request(
             "event.acknowledge", {"eventids": id, "severity": severity, "action": 8}
         )
@@ -463,6 +478,40 @@ def __send_request(
             raise ProviderMethodException(error_data)
         return response_json
 
+    @staticmethod
+    def _convert_severity(severity: Union[int, str]) -> AlertSeverity:
+        """
+        Convert Zabbix severity to Keep AlertSeverity.
+
+        Args:
+            severity (Union[int, str]): The severity value. Can be:
+                - Integer (0-5): 0=Not classified, 1=Information, 2=Warning, 3=Average, 4=High, 5=Disaster
+                - String: "not classified", "information", "warning", "average", "high", "disaster"
+
+        Returns:
+            AlertSeverity: The corresponding Keep AlertSeverity
+        """
+        if isinstance(severity, int):
+            return ZabbixProvider.SEVERITIES_MAP.get(severity, AlertSeverity.INFO)
+
+        # Handle string input
+        if isinstance(severity, str):
+            severity_stripped = severity.strip()
+
+            # First, check if it's a numeric string
+            if severity_stripped.isdigit():
+                severity_int = int(severity_stripped)
+                if 0 <= severity_int <= 5:
+                    return ZabbixProvider.SEVERITIES_MAP.get(severity_int, AlertSeverity.INFO)
+
+            # If not a valid integer string, handle as text
+            severity_lower = severity_stripped.lower()
+            severity_int = ZabbixProvider.SEVERITY_NAME_TO_ID_MAP.get(severity_lower, 1)  # Default to Information
+            return ZabbixProvider.SEVERITIES_MAP.get(severity_int, AlertSeverity.INFO)
+
+        # Fallback for any other type
+        return AlertSeverity.INFO
+
     def _get_alerts(self) -> list[AlertDto]:
         # https://www.zabbix.com/documentation/current/en/manual/api/reference/problem/get
         time_from = int(
@@ -485,9 +534,7 @@ def _get_alerts(self) -> list[AlertDto]:
             if environment is None:
                 environment = "unknown"
 
-            severity = ZabbixProvider.SEVERITIES_MAP.get(
-                problem.pop("severity", "").lower(), AlertSeverity.INFO
-            )
+            severity = self._convert_severity(problem.pop("severity", 1))
             status = ZabbixProvider.STATUS_MAP.get(
                 problem.pop("status", "").lower(), AlertStatus.FIRING
             )
@@ -694,8 +741,7 @@ def _format_alert(
                 f"{zabbix_url}/tr_events.php?triggerid={trigger_id}&eventid={event_id}"
             )
 
-        severity = event.pop("severity", "").lower()
-        severity = ZabbixProvider.SEVERITIES_MAP.get(severity, AlertSeverity.INFO)
+        severity = ZabbixProvider._convert_severity(event.pop("severity", 1))
 
         status = event.pop("status", "").lower()
         status = ZabbixProvider.STATUS_MAP.get(status, AlertStatus.FIRING)


Code review comments:
diff_hunk: @@ -205,12 +205,21 @@ class ZabbixProvider(BaseProvider):
     ]
 
     SEVERITIES_MAP = {
-        "not_classified": AlertSeverity.LOW,
-        "information": AlertSeverity.INFO,
-        "warning": AlertSeverity.WARNING,
-        "average": AlertSeverity.WARNING,
-        "high": AlertSeverity.HIGH,
-        "disaster": AlertSeverity.CRITICAL,
+        0: AlertSeverity.LOW,
+        1: AlertSeverity.INFO,
+        2: AlertSeverity.WARNING,
+        3: AlertSeverity.WARNING,
+        4: AlertSeverity.HIGH,
+        5: AlertSeverity.CRITICAL,
+    }
+
+    SEVERITY_NAME_TO_ID_MAP = {
+        "not classified": 0,
path: keep/providers/zabbix_provider/zabbix_provider.py
text: @user1:
only thing I worry about that it was `not_classified` and now it's `not classified`, is that a mistake?

@author:
By default the label is `Not classified` in zabbix. Hence, I used space in-place of underscore(_) there. Just to make sure we don't break things, I can probably add a mapping  `"not_classified": 0` as well.

@author:
Made the change