diff --git a/docs/mint.json b/docs/mint.json index cce7cb52f2..fb156d8f9e 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -164,6 +164,7 @@ "providers/documentation/clickhouse-provider", "providers/documentation/cloudwatch-provider", "providers/documentation/console-provider", + "providers/documentation/coroot-provider", "providers/documentation/coralogix-provider", "providers/documentation/dash0-provider", "providers/documentation/databend-provider", diff --git a/docs/providers/documentation/coroot-provider.mdx b/docs/providers/documentation/coroot-provider.mdx new file mode 100644 index 0000000000..5e36f8605e --- /dev/null +++ b/docs/providers/documentation/coroot-provider.mdx @@ -0,0 +1,92 @@ +--- +title: "Coroot" +sidebarTitle: "Coroot Provider" +description: "Coroot provider allows you to pull alerts from Coroot into Keep." +--- +import AutoGeneratedSnippet from '/snippets/providers/coroot-snippet-autogenerated.mdx'; + +## Overview + +[Coroot](https://coroot.com) is an open-source observability platform for Kubernetes applications. It provides comprehensive monitoring of applications, infrastructure, and dependencies with zero-instrumentation and automatic root cause analysis. + +Coroot collects metrics from Prometheus, logs from Loki or ClickHouse, and distributed traces from OpenTelemetry or Jaeger to provide a complete picture of your system's health. + +## Connecting Coroot to Keep + +To connect Coroot to Keep, you'll need to configure the Coroot provider with your Coroot instance URL and optionally an API key. + +### Prerequisites + +- A running Coroot instance (self-hosted or cloud) +- Coroot URL (e.g., `https://coroot.example.com`) +- API Key (if authentication is enabled on your Coroot instance) + +### Configuration Parameters + +| Parameter | Required | Description | Example | +|-----------|----------|-------------|---------| +| URL | Yes | Your Coroot instance URL | `https://coroot.example.com` | +| API Key | No | Coroot API key for authentication | `coroot_api_xxx` | +| Project ID | No | Coroot project ID (if using projects) | `default` | +| Verify SSL | No | Verify SSL certificates (default: true) | `true` or `false` | + +### Obtaining Your Coroot API Key + +1. Log in to your Coroot instance +2. Navigate to **Settings** → **API Keys** (or User Profile) +3. Generate a new API key +4. Copy the key for use in Keep + +### Setting Up the Provider in Keep + +1. Go to the Keep platform and navigate to **Providers** +2. Search for "Coroot" and click **Connect** +3. Enter your Coroot URL +4. If your Coroot instance requires authentication, enter your API key +5. Optionally specify a Project ID if you're using Coroot's multi-project feature +6. Click **Connect** to validate the connection + + + +## Fingerprinting + +Alerts from Coroot are fingerprinted based on: +- Alert name/alertname +- Service name (if available) +- Project ID (if configured) + +This ensures that recurring alerts are properly deduplicated and correlated within Keep. + +## Alert Severity Mapping + +Coroot severities are mapped to Keep alert severities as follows: + +| Coroot Severity | Keep Severity | +|-----------------|---------------| +| critical | CRITICAL | +| error / high | HIGH | +| warning / warn / medium | WARNING | +| info | INFO | +| low | LOW | + +## Alert Status Mapping + +Coroot alert statuses are mapped to Keep alert statuses: + +| Coroot Status | Keep Status | +|---------------|-------------| +| firing / open | FIRING | +| resolved / closed | RESOLVED | + +## Notes + +- The Coroot provider supports both webhook-based and polling-based alert ingestion +- For webhook integration, configure your Coroot instance to send alerts to Keep's webhook endpoint +- The provider handles various alert formats from different Coroot versions + +## Useful Links + +- [Coroot Documentation](https://docs.coroot.com) +- [Coroot GitHub Repository](https://github.com/coroot/coroot) +- [Coroot Installation Guide](https://docs.coroot.com/installation) +- [Coroot Alerting Configuration](https://docs.coroot.com/alerting) diff --git a/docs/snippets/providers/coroot-snippet-autogenerated.mdx b/docs/snippets/providers/coroot-snippet-autogenerated.mdx new file mode 100644 index 0000000000..1497c71395 --- /dev/null +++ b/docs/snippets/providers/coroot-snippet-autogenerated.mdx @@ -0,0 +1,33 @@ +{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py +Do not edit it manually, as it will be overwritten */} + + +## In workflows + +This provider can be used in workflows. + + +As "step" to query data, example: +```yaml +steps: + - name: Get alerts from Coroot + provider: coroot + config: "{{ provider.my_coroot_provider }}" +``` + + + + + + +## Authentication Parameters + +The `api_key` is optional. If your Coroot instance has authentication enabled, you need to provide it. + + +## Provider Configuration + +- `url` (required): Coroot server URL, e.g., `https://coroot.example.com` +- `api_key` (optional): Coroot API key if authentication is enabled +- `project_id` (optional): Coroot project ID (leave empty for default project) +- `verify` (optional): Verify SSL certificates (default: `true`, set to `false` to allow self-signed certificates) diff --git a/keep/providers/coroot_provider/__init__.py b/keep/providers/coroot_provider/__init__.py new file mode 100644 index 0000000000..5b06bf71d6 --- /dev/null +++ b/keep/providers/coroot_provider/__init__.py @@ -0,0 +1,4 @@ +""" +Coroot provider package. +""" +from keep.providers.coroot_provider.coroot_provider import CorootProvider # noqa: F401 diff --git a/keep/providers/coroot_provider/coroot_provider.py b/keep/providers/coroot_provider/coroot_provider.py new file mode 100644 index 0000000000..a8761d8f2f --- /dev/null +++ b/keep/providers/coroot_provider/coroot_provider.py @@ -0,0 +1,225 @@ +""" +CorootProvider is a class that provides a way to read data from Coroot. +""" + +import dataclasses +import datetime +from typing import Optional + +import pydantic +import requests + +from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.base.base_provider import BaseProvider, ProviderHealthMixin +from keep.providers.models.provider_config import ProviderConfig, ProviderScope + + +@pydantic.dataclasses.dataclass +class CorootProviderAuthConfig: + url: pydantic.AnyHttpUrl = dataclasses.field( + metadata={ + "required": True, + "description": "Coroot server URL", + "hint": "https://coroot.example.com", + "validation": "any_http_url", + } + ) + api_key: str = dataclasses.field( + metadata={ + "description": "Coroot API key (if authentication is enabled)", + "sensitive": True, + }, + default="", + ) + project_id: str = dataclasses.field( + metadata={ + "description": "Coroot project ID", + "hint": "Leave empty for default project", + }, + default="", + ) + verify: bool = dataclasses.field( + metadata={ + "description": "Verify SSL certificates", + "hint": "Set to false to allow self-signed certificates", + "sensitive": False, + }, + default=True, + ) + + +class CorootProvider(BaseProvider, ProviderHealthMixin): + """Get alerts from Coroot into Keep.""" + + PROVIDER_DISPLAY_NAME = "Coroot" + PROVIDER_TAGS = ["alert"] + PROVIDER_CATEGORY = ["Monitoring"] + + PROVIDER_SCOPES = [ + ProviderScope( + name="connectivity", + description="Connectivity Test", + mandatory=True, + ) + ] + + SEVERITIES_MAP = { + "critical": AlertSeverity.CRITICAL, + "error": AlertSeverity.HIGH, + "high": AlertSeverity.HIGH, + "warning": AlertSeverity.WARNING, + "warn": AlertSeverity.WARNING, + "medium": AlertSeverity.WARNING, + "info": AlertSeverity.INFO, + "low": AlertSeverity.LOW, + } + + STATUS_MAP = { + "firing": AlertStatus.FIRING, + "resolved": AlertStatus.RESOLVED, + "open": AlertStatus.FIRING, + "closed": AlertStatus.RESOLVED, + } + + def __init__( + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + ): + super().__init__(context_manager, provider_id, config) + + def validate_config(self): + """ + Validates required configuration for Coroot's provider. + """ + self.authentication_config = CorootProviderAuthConfig( + **self.config.authentication + ) + + def validate_scopes(self) -> dict[str, bool | str]: + validated_scopes = {"connectivity": True} + try: + self._get_alerts() + except Exception as e: + validated_scopes["connectivity"] = str(e) + return validated_scopes + + def _get_auth_headers(self) -> dict: + """Get authentication headers if API key is provided.""" + headers = {"Content-Type": "application/json"} + if self.authentication_config.api_key: + headers["Authorization"] = f"Bearer {self.authentication_config.api_key}" + return headers + + def _get_project_param(self) -> dict: + """Get project query parameter if project_id is provided.""" + params = {} + if self.authentication_config.project_id: + params["project"] = self.authentication_config.project_id + return params + + def _get_alerts(self) -> list[AlertDto]: + """Fetch alerts from Coroot API.""" + url = f"{self.authentication_config.url}/api/alerts" + headers = self._get_auth_headers() + params = self._get_project_param() + + response = requests.get( + url, + headers=headers, + params=params, + verify=self.authentication_config.verify, + timeout=30, + ) + response.raise_for_status() + + alerts_data = response.json() + alert_dtos = self._format_alerts(alerts_data) + return alert_dtos + + @staticmethod + def _format_alerts( + data: dict | list, provider_instance: Optional["CorootProvider"] = None + ) -> list[AlertDto]: + """Format Coroot alerts into Keep AlertDto format.""" + alert_dtos = [] + + # Handle both dict and list responses + if isinstance(data, dict): + alerts = data.get("alerts", data.get("items", [])) + elif isinstance(data, list): + alerts = data + else: + return alert_dtos + + for alert in alerts: + if not isinstance(alert, dict): + continue + + # Extract alert fields + alert_id = alert.get("id") or alert.get("alert_id") or alert.get("name") + name = alert.get("name") or alert.get("alertname") or alert_id + description = alert.get("description") or alert.get("summary") or alert.get("message", name) + + # Extract labels/annotations + labels = alert.get("labels", {}) + if isinstance(labels, dict): + labels = {k.lower(): v for k, v in labels.items()} + + # Map severity + severity_str = ( + alert.get("severity") + or labels.get("severity", "info") + ).lower() + severity = CorootProvider.SEVERITIES_MAP.get(severity_str, AlertSeverity.INFO) + + # Map status + status_str = ( + alert.get("status") + or alert.get("state") + or labels.get("status", "firing") + ).lower() + status = CorootProvider.STATUS_MAP.get(status_str, AlertStatus.FIRING) + + # Extract service name + service = ( + labels.get("service") + or labels.get("application") + or labels.get("app") + or alert.get("service") + or alert.get("application") + ) + + # Extract timestamp + timestamp_str = alert.get("timestamp") or alert.get("created_at") or alert.get("starts_at") + try: + if timestamp_str: + timestamp = datetime.datetime.fromisoformat(timestamp_str.replace("Z", "+00:00")) + else: + timestamp = datetime.datetime.now(datetime.timezone.utc) + except (ValueError, TypeError): + timestamp = datetime.datetime.now(datetime.timezone.utc) + + # Build AlertDto + alert_dto = AlertDto( + id=str(alert_id) if alert_id else None, + name=name, + description=description, + status=status, + severity=severity, + service=service, + timestamp=timestamp, + source=["coroot"], + labels=labels, + annotations=alert.get("annotations", {}), + raw_payload=alert, + ) + alert_dtos.append(alert_dto) + + return alert_dtos + + def dispose(self): + """ + Dispose of the provider. + """ + pass + diff --git a/tests/test_coroot_provider.py b/tests/test_coroot_provider.py new file mode 100644 index 0000000000..8a94daedf1 --- /dev/null +++ b/tests/test_coroot_provider.py @@ -0,0 +1,151 @@ +""" +Tests for Coroot provider. +""" + +import pytest +from datetime import datetime, timezone +from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus +from keep.providers.coroot_provider.coroot_provider import CorootProvider + + +class TestCorootProvider: + """Test cases for CorootProvider.""" + + def test_format_alerts_empty_list(self): + """Test formatting empty alert list.""" + result = CorootProvider._format_alerts([]) + assert result == [] + + def test_format_alerts_empty_dict(self): + """Test formatting empty alert dict.""" + result = CorootProvider._format_alerts({"alerts": []}) + assert result == [] + + def test_format_alerts_simple_alert(self): + """Test formatting a simple alert.""" + alerts_data = { + "alerts": [ + { + "id": "test-alert-1", + "name": "High CPU Usage", + "description": "CPU usage is above 90%", + "severity": "warning", + "status": "firing", + "timestamp": "2024-01-15T10:30:00Z", + "labels": { + "service": "api-gateway", + "severity": "warning" + } + } + ] + } + + result = CorootProvider._format_alerts(alerts_data) + + assert len(result) == 1 + alert = result[0] + assert isinstance(alert, AlertDto) + assert alert.name == "High CPU Usage" + assert alert.description == "CPU usage is above 90%" + assert alert.severity == AlertSeverity.WARNING + assert alert.status == AlertStatus.FIRING + assert alert.service == "api-gateway" + assert alert.source == ["coroot"] + + def test_format_alerts_critical(self): + """Test formatting critical severity alert.""" + alerts_data = [ + { + "id": "critical-1", + "name": "Database Connection Failed", + "severity": "critical", + "status": "firing", + } + ] + + result = CorootProvider._format_alerts(alerts_data) + + assert len(result) == 1 + assert result[0].severity == AlertSeverity.CRITICAL + assert result[0].status == AlertStatus.FIRING + + def test_format_alerts_resolved(self): + """Test formatting resolved alert.""" + alerts_data = [ + { + "id": "resolved-1", + "name": "Memory Alert", + "severity": "info", + "status": "resolved", + } + ] + + result = CorootProvider._format_alerts(alerts_data) + + assert len(result) == 1 + assert result[0].status == AlertStatus.RESOLVED + + def test_format_alerts_list_response(self): + """Test formatting alerts from list response.""" + alerts_data = [ + { + "alert_id": "alert-1", + "name": "Alert One", + "summary": "Summary of alert one", + "severity": "high", + "state": "open", + "application": "app-1", + "created_at": "2024-01-15T12:00:00Z", + }, + { + "alert_id": "alert-2", + "name": "Alert Two", + "description": "Description of alert two", + "severity": "low", + "status": "closed", + "service": "service-2", + } + ] + + result = CorootProvider._format_alerts(alerts_data) + + assert len(result) == 2 + assert result[0].name == "Alert One" + assert result[0].severity == AlertSeverity.HIGH + assert result[0].service == "app-1" + assert result[1].name == "Alert Two" + assert result[1].severity == AlertSeverity.LOW + assert result[1].status == AlertStatus.RESOLVED + + def test_severity_mapping(self): + """Test all severity mappings.""" + test_cases = [ + ("critical", AlertSeverity.CRITICAL), + ("error", AlertSeverity.HIGH), + ("high", AlertSeverity.HIGH), + ("warning", AlertSeverity.WARNING), + ("warn", AlertSeverity.WARNING), + ("medium", AlertSeverity.WARNING), + ("info", AlertSeverity.INFO), + ("low", AlertSeverity.LOW), + ("unknown", AlertSeverity.INFO), # default + ] + + for input_severity, expected in test_cases: + alerts_data = [{"id": "test", "name": "Test", "severity": input_severity}] + result = CorootProvider._format_alerts(alerts_data) + assert result[0].severity == expected, f"Failed for severity: {input_severity}" + + def test_status_mapping(self): + """Test all status mappings.""" + test_cases = [ + ("firing", AlertStatus.FIRING), + ("open", AlertStatus.FIRING), + ("resolved", AlertStatus.RESOLVED), + ("closed", AlertStatus.RESOLVED), + ] + + for input_status, expected in test_cases: + alerts_data = [{"id": "test", "name": "Test", "status": input_status}] + result = CorootProvider._format_alerts(alerts_data) + assert result[0].status == expected, f"Failed for status: {input_status}"