Skip to content

Commit 9157eb3

Browse files
35C4n0rtalborenMatvey-Kukezhil56x
authored
feat: add incidentio provider (#1313)
Signed-off-by: 35C4n0r <[email protected]> Co-authored-by: Tal <[email protected]> Co-authored-by: Matvey Kukuy <[email protected]> Co-authored-by: Ezhil Shanmugham <[email protected]>
1 parent 65aa736 commit 9157eb3

File tree

6 files changed

+303
-0
lines changed

6 files changed

+303
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ Workflow triggers can either be executed manually when an alert is activated or
196196
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
197197
<img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/netdata-icon.png?raw=true"/>
198198
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
199+
<img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/incidentio-icon.png?raw=true"/>
200+
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
199201
<img width=32 height=32 src="https://github.com/keephq/keep/blob/main/keep-ui/public/icons/grafana_incident-icon.png?raw=true"/>
200202
</p>
201203
<h3 align="center">Ticketing tools</h2>

docs/mint.json

+1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@
100100
"providers/documentation/grafana-provider",
101101
"providers/documentation/http-provider",
102102
"providers/documentation/ilert-provider",
103+
"providers/documentation/incidentio-provider",
103104
"providers/documentation/jira-provider",
104105
"providers/documentation/kibana-provider",
105106
"providers/documentation/kubernetes-provider",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
title: "Incident.io Provider"
3+
sidebarTitle: "Incident.io Provider"
4+
description: "The Incident.io provider enables the querying of incidents on Incident.io, leveraging incident management capabilities for effective response."
5+
---
6+
7+
## Overview
8+
9+
The Incident.io provider facilitates interaction with Incident.io's API, allowing for the management of incidents. This includes the ability to query specific incidents, retrieve all incidents, and manage incident details. This provider integrates Keep's system with Incident.io's robust incident management platform.
10+
11+
12+
### Query Specific Incident
13+
14+
- `incident_id`: The ID of the incident to be queried. Required for fetching specific incident details.
15+
16+
## Outputs
17+
18+
Returns the specific incident with id=`incident_id`
19+
20+
## Authentication Parameters
21+
22+
- `incidentIoApiKey`: API key for authenticating with Incident.io's API.
23+
24+
## Scopes
25+
26+
- `authenticated`: Mandatory for all operations, ensures the user is authenticated.
27+
- `read_access`: Mandatory for querying incidents, ensures the user has read access.
28+
29+
## Connecting with the Provider
30+
31+
### API Key
32+
33+
To use the Incident.io API:
34+
1. Log in to your Incident.io account.
35+
2. Navigate to the "API Keys" section under your account settings.
36+
3. Generate a new API key or use an existing one.
37+
4. Ensure it has `read` permissions enabled for reading and managing incidents.
38+
39+
### Incident Endpoint
40+
41+
The Incident.io incident endpoint allows querying and managing incidents. Operations include retrieving specific incident details or fetching a list of all incidents. This is crucial for monitoring and responding to incidents efficiently.
42+
43+
For more details, refer to the [Incident.io API Documentation](https://api-docs.incident.io/).
44+
45+
## Notes
46+
47+
This provider is part of Keep's integration with Incident.io, designed to enhance operational resilience by enabling efficient incident management and response.
48+
49+
## Useful Links
50+
51+
- [Incident.io API Documentation](https://api-docs.incident.io/)
52+
- [Incident.io Incidents](https://api-docs.incident.io/tag/Incidents-V2)
53+
- [Incident.io Api_Keys and Permissions](https://help.incident.io/en/articles/6149651-our-api)
4.39 KB
Loading

keep/providers/incidentio_provider/__init__.py

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
"""
2+
IncidentioProvider is a class that allows to get all incidents as well query specific incidents in Incidentio.
3+
"""
4+
5+
import dataclasses
6+
from typing import List
7+
from urllib.parse import urlencode, urljoin
8+
9+
import pydantic
10+
import requests
11+
12+
from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus
13+
from keep.contextmanager.contextmanager import ContextManager
14+
from keep.providers.base.base_provider import BaseProvider
15+
from keep.providers.models.provider_config import ProviderConfig, ProviderScope
16+
17+
18+
class ResourceAlreadyExists(Exception):
19+
def __init__(self, *args):
20+
super().__init__(*args)
21+
22+
23+
@pydantic.dataclasses.dataclass
24+
class IncidentioProviderAuthConfig:
25+
"""
26+
Incidentio authentication configuration.
27+
"""
28+
incidentIoApiKey: str = dataclasses.field(
29+
metadata={
30+
"required": True,
31+
"description": "IncidentIO's API_KEY",
32+
"hint": "API KEY for incident.io",
33+
"sensitive": True,
34+
},
35+
)
36+
37+
38+
class IncidentioProvider(BaseProvider):
39+
"""Receive Incidents from Incidentio."""
40+
41+
PROVIDER_SCOPES = [
42+
ProviderScope(
43+
name="authenticated",
44+
description="User is Authenticated",
45+
mandatory=True,
46+
alias="authenticated",
47+
),
48+
ProviderScope(
49+
name="read_access",
50+
description="User has read access",
51+
mandatory=True,
52+
alias="can_read",
53+
)
54+
]
55+
56+
SEVERITIES_MAP = {
57+
"Warning": AlertSeverity.WARNING,
58+
"Major": AlertSeverity.HIGH,
59+
"Info": AlertSeverity.INFO,
60+
"Critical": AlertSeverity.CRITICAL,
61+
"Minor": AlertSeverity.LOW
62+
}
63+
64+
STATUS_MAP = {
65+
"triage": AlertStatus.ACKNOWLEDGED,
66+
"declined": AlertStatus.SUPPRESSED,
67+
"merged": AlertStatus.RESOLVED,
68+
"canceled": AlertStatus.SUPPRESSED,
69+
"live": AlertStatus.FIRING,
70+
"learning": AlertStatus.PENDING,
71+
"closed": AlertStatus.RESOLVED,
72+
"paused": AlertStatus.SUPPRESSED
73+
}
74+
75+
def __init__(
76+
self, context_manager: ContextManager, provider_id: str, config: ProviderConfig
77+
):
78+
super().__init__(context_manager, provider_id, config)
79+
80+
def dispose(self):
81+
"""
82+
Dispose the provider.
83+
"""
84+
pass
85+
86+
def validate_config(self):
87+
"""
88+
Validates required configuration for Incidentio provider.
89+
"""
90+
self.authentication_config = IncidentioProviderAuthConfig(
91+
**self.config.authentication
92+
)
93+
94+
def __get_url(self, paths: List[str] = [], query_params: dict = None, **kwargs):
95+
"""
96+
Helper method to build the url for Incidentio api requests.
97+
98+
Example:
99+
100+
paths = ["issue", "createmeta"]
101+
query_params = {"projectKeys": "key1"}
102+
url = __get_url("test", paths, query_params)
103+
104+
# url = https://incidentio.com/api/2/issue/createmeta?projectKeys=key1
105+
"""
106+
107+
base_url = "https://api.incident.io/v2/"
108+
path_str = "/".join(str(path) for path in paths)
109+
url = urljoin(base_url, path_str)
110+
111+
# add query params
112+
if query_params:
113+
url = f"{url}?{urlencode(query_params)}"
114+
115+
return url
116+
117+
def __get_headers(self):
118+
"""
119+
Building the headers for api requests
120+
"""
121+
return {
122+
'Authorization': f'Bearer {self.authentication_config.incidentIoApiKey}',
123+
}
124+
125+
def validate_scopes(self) -> dict[str, bool | str]:
126+
self.logger.info("Validating IncidentIO scopes...")
127+
try:
128+
print(self.__get_url(paths=["incidents"]))
129+
response = requests.get(
130+
url=self.__get_url(paths=["incidents"]),
131+
timeout=10,
132+
headers=self.__get_headers(),
133+
)
134+
135+
if response.ok:
136+
return {"authenticated": True, "read_access": True}
137+
else:
138+
self.logger.error(f"Failed to validate scopes: {response.status_code}")
139+
scopes = {"authenticated": "Unable to query incidents: {response.status_code}", "read_access": False}
140+
except Exception as e:
141+
self.logger.error("Error getting IncidentIO scopes:", extra={"exception": str(e)})
142+
scopes = {"authenticated": "Unable to query incidents: {e}", "read_access": False}
143+
144+
return scopes
145+
146+
def _query(self, incident_id, **kwargs) -> AlertDto:
147+
"""query IncidentIO Incident"""
148+
self.logger.info("Querying IncidentIO incident",
149+
extra={
150+
"incident_id": incident_id,
151+
**kwargs,
152+
}, )
153+
try:
154+
response = requests.get(
155+
url=self.__get_url(paths=["incidents", incident_id]),
156+
headers=self.__get_headers(),
157+
)
158+
except Exception as e:
159+
self.logger.error("Error while fetching Incident",
160+
extra={"incident_id": incident_id, "kwargs": kwargs, "exception": str(e)})
161+
raise e
162+
else:
163+
if response.ok:
164+
res = response.json()
165+
return self.__map_alert_to_AlertDTO({"event": res})
166+
else:
167+
self.logger.error("Error while fetching Incident",
168+
extra={"incident_id": incident_id, "kwargs": kwargs, "res": response.text})
169+
170+
def _get_alerts(self) -> list[AlertDto]:
171+
alerts = []
172+
next_page = None
173+
174+
while True:
175+
try:
176+
params = {'page_size': 100}
177+
if next_page:
178+
params['after'] = next_page
179+
180+
response = requests.get(self.__get_url(paths=["incidents"]), headers=self.__get_headers(), params=params,
181+
timeout=15)
182+
response.raise_for_status()
183+
except requests.RequestException as e:
184+
self.logger.error("Error getting IncidentIO scopes:", extra={"exception": str(e)})
185+
raise e
186+
else:
187+
data = response.json()
188+
try:
189+
for incident in data.get('incidents', []):
190+
alerts.append(
191+
self.__map_alert_to_AlertDTO(incident)
192+
)
193+
except Exception as e:
194+
self.logger.error("Error while mapping incidents to AlertDTO", extra={"exception": str(e)})
195+
raise e
196+
pagination_meta = data.get('pagination_meta', {})
197+
next_page = pagination_meta.get('after')
198+
199+
if not next_page:
200+
break
201+
202+
return alerts
203+
204+
def __map_alert_to_AlertDTO(self, incident) -> AlertDto:
205+
return AlertDto(
206+
id=incident['id'],
207+
fingerprint=incident['id'],
208+
name=incident['name'],
209+
status=IncidentioProvider.STATUS_MAP[incident['incident_status']["category"]],
210+
severity=IncidentioProvider.SEVERITIES_MAP.get(
211+
incident.get("severity", {}).get("name", "minor"), AlertSeverity.WARNING),
212+
lastReceived=incident.get('created_at'),
213+
description=incident.get('summary', ""),
214+
apiKeyRef=incident["creator"]["api_key"]["id"],
215+
assignee=", ".join(
216+
assignment["role"]["name"] for assignment in
217+
incident["incident_role_assignments"]),
218+
url=incident.get("permalink", "https://app.incident.io/")
219+
)
220+
221+
if __name__ == "__main__":
222+
import logging
223+
224+
logging.basicConfig(level=logging.DEBUG, handlers=[logging.StreamHandler()])
225+
context_manager = ContextManager(
226+
tenant_id="singletenant",
227+
workflow_id="test",
228+
)
229+
230+
import os
231+
232+
api_key = os.getenv("INCIDENTIO_API_KEY")
233+
234+
config = ProviderConfig(
235+
description="Incidentio Provider",
236+
authentication={
237+
"incidentIoApiKey": api_key
238+
},
239+
)
240+
241+
provider = IncidentioProvider(
242+
context_manager,
243+
provider_id="incidentio_provider",
244+
config=config,
245+
)
246+
print(provider.validate_scopes())
247+
print(provider._get_alerts())

0 commit comments

Comments
 (0)