Skip to content

Commit d0d8485

Browse files
authored
Merge pull request #122 from GabrielSalla/create-server-routes
Create server routes
2 parents c3e3318 + 804a3b1 commit d0d8485

File tree

10 files changed

+358
-13
lines changed

10 files changed

+358
-13
lines changed

src/components/http_server/alert_routes.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,64 @@
33
from aiohttp.web_response import Response
44

55
import commands as commands
6-
from models import Alert
6+
from models import Alert, Issue, IssueStatus
7+
from utils.time import localize
78

89
alert_routes = web.RouteTableDef()
910
base_route = "/alert"
1011

1112

13+
@alert_routes.get(base_route + "/{alert_id}")
14+
@alert_routes.get(base_route + "/{alert_id}/")
15+
async def get_alert(request: Request) -> Response:
16+
"""Route to get the information for an alert"""
17+
alert_id = int(request.match_info["alert_id"])
18+
19+
alert = await Alert.get_by_id(alert_id)
20+
if not alert:
21+
error_response = {"status": "error", "message": f"alert '{alert_id}' not found"}
22+
return web.json_response(error_response, status=404)
23+
24+
response = {
25+
"id": alert.id,
26+
"status": alert.status.value,
27+
"acknowledged": alert.acknowledged,
28+
"locked": alert.locked,
29+
"priority": alert.priority,
30+
"acknowledge_priority": alert.acknowledge_priority,
31+
"can_acknowledge": alert.can_acknowledge,
32+
"can_lock": alert.can_lock,
33+
"can_solve": alert.can_solve,
34+
"created_at": localize(alert.created_at).strftime("%Y-%m-%d %H:%M:%S"),
35+
}
36+
return web.json_response(response)
37+
38+
39+
@alert_routes.get(base_route + "/{alert_id}/issues")
40+
@alert_routes.get(base_route + "/{alert_id}/issues/")
41+
async def list_alert_active_issues(request: Request) -> Response:
42+
"""List active issues for an alert"""
43+
alert_id = int(request.match_info["alert_id"])
44+
45+
issues = await Issue.get_all(
46+
Issue.alert_id == alert_id,
47+
Issue.status == IssueStatus.active,
48+
order_by=[Issue.id],
49+
)
50+
51+
response = [
52+
{
53+
"id": issue.id,
54+
"status": issue.status.value,
55+
"model_id": issue.model_id,
56+
"data": issue.data,
57+
"created_at": localize(issue.created_at).strftime("%Y-%m-%d %H:%M:%S"),
58+
}
59+
for issue in issues
60+
]
61+
return web.json_response(response)
62+
63+
1264
@alert_routes.post(base_route + "/{alert_id}/acknowledge")
1365
@alert_routes.post(base_route + "/{alert_id}/acknowledge/")
1466
async def alert_acknowledge(request: Request) -> Response:

src/components/http_server/monitor_routes.py

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import logging
22
import traceback
3+
from collections import Counter
34
from typing import Any
45

56
import pydantic
@@ -10,7 +11,8 @@
1011
import commands
1112
from components.http_server.format_monitor_name import format_monitor_name
1213
from components.monitors_loader import MonitorValidationError
13-
from models import CodeModule, Monitor
14+
from models import Alert, AlertStatus, CodeModule, Monitor
15+
from utils.time import localize
1416

1517
_logger = logging.getLogger("monitor_routes")
1618

@@ -22,21 +24,65 @@
2224
@monitor_routes.get(base_route + "/list")
2325
@monitor_routes.get(base_route + "/list/")
2426
async def list_monitors(request: Request) -> Response:
25-
monitors = await Monitor.get_raw(columns=[Monitor.id, Monitor.name, Monitor.enabled])
27+
"""Route to list all monitors"""
28+
monitors = await Monitor.get_all()
29+
enabled_monitors = (monitor for monitor in monitors if monitor.enabled)
30+
31+
active_alerts = await Alert.get_raw(
32+
columns=[Alert.monitor_id],
33+
column_filters=[
34+
Alert.status == AlertStatus.active,
35+
Alert.monitor_id.in_(monitor.id for monitor in enabled_monitors),
36+
],
37+
)
38+
alerts_counter = Counter(alert.monitor_id for alert in active_alerts)
39+
2640
response = [
2741
{
28-
"id": monitor[0],
29-
"name": monitor[1],
30-
"enabled": monitor[2],
42+
"id": monitor.id,
43+
"name": monitor.name,
44+
"enabled": monitor.enabled,
45+
"active_alerts": alerts_counter.get(monitor.id, 0),
3146
}
3247
for monitor in monitors
3348
]
3449
return web.json_response(response)
3550

3651

52+
@monitor_routes.get(base_route + "/{monitor_id}/alerts")
53+
@monitor_routes.get(base_route + "/{monitor_id}/alerts/")
54+
async def list_monitor_active_alerts(request: Request) -> Response:
55+
"""Route to list active alerts for a monitor"""
56+
monitor_id = int(request.match_info["monitor_id"])
57+
58+
alerts = await Alert.get_all(
59+
Alert.monitor_id == monitor_id,
60+
Alert.status == AlertStatus.active,
61+
order_by=[Alert.id],
62+
)
63+
64+
response = [
65+
{
66+
"id": alert.id,
67+
"status": alert.status.value,
68+
"acknowledged": alert.acknowledged,
69+
"locked": alert.locked,
70+
"priority": alert.priority,
71+
"acknowledge_priority": alert.acknowledge_priority,
72+
"can_acknowledge": alert.can_acknowledge,
73+
"can_lock": alert.can_lock,
74+
"can_solve": alert.can_solve,
75+
"created_at": localize(alert.created_at).strftime("%Y-%m-%d %H:%M:%S"),
76+
}
77+
for alert in alerts
78+
]
79+
return web.json_response(response)
80+
81+
3782
@monitor_routes.get(base_route + "/{monitor_name}")
3883
@monitor_routes.get(base_route + "/{monitor_name}/")
3984
async def get_monitor(request: Request) -> Response:
85+
"""Route to get a monitor by name"""
4086
monitor_name = request.match_info["monitor_name"]
4187

4288
monitor = await Monitor.get(Monitor.name == monitor_name)

src/models/alert.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ def can_lock(self) -> bool:
7474
"""Check if the alert can be locked"""
7575
return not self.locked
7676

77+
@property
78+
def can_solve(self) -> bool:
79+
"""Check if the alert can be solved"""
80+
return not self.issue_options.solvable
81+
7782
@staticmethod
7883
def calculate_priority(
7984
rule: AgeRule | CountRule | ValueRule, issues: list[Issue] | Sequence[Issue]

src/plugins/slack/notifications/slack_notification.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ async def _build_notification_buttons(
236236
text="Lock", action_id=f"sentinela_lock_{alert.id}", value=f"lock {alert.id}"
237237
)
238238
)
239-
if not monitor.code.issue_options.solvable:
239+
if alert.can_solve:
240240
buttons.append(
241241
slack.MessageButton(
242242
text="Solve", action_id=f"sentinela_solve_{alert.id}", value=f"solve {alert.id}"

src/utils/time.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ def now() -> datetime.datetime:
1212
return datetime.datetime.now(tz=timezone(configs.time_zone))
1313

1414

15+
def localize(dt: datetime.datetime) -> datetime.datetime:
16+
"""Localize a datetime object to the configured timezone"""
17+
return dt.astimezone(timezone(configs.time_zone))
18+
19+
1520
def format_datetime_iso(timestamp: datetime.datetime | None) -> str | None:
1621
return timestamp.isoformat(timespec="milliseconds") if timestamp is not None else None
1722

tests/components/http_server/test_alert_routes.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66

77
import components.controller.controller as controller
88
import components.http_server as http_server
9-
from models import Alert, Monitor
9+
from models import Alert, Issue, Monitor
1010
from tests.message_queue.utils import get_queue_items
11+
from utils.time import localize
1112

1213
pytestmark = pytest.mark.asyncio(loop_scope="session")
1314

@@ -23,6 +24,78 @@ async def setup_http_server():
2324
await http_server.wait_stop()
2425

2526

27+
async def test_get_alert(sample_monitor: Monitor):
28+
alerts = await Alert.create_batch(
29+
[
30+
Alert(
31+
monitor_id=sample_monitor.id,
32+
priority=1,
33+
),
34+
Alert(
35+
monitor_id=sample_monitor.id,
36+
priority=2,
37+
),
38+
]
39+
)
40+
41+
for alert in alerts:
42+
async with aiohttp.ClientSession() as session:
43+
async with session.get(BASE_URL + f"/{alert.id}") as response:
44+
assert await response.json() == {
45+
"id": alert.id,
46+
"status": alert.status.value,
47+
"acknowledged": alert.acknowledged,
48+
"locked": alert.locked,
49+
"priority": alert.priority,
50+
"acknowledge_priority": alert.acknowledge_priority,
51+
"can_acknowledge": alert.can_acknowledge,
52+
"can_lock": alert.can_lock,
53+
"can_solve": alert.can_solve,
54+
"created_at": localize(alert.created_at).strftime("%Y-%m-%d %H:%M:%S"),
55+
}
56+
57+
58+
async def test_get_alert_not_found(sample_monitor: Monitor):
59+
async with aiohttp.ClientSession() as session:
60+
async with session.get(BASE_URL + "/0") as response:
61+
assert response.status == 404
62+
assert await response.json() == {"status": "error", "message": "alert '0' not found"}
63+
64+
65+
@pytest.mark.parametrize("issues_count", range(4))
66+
async def test_list_alert_active_issues(sample_monitor: Monitor, issues_count):
67+
alert = await Alert.create(monitor_id=sample_monitor.id)
68+
69+
issues = await Issue.create_batch(
70+
[
71+
Issue(
72+
monitor_id=sample_monitor.id,
73+
alert_id=alert.id,
74+
)
75+
for _ in range(issues_count)
76+
]
77+
)
78+
79+
async with aiohttp.ClientSession() as session:
80+
async with session.get(BASE_URL + f"/{alert.id}/issues") as response:
81+
assert await response.json() == [
82+
{
83+
"id": issue.id,
84+
"status": issue.status.value,
85+
"model_id": issue.model_id,
86+
"data": issue.data,
87+
"created_at": localize(issue.created_at).strftime("%Y-%m-%d %H:%M:%S"),
88+
}
89+
for issue in issues
90+
]
91+
92+
93+
async def test_list_alert_active_issues_alert_not_found():
94+
async with aiohttp.ClientSession() as session:
95+
async with session.get(BASE_URL + "/0/issues") as response:
96+
assert await response.json() == []
97+
98+
2699
async def test_alert_acknowledge(clear_queue, sample_monitor: Monitor):
27100
"""The 'alert acknowledge' route should queue an request to acknowledge the provided alert"""
28101
alert = await Alert.create(monitor_id=sample_monitor.id)

tests/components/http_server/test_monitor_routes.py

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import components.http_server as http_server
1010
import components.monitors_loader as monitors_loader
1111
import databases as databases
12-
from models import CodeModule, Monitor
12+
from models import Alert, AlertStatus, CodeModule, Monitor
1313

1414
pytestmark = pytest.mark.asyncio(loop_scope="session")
1515

@@ -37,10 +37,103 @@ async def test_list_monitors(clear_database, sample_monitor: Monitor):
3737
"id": sample_monitor.id,
3838
"name": sample_monitor.name,
3939
"enabled": sample_monitor.enabled,
40+
"active_alerts": 0,
4041
},
4142
]
4243

4344

45+
@pytest.mark.parametrize("active_alerts", range(1, 5))
46+
async def test_list_monitors_with_alerts(clear_database, sample_monitor: Monitor, active_alerts):
47+
"""The 'monitor list' route should return a list of all monitors and the count of active alerts
48+
for them"""
49+
await Alert.create_batch(
50+
Alert(
51+
monitor_id=sample_monitor.id,
52+
priority=i,
53+
acknowledge_priority=5 - i,
54+
)
55+
for i in range(active_alerts)
56+
)
57+
await Alert.create(
58+
monitor_id=sample_monitor.id,
59+
status=AlertStatus.solved,
60+
priority=1,
61+
acknowledge_priority=1,
62+
)
63+
64+
url = BASE_URL + "/list"
65+
async with aiohttp.ClientSession() as session:
66+
async with session.get(url) as response:
67+
response_data = await response.json()
68+
69+
assert response_data == [
70+
{
71+
"id": sample_monitor.id,
72+
"name": sample_monitor.name,
73+
"enabled": sample_monitor.enabled,
74+
"active_alerts": active_alerts,
75+
},
76+
]
77+
78+
79+
async def test_list_monitors_not_enabled(clear_database, sample_monitor: Monitor):
80+
"""The 'monitor list' route should return a list of all monitors and the count of active alerts
81+
for them"""
82+
await Alert.create(
83+
monitor_id=sample_monitor.id,
84+
priority=1,
85+
acknowledge_priority=1,
86+
)
87+
sample_monitor.enabled = False
88+
await sample_monitor.save()
89+
90+
url = BASE_URL + "/list"
91+
async with aiohttp.ClientSession() as session:
92+
async with session.get(url) as response:
93+
response_data = await response.json()
94+
95+
assert response_data == [
96+
{
97+
"id": sample_monitor.id,
98+
"name": sample_monitor.name,
99+
"enabled": sample_monitor.enabled,
100+
"active_alerts": 0,
101+
},
102+
]
103+
104+
105+
@pytest.mark.parametrize("alerts_number", [1, 2])
106+
async def test_list_monitor_active_alerts(clear_database, alerts_number, sample_monitor: Monitor):
107+
"""The 'monitor active alerts' route should return a list of all active alerts for a monitor"""
108+
alerts = await Alert.create_batch(
109+
Alert(
110+
monitor_id=sample_monitor.id,
111+
priority=i,
112+
acknowledge_priority=5 - 1,
113+
)
114+
for i in range(alerts_number)
115+
)
116+
117+
url = BASE_URL + f"/{sample_monitor.id}/alerts"
118+
async with aiohttp.ClientSession() as session:
119+
async with session.get(url) as response:
120+
response_data = await response.json()
121+
122+
assert isinstance(response_data, list)
123+
assert len(response_data) == alerts_number
124+
for alert, response_alert in zip(alerts, response_data):
125+
assert alert.id == response_alert["id"]
126+
assert alert.status == response_alert["status"]
127+
assert alert.acknowledged == response_alert["acknowledged"]
128+
assert alert.locked == response_alert["locked"]
129+
assert alert.priority == response_alert["priority"]
130+
assert alert.acknowledge_priority == response_alert["acknowledge_priority"]
131+
assert alert.can_acknowledge == response_alert["can_acknowledge"]
132+
assert alert.can_lock == response_alert["can_lock"]
133+
assert alert.can_solve == response_alert["can_solve"]
134+
assert alert.created_at.strftime("%Y-%m-%d %H:%M:%S") == response_alert["created_at"]
135+
136+
44137
async def test_get_monitor(clear_database, sample_monitor: Monitor):
45138
"""The 'monitor get' route should return the monitor attributes and code information"""
46139
code_module = await CodeModule.get(CodeModule.monitor_id == sample_monitor.id)

0 commit comments

Comments
 (0)