diff --git a/.github/workflows/auto-resolve-keep.yml b/.github/workflows/auto-resolve-keep.yml index 8ec8138e1..a694a5f4f 100644 --- a/.github/workflows/auto-resolve-keep.yml +++ b/.github/workflows/auto-resolve-keep.yml @@ -61,7 +61,7 @@ jobs: url: "https://api.keephq.dev/incidents/${{ steps.set_ids.outputs.final_incident_id }}/enrich" method: "POST" customHeaders: '{"X-API-KEY": "${{ secrets.KEEP_API_KEY }}", "Content-Type": "application/json"}' - data: '{"enrichments":{"incident_title":"${{ github.event.pull_request.title || ''Manual resolution'' }}","incident_url":"${{ github.event.pull_request.html_url || github.server_url }}//${{ github.repository }}/actions/runs/${{ github.run_id }}", "incident_id": "${{ github.run_id }}", "incident_provider": "github"}}' + data: '{"enrichments":{"incident_title":"${{ github.event.pull_request.title || ''Manual resolution'' }}","incident_url":"${{ github.event.pull_request.html_url || format(''{0}/{1}/actions/runs/{2}'', github.server_url, github.repository, github.run_id) }}", "incident_id": "${{ github.run_id }}", "incident_provider": "github"}}' - name: Auto resolve Keep alert if: | @@ -72,4 +72,4 @@ jobs: url: "https://api.keephq.dev/alerts/enrich?dispose_on_new_alert=true" method: "POST" customHeaders: '{"Content-Type": "application/json", "X-API-KEY": "${{ secrets.KEEP_API_KEY }}"}' - data: '{"enrichments":{"status":"${{ inputs.status || ''resolved'' }}","dismissed":false,"dismissUntil":"","note":"${{ github.event.pull_request.title || ''Manual resolution'' }}","ticket_url":"${{ github.event.pull_request.html_url || github.server_url }}//${{ github.repository }}/actions/runs/${{ github.run_id }}"},"fingerprint":"${{ steps.set_ids.outputs.final_alert_fingerprint }}"}' + data: '{"enrichments":{"status":"${{ inputs.status || ''resolved'' }}","dismissed":false,"dismissUntil":"","note":"${{ github.event.pull_request.title || ''Manual resolution'' }}","ticket_url":"${{ github.event.pull_request.html_url || format(''{0}/{1}/actions/runs/{2}'', github.server_url, github.repository, github.run_id) }}"},"fingerprint":"${{ steps.set_ids.outputs.final_alert_fingerprint }}"}' diff --git a/README.md b/README.md index 59b22dc63..4d9f5067a 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,12 @@ Cilium + + + Checkly
+ Checkly +
+ CloudWatch
diff --git a/docs/images/checkly-provider_1.png b/docs/images/checkly-provider_1.png new file mode 100644 index 000000000..f797b2e03 Binary files /dev/null and b/docs/images/checkly-provider_1.png differ diff --git a/docs/images/checkly-provider_10.png b/docs/images/checkly-provider_10.png new file mode 100644 index 000000000..0628ccd7b Binary files /dev/null and b/docs/images/checkly-provider_10.png differ diff --git a/docs/images/checkly-provider_11.png b/docs/images/checkly-provider_11.png new file mode 100644 index 000000000..4dfa318c6 Binary files /dev/null and b/docs/images/checkly-provider_11.png differ diff --git a/docs/images/checkly-provider_12.png b/docs/images/checkly-provider_12.png new file mode 100644 index 000000000..2fbe5fa68 Binary files /dev/null and b/docs/images/checkly-provider_12.png differ diff --git a/docs/images/checkly-provider_2.png b/docs/images/checkly-provider_2.png new file mode 100644 index 000000000..0ac91e04e Binary files /dev/null and b/docs/images/checkly-provider_2.png differ diff --git a/docs/images/checkly-provider_3.png b/docs/images/checkly-provider_3.png new file mode 100644 index 000000000..e974e1fbb Binary files /dev/null and b/docs/images/checkly-provider_3.png differ diff --git a/docs/images/checkly-provider_4.png b/docs/images/checkly-provider_4.png new file mode 100644 index 000000000..58ae31ad1 Binary files /dev/null and b/docs/images/checkly-provider_4.png differ diff --git a/docs/images/checkly-provider_5.png b/docs/images/checkly-provider_5.png new file mode 100644 index 000000000..3b3946e92 Binary files /dev/null and b/docs/images/checkly-provider_5.png differ diff --git a/docs/images/checkly-provider_6.png b/docs/images/checkly-provider_6.png new file mode 100644 index 000000000..71cc7483a Binary files /dev/null and b/docs/images/checkly-provider_6.png differ diff --git a/docs/images/checkly-provider_7.png b/docs/images/checkly-provider_7.png new file mode 100644 index 000000000..894a5f3c3 Binary files /dev/null and b/docs/images/checkly-provider_7.png differ diff --git a/docs/images/checkly-provider_8.png b/docs/images/checkly-provider_8.png new file mode 100644 index 000000000..1f46abf61 Binary files /dev/null and b/docs/images/checkly-provider_8.png differ diff --git a/docs/images/checkly-provider_9.png b/docs/images/checkly-provider_9.png new file mode 100644 index 000000000..c9205b53f Binary files /dev/null and b/docs/images/checkly-provider_9.png differ diff --git a/docs/mint.json b/docs/mint.json index 53b41543e..f90c61e29 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -137,6 +137,7 @@ "providers/documentation/bigquery-provider", "providers/documentation/centreon-provider", "providers/documentation/checkmk-provider", + "providers/documentation/checkly-provider", "providers/documentation/cilium-provider", "providers/documentation/clickhouse-provider", "providers/documentation/cloudwatch-provider", diff --git a/docs/providers/documentation/checkly-provider.mdx b/docs/providers/documentation/checkly-provider.mdx new file mode 100644 index 000000000..19a502f3c --- /dev/null +++ b/docs/providers/documentation/checkly-provider.mdx @@ -0,0 +1,124 @@ +--- +title: 'Checkly' +sidebarTitle: 'Checkly Provider' +description: 'StatusCake allows you to receive alerts from Checkly using API endpoints as well as webhooks' +--- + +## Authentication Parameters + +The Checkly provider offers two ways to authenticate: + +- `Checkly API Key` - This is the API key created in the User Settings of your Checkly account and is used to authenticate requests to the Checkly API. +- `Checkly Account ID` - This is the account ID of your Checkly account. + +## Connecting Checkly to Keep + +1. Open Checkly dashboard and click on your profile picture in the top right corner. + +2. Click on `User Settings`. + + + + + +3. Open the `API Keys` tab and click on `Create API Key` to generate a new API key. + + + + + +4. Copy the API key. + +5. Open `General` tab under Account Settings and copy the `Account ID`. + + + + + +6. Go to Keep, add Checkly as a provider and enter the API key and Account ID in the respective fields and click on `Connect`. + +## Webhooks Integration + +1. Open Checkly dashboard and open `Alerts` tab in the left sidebar. + + + + + +2. Click on `Add more channels` + + + + + +3. Select `Webhook` from the list of available channels. + + + + + +4. Enter a name for the webhook, select the method as `POST` + +5. Enter [https://api.keephq.dev/alerts/event/checkly](https://api.keephq.dev/alerts/event/checkly) as the URL. + +6. Copy the below snippet and paste in the `Body` of Webhook. Refer the screenshot below for reference. + +```json +{ + "event": "{{ALERT_TITLE}}", + "alert_type": "{{ALERT_TYPE}}", + "check_name": "{{CHECK_NAME}}", + "group_name": "{{GROUP_NAME}}", + "check_id": "{{CHECK_ID}}", + "check_type": "{{CHECK_TYPE}}", + "check_result_id": "{{CHECK_RESULT_ID}}", + "check_error_message": "{{CHECK_ERROR_MESSAGE}}", + "response_time": "{{RESPONSE_TIME}}", + "api_check_response_status_code": "{{API_CHECK_RESPONSE_STATUS_CODE}}", + "api_check_response_status_text": "{{API_CHECK_RESPONSE_STATUS_TEXT}}", + "run_location": "{{RUN_LOCATION}}", + "ssl_days_remaining": "{{SSL_DAYS_REMAINING}}", + "ssl_check_domain": "{{SSL_CHECK_DOMAIN}}", + "started_at": "{{STARTED_AT}}", + "tags": "{{TAGS}}", + "link": "{{RESULT_LINK}}", + "region": "{{REGION}}", + "uuid": "{{$UUID}}" +} +``` + + + + + +8. Go to Headers tab and add a new header with key as `X-API-KEY` and create a new API key in Keep and paste it as the value and save the webhook. + + + + + +9. Follow the below steps to create a new API key in Keep. + +7. Go to Keep dashboard and click on the profile icon in the botton left corner and click `Settings`. + + + + + +8. Select `Users and Access` tab and then select `API Keys` tab and create a new API key. + + + + + +9. Give name and select the role as `webhook` and click on `Create API Key`. + + + + + +10. Use the generated API key in the `X-API-KEY` header of the webhook created in Checkly. + +## Useful Links + +- [Checkly Website](https://www.checklyhq.com/) diff --git a/docs/providers/overview.mdx b/docs/providers/overview.mdx index 5f8b47146..53a16c3c7 100644 --- a/docs/providers/overview.mdx +++ b/docs/providers/overview.mdx @@ -84,6 +84,12 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t icon={ } > + } +> + router.push("/alerts/feed")} /> ); diff --git a/keep-ui/app/(keep)/incidents/[id]/incident-layout-client.tsx b/keep-ui/app/(keep)/incidents/[id]/incident-layout-client.tsx index 9a993ac51..3ba696f6e 100644 --- a/keep-ui/app/(keep)/incidents/[id]/incident-layout-client.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/incident-layout-client.tsx @@ -44,7 +44,7 @@ export function IncidentLayoutClient({ /> } - initialLeftWidth={60} + initialLeftWidth={65} /> ) : (
{children}
diff --git a/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx b/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx index 0e048775e..7ac903814 100644 --- a/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx +++ b/keep-ui/app/(keep)/incidents/[id]/timeline/incident-timeline.tsx @@ -283,9 +283,9 @@ export default function IncidentTimeline({ // TODO: Load data on server side // Loading state is true if the data is not loaded and there is no error for smoother loading state on initial load - const alertsLoading = _alertsLoading || (!alerts && !alertsError); - const auditEventsLoading = - _auditEventsLoading || (!auditEvents && !auditEventsError); + // const alertsLoading = _alertsLoading || (!alerts && !alertsError); + // const auditEventsLoading = + // _auditEventsLoading || (!auditEvents && !auditEventsError); const [selectedEvent, setSelectedEvent] = useState(null); @@ -358,7 +358,7 @@ export default function IncidentTimeline({ return {}; }, [auditEvents, alerts]); - if (auditEventsLoading || alertsLoading) { + if (_auditEventsLoading || _alertsLoading) { return ( diff --git a/keep-ui/components/ui/EmptyStateCard.tsx b/keep-ui/components/ui/EmptyStateCard.tsx index 43cb90d84..5559c57da 100644 --- a/keep-ui/components/ui/EmptyStateCard.tsx +++ b/keep-ui/components/ui/EmptyStateCard.tsx @@ -10,7 +10,7 @@ export function EmptyStateCard({ }: { title: string; description: string; - buttonText: string; + buttonText?: string; onClick: (e: React.MouseEvent) => void; className?: string; }) { @@ -29,9 +29,11 @@ export function EmptyStateCard({

{description}

- + {buttonText && ( + + )}
); diff --git a/keep-ui/components/ui/ResizableColumns.tsx b/keep-ui/components/ui/ResizableColumns.tsx index 128778e09..8b10b67e1 100644 --- a/keep-ui/components/ui/ResizableColumns.tsx +++ b/keep-ui/components/ui/ResizableColumns.tsx @@ -62,7 +62,7 @@ const ResizableColumns = ({ onMouseDown={startDragging} /> -
{rightChild}
+
{rightChild}
); }; diff --git a/keep-ui/public/icons/checkly-icon.png b/keep-ui/public/icons/checkly-icon.png new file mode 100644 index 000000000..563513d4b Binary files /dev/null and b/keep-ui/public/icons/checkly-icon.png differ diff --git a/keep/providers/checkly_provider/__init__.py b/keep/providers/checkly_provider/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keep/providers/checkly_provider/alerts_mock.py b/keep/providers/checkly_provider/alerts_mock.py new file mode 100644 index 000000000..a5d70e8db --- /dev/null +++ b/keep/providers/checkly_provider/alerts_mock.py @@ -0,0 +1,21 @@ +ALERTS = { + "event": "API Check #1 has recovered", + "alert_type": "ALERT_RECOVERY", + "check_name": "API Check #1", + "group_name": "", + "check_id": "927a2982-1007-4b81-b383-eae8bf717e61", + "check_type": "API", + "check_result_id": "a34867c0-9239-421f-92f2-4408bbd05417", + "check_error_message": "", + "response_time": "258", + "api_check_response_status_code": "200", + "api_check_response_status_text": "OK", + "run_location": "Singapore", + "ssl_days_remaining": "", + "ssl_check_domain": "", + "started_at": "2025-01-26T11:19:40.544Z", + "tags": "", + "link": "https://app.checklyhq.com/checks/927a2982-1007-4b81-b383-eae8bf717e61/check-sessions/478cacb1-c40f-4675-89d7-a4e3ecaafb7b", + "region": "", + "uuid": "4583208e-0bca-48c6-8dc8-d14faf6102b3" +} diff --git a/keep/providers/checkly_provider/checkly_provider.py b/keep/providers/checkly_provider/checkly_provider.py new file mode 100644 index 000000000..64f1736a2 --- /dev/null +++ b/keep/providers/checkly_provider/checkly_provider.py @@ -0,0 +1,258 @@ +""" +ChecklyProvider is a class that allows you to receive alerts from Checkly using API endpoints as well as webhooks. +""" + +import dataclasses + +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 +from keep.providers.models.provider_config import ProviderConfig, ProviderScope + +@pydantic.dataclasses.dataclass +class ChecklyProviderAuthConfig: + """ + ChecklyProviderAuthConfig is a class that allows you to authenticate in Checkly. + """ + + checklyApiKey: str = dataclasses.field( + metadata={ + "required": True, + "description": "Checkly API Key", + "sensitive": True, + }, + ) + + accountId: str = dataclasses.field( + metadata={ + "required": True, + "description": "Checkly Account ID", + "sensitive": True, + }, + ) + +class ChecklyProvider(BaseProvider): + """ + Get alerts from Checkly into Keep. + """ + + webhook_description = "" + webhook_template = "" + webhook_markdown = """ +💡 For more details on how to configure Checkly to send alerts to Keep, see the [Keep documentation](https://docs.keephq.dev/providers/documentation/checkly-provider). + +To send alerts from Checkly to Keep, Use the following webhook url to configure Checkly send alerts to Keep: + +1. In Checkly dashboard open "Alerts" tab. +2. Click on "Add more channels". +3. Select "Webhook" from the list. +4. Enter a name for the webhook, select the method as "POST" and enter the webhook URL as {keep_webhook_api_url}. +5. Copy the Body template from the [Keep documentation](https://docs.keephq.dev/providers/documentation/checkly-provider) and paste it in the Body field of the webhook. +6. Add a request header with the key "X-API-KEY" and the value as {api_key}. +7. Save the webhook. + """ + + PROVIDER_DISPLAY_NAME = "Checkly" + PROVIDER_TAGS = ["alert"] + PROVIDER_CATEGORY = ["Monitoring"] + + PROVIDER_SCOPES = [ + ProviderScope( + name="read_alerts", + description="Read alerts from Checkly", + ), + ] + + # Based on the Alert states in Checkly, we map them to the AlertStatus and AlertSeverity in Keep. + STATUS_MAP = { + "NO_ALERT": AlertStatus.RESOLVED, + "ALERT_DEGRADED": AlertStatus.FIRING, + "ALERT_FAILURE": AlertStatus.FIRING, + "ALERT_DEGRADED_REMAIN": AlertStatus.ACKNOWLEDGED, + "ALERT_DEGRADED_RECOVERY": AlertStatus.RESOLVED, + "ALERT_DEGRADED_FAILURE": AlertStatus.FIRING, + "ALERT_FAILURE_REMAIN": AlertStatus.ACKNOWLEDGED, + "ALERT_FAILURE_DEGRADED": AlertStatus.ACKNOWLEDGED, + "ALERT_RECOVERY": AlertStatus.RESOLVED + } + + SEVERITY_MAP = { + "NO_ALERT": AlertSeverity.INFO, + "ALERT_DEGRADED": AlertSeverity.WARNING, + "ALERT_FAILURE": AlertSeverity.CRITICAL, + "ALERT_DEGRADED_REMAIN": AlertSeverity.WARNING, + "ALERT_DEGRADED_RECOVERY": AlertSeverity.INFO, + "ALERT_DEGRADED_FAILURE": AlertSeverity.HIGH, + "ALERT_FAILURE_REMAIN": AlertSeverity.CRITICAL, + "ALERT_FAILURE_DEGRADED": AlertSeverity.WARNING, + "ALERT_RECOVERY": AlertSeverity.INFO + } + + def __init__( + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + ): + super().__init__(context_manager, provider_id, config) + + def dispose(self): + """ + Dispose the provider. + """ + pass + + def validate_config(self): + """ + Validates required configuration for ilert provider. + """ + self.authentication_config = ChecklyProviderAuthConfig( + **self.config.authentication + ) + + def validate_scopes(self): + """ + Validate scopes for the provider + """ + self.logger.info("Validating Checkly provider scopes") + try: + response = requests.get( + self.__get_url(), + headers=self.__get_auth_headers(), + ) + + if response.status_code != 200: + response.raise_for_status() + + self.logger.info("Successfully validated scopes", extra={"response": response.json()}) + + return {"read_alerts": True} + + except Exception as e: + self.logger.exception("Failed to validate scopes", extra={"error": e}) + return {"read_alerts": str(e)} + + def _get_alerts(self) -> list[AlertDto]: + """ + Get alerts from Checkly. + """ + self.logger.info("Getting alerts from Checkly") + alerts = self.__get_paginated_data() + return [ + AlertDto( + id=alert["id"], + name=alert["name"], + status=ChecklyProvider.STATUS_MAP[alert["alertType"]], + severity=ChecklyProvider.SEVERITY_MAP[alert["alertType"]], + lastReceivedAt=alert["created_at"], + alertType=alert["alertType"], + checkId=alert["checkId"], + checkType=alert["checkType"], + runLocation=alert["runLocation"], + responseTime=alert["responseTime"], + error=alert["error"], + statusCode=alert["statusCode"], + created_at=alert["created_at"], + startedAt=alert["startedAt"], + source=["checkly"] + ) for alert in alerts + ] + + @staticmethod + def _format_alert( + event: dict, provider_instance: "BaseProvider" = None + ) -> AlertDto | list[AlertDto]: + alert = AlertDto( + id=event["uuid"], + name=event["check_name"], + description=event["event"], + status=ChecklyProvider.STATUS_MAP[event["alert_type"]], + severity=ChecklyProvider.SEVERITY_MAP[event["alert_type"]], + lastReceived=event["started_at"], + alertType=event["alert_type"], + groupName=event["group_name"], + checkId=event["check_id"], + checkType=event["check_type"], + checkResultId=event["check_result_id"], + checkErrorMessage=event["check_error_message"], + responseTime=event["response_time"], + apiCheckResponseStatus=event["api_check_response_status_code"], + apiCheckResponseStatusText=event["api_check_response_status_text"], + runLocation=event["run_location"], + sslDaysRemaining=event["ssl_days_remaining"], + sslCheckDomain=event["ssl_check_domain"], + startedAt=event["started_at"], + tags=event["tags"], + url=event["link"], + region=event["region"], + source=["checkly"] + ) + + return alert + + + def __get_auth_headers(self): + return { + "Authorization": f"Bearer {self.authentication_config.checklyApiKey}", + "X-Checkly-Account": self.authentication_config.accountId, + "accept": "application/json" + } + + def __get_paginated_data(self, query_params: dict = {}) -> list: + data = [] + page = 1 + + while True: + self.logger.info(f"Getting data from page {page}") + query_params["page"] = page + try: + url = self.__get_url(query_params) + headers = self.__get_auth_headers() + response = requests.get(url, headers=headers) + response.raise_for_status() + page_data = response.json() + if not page_data: + break + self.logger.info(f"Got {len(page_data)} data from page {page}") + data.extend(page_data) + page += 1 + except Exception as e: + self.logger.error(f"Error getting data from page {page}: {e}") + break + return data + + def __get_url(self, query_params: dict = {}): + url = "https://api.checklyhq.com/v1/check-alerts" + if query_params: + url += "?" + for key, value in query_params.items(): + url += f"{key}={value}&" + url = url[:-1] + return url + +if __name__ == "__main__": + import logging + + logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()]) + context_manager = ContextManager( + tenant_id="singletenant", + workflow_id="test", + ) + + import os + + checkly_api_key = os.getenv("CHECKLY_API_KEY") + checkly_account_id = os.getenv("CHECKLY_ACCOUNT_ID") + + config = ProviderConfig( + description="Checkly Provider", + authentication={ + "checklyApiKey": checkly_api_key, + "accountId": checkly_account_id, + } + ) + + provider = ChecklyProvider(context_manager, "checkly", config) + + alerts = provider.get_alerts() + print(alerts)