From 8d7e0516ab7607094a096c43840f26d73bdedba0 Mon Sep 17 00:00:00 2001 From: Alex Hedtke Date: Thu, 30 Jul 2026 00:37:58 -0500 Subject: [PATCH] feat: add MongoDB Atlas provider --- README.md | 6 + docs/mint.json | 1 + .../documentation/mongodbatlas-provider.mdx | 69 ++++++ docs/providers/overview.mdx | 8 + .../mongodbatlas-snippet-autogenerated.mdx | 35 +++ keep-ui/public/icons/mongodbatlas-icon.png | Bin 0 -> 6621 bytes .../mongodbatlas_provider/__init__.py | 0 .../mongodbatlas_provider.py | 226 ++++++++++++++++++ 8 files changed, 345 insertions(+) create mode 100644 docs/providers/documentation/mongodbatlas-provider.mdx create mode 100644 docs/snippets/providers/mongodbatlas-snippet-autogenerated.mdx create mode 100644 keep-ui/public/icons/mongodbatlas-icon.png create mode 100644 keep/providers/mongodbatlas_provider/__init__.py create mode 100644 keep/providers/mongodbatlas_provider/mongodbatlas_provider.py diff --git a/README.md b/README.md index 5b44b1df93..aa591e72b0 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,12 @@ MongoDB + + + MongoDB Atlas
+ MongoDB Atlas +
+ MySQL
diff --git a/docs/mint.json b/docs/mint.json index 7672da965b..b0312f68dd 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -212,6 +212,7 @@ "providers/documentation/mock-provider", "providers/documentation/monday-provider", "providers/documentation/mongodb-provider", + "providers/documentation/mongodbatlas-provider", "providers/documentation/mysql-provider", "providers/documentation/netbox-provider", "providers/documentation/netdata-provider", diff --git a/docs/providers/documentation/mongodbatlas-provider.mdx b/docs/providers/documentation/mongodbatlas-provider.mdx new file mode 100644 index 0000000000..7be3b25acc --- /dev/null +++ b/docs/providers/documentation/mongodbatlas-provider.mdx @@ -0,0 +1,69 @@ +--- +title: "MongoDB Atlas Provider" +sidebarTitle: "MongoDB Atlas" +description: "MongoDB Atlas provider allows pulling alerts from Atlas projects and receiving Atlas alert webhooks." +--- +import AutoGeneratedSnippet from '/snippets/providers/mongodbatlas-snippet-autogenerated.mdx'; + + + +import ProviderLogo from '@components/ProviderLogo'; + + + +# MongoDB Atlas Provider + +The MongoDB Atlas provider connects Keep to an Atlas project. It supports two ways of getting alerts into Keep: + +1. **Pulling** open, tracking, and closed alerts from the [Atlas Administration API](https://www.mongodb.com/docs/api/doc/atlas-admin-api-v2/operation/operation-listalerts). +2. **Receiving** alert notifications pushed by an Atlas [webhook integration](https://www.mongodb.com/docs/atlas/tutorial/webhook-integration/). + +## Setup + +### Prerequisites + +1. An Atlas project. +2. An API key pair (organization or project) with the **Project Read Only** role on the project. See [Grant Programmatic Access to Atlas](https://www.mongodb.com/docs/atlas/configure-api-access/). +3. The project (group) ID: a 24-character hex string, found under **Project Settings**. + +### Configuration + +```yaml +authentication: + api_public_key: "abcdefgh" # Atlas API public key + api_private_key: "00000000-0000-0000-0000-000000000000" # Atlas API private key + group_id: "5d1b6f8e8c2e4e2d3c4a5b6d" # Atlas project (group) ID + # api_url: "https://cloud.mongodbgov.com" # only for Atlas for Government +``` + +If your Atlas project restricts API access by IP, add Keep's IP to the API key's access list. + +### Webhook configuration + +To have Atlas push alerts to Keep: + +1. In Atlas, go to your project and click **Integrations**. +2. Choose **Webhook** and click **Configure**. +3. Set the URL to your Keep webhook URL, with your API key as the `api_key` query parameter. +4. In **Alerts** > **Alert Settings**, select **Webhook** as a notification method on the alert configurations you want in Keep. + +Notes: + +- Atlas webhook requests can't carry custom headers, so authentication uses the `api_key` query parameter rather than the `X-API-KEY` header. +- Atlas payloads carry a status (`OPEN`, `TRACKING`, `CLOSED`) but no severity, so alerts arrive in Keep with `warning` severity. Use Keep mapping rules if you want severities derived from `eventTypeName`. +- Alerts are deduplicated on the Atlas alert `id`, which is stable across an alert's open/close lifecycle. + +## Status mapping + +| Atlas status | Keep status | +| ------------ | ----------- | +| `OPEN` | `firing` | +| `TRACKING` | `pending` | +| `CLOSED`, `CANCELLED` | `resolved` | +| acknowledged (via `acknowledgedUntil`) | `acknowledged` | + +## Useful links + +- [Atlas alert basics](https://www.mongodb.com/docs/atlas/alert-basics/) +- [Configure alerts](https://www.mongodb.com/docs/atlas/configure-alerts/) +- [Webhook integration](https://www.mongodb.com/docs/atlas/tutorial/webhook-integration/) diff --git a/docs/providers/overview.mdx b/docs/providers/overview.mdx index 332c53fe4f..d35e4e561b 100644 --- a/docs/providers/overview.mdx +++ b/docs/providers/overview.mdx @@ -532,6 +532,14 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t } > + + } +> + Alert Settings, add or edit alert configurations and select Webhook as a notification method. + +Atlas does not include a severity in webhook payloads, so alerts arrive with warning severity and can be re-mapped in Keep. + diff --git a/keep-ui/public/icons/mongodbatlas-icon.png b/keep-ui/public/icons/mongodbatlas-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..e58a4a685b3424561d8faf379362adf7e24e7530 GIT binary patch literal 6621 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 Alert Settings, add or edit alert configurations and select Webhook as a notification method. + +Atlas does not include a severity in webhook payloads, so alerts arrive with warning severity and can be re-mapped in Keep. +""" + + def __init__( + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + ): + super().__init__(context_manager, provider_id, config) + + def validate_config(self): + self.authentication_config = MongodbatlasProviderAuthConfig( + **self.config.authentication + ) + + def dispose(self): + pass + + @property + def _auth(self) -> HTTPDigestAuth: + return HTTPDigestAuth( + self.authentication_config.api_public_key, + self.authentication_config.api_private_key, + ) + + def _alerts_url(self) -> str: + base = self.authentication_config.api_url.rstrip("/") + return ( + f"{base}/api/atlas/v2/groups/{self.authentication_config.group_id}/alerts" + ) + + def validate_scopes(self) -> dict[str, bool | str]: + try: + response = requests.get( + self._alerts_url(), + auth=self._auth, + headers={"Accept": self.ATLAS_API_VERSION_HEADER}, + params={"itemsPerPage": 1}, + timeout=10, + ) + response.raise_for_status() + return {"Project Read Only": True} + except requests.exceptions.HTTPError as e: + if e.response.status_code in (401, 403): + return { + "Project Read Only": "Authentication failed, check the API key pair and that it has Project Read Only access" + } + return {"Project Read Only": str(e)} + except requests.exceptions.RequestException as e: + return {"Project Read Only": str(e)} + + def _get_alerts(self) -> list[AlertDto]: + alerts = [] + page_num = 1 + while True: + try: + response = requests.get( + self._alerts_url(), + auth=self._auth, + headers={"Accept": self.ATLAS_API_VERSION_HEADER}, + params={"itemsPerPage": self.PAGE_SIZE, "pageNum": page_num}, + timeout=30, + ) + response.raise_for_status() + except requests.exceptions.RequestException as e: + raise ProviderException( + f"Failed to get alerts from MongoDB Atlas: {e}" + ) from e + data = response.json() + results = data.get("results", []) + alerts.extend(self._format_alert(alert) for alert in results) + if not results or page_num * self.PAGE_SIZE >= data.get("totalCount", 0): + break + page_num += 1 + return alerts + + @staticmethod + def _format_alert( + event: dict, provider_instance: "BaseProvider" = None + ) -> AlertDto: + # Webhook payloads use the same schema as the Admin API alert resource, + # so this formatter serves both the pull and push paths. Atlas does not + # include a severity, only a status. + status = MongodbatlasProvider.STATUS_MAP.get( + event.get("status"), AlertStatus.FIRING + ) + if event.get("acknowledgedUntil") and status == AlertStatus.FIRING: + status = AlertStatus.ACKNOWLEDGED + current_value = event.get("currentValue") or {} + return AlertDto( + id=event.get("id"), + name=event.get("eventTypeName", "MongoDB Atlas alert"), + status=status, + severity=AlertSeverity.WARNING, + lastReceived=event.get("updated") + or event.get("created") + or datetime.datetime.now(tz=datetime.timezone.utc).isoformat(), + description=event.get("humanReadable"), + source=["mongodbatlas"], + cluster=event.get("clusterName"), + replicaSet=event.get("replicaSetName"), + host=event.get("hostnameAndPort"), + metric=event.get("metricName"), + currentValue=current_value.get("number"), + currentValueUnits=current_value.get("units"), + groupId=event.get("groupId"), + alertConfigId=event.get("alertConfigId"), + ) + + +if __name__ == "__main__": + import logging + import os + + logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()]) + context_manager = ContextManager( + tenant_id="singletenant", + workflow_id="test", + ) + config = ProviderConfig( + authentication={ + "api_public_key": os.environ.get("ATLAS_API_PUBLIC_KEY"), + "api_private_key": os.environ.get("ATLAS_API_PRIVATE_KEY"), + "group_id": os.environ.get("ATLAS_GROUP_ID"), + } + ) + provider = MongodbatlasProvider( + context_manager=context_manager, + provider_id="mongodbatlas", + config=config, + ) + print(provider.validate_scopes()) + alerts = provider.get_alerts() + print(f"Got {len(alerts)} alerts")