Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions docs/monitor.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,24 +268,21 @@ Reactions are defined as a list of **async functions** that are triggered when s
Below is an example of defining a reaction function that responds to the creation of a new issue:

```python
async def reaction_issue_created(event_payload: dict[str, Any]) -> None:
from monitor_utils import EventPayload


async def reaction_issue_created(event_payload: EventPayload) -> None:
# Do something
```

### Event payload
The event payload provided to each reaction function contains structured information about the event source, details, and any additional context. This allows reaction functions to respond precisely to specific events.

```python
{
"event_source": "Specifies the model that generated the event (e.g., `monitor`, `issue`, `alert`)."
"event_source_id": "The unique identifier of the object that triggered the event (e.g., `monitor_id`, `issue_id`)."
"event_source_monitor_id": "The monitor ID associated with the object that generated the event."
"event_name": "Name of the event, such as `alert_created` or `issue_solved`.",
"event_data": {
"Object with detailed information about the event source."
},
"extra_payload": "Additional information that may be sent along with the event, providing further context.",
}
```
- `event_source`: Specifies the model that generated the event (e.g., `monitor`, `issue`, `alert`).
- `event_source_id`: The unique identifier of the object that triggered the event (e.g., `monitor_id`, `issue_id`).
- `event_source_monitor_id`: The monitor ID associated with the object that generated the event.
- `event_name`: Name of the event (e.g., `alert_created`, `issue_solved`).
- `event_data`: Dictionary with detailed information about the event source.
- `extra_payload`: Additional information that may be sent along with the event, providing further context.

Reaction functions can be assigned to specific events when creating an instance of `ReactionOptions`. This configuration ensures that designated functions are triggered whenever specified events occur.

Expand Down
14 changes: 10 additions & 4 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,21 +36,27 @@ Notifications are used by monitors, usually to notify about an event. Each notif

Notifications must have the structure defined by the `src.notifications.base_notification.BaseNotification` **protocol**. The method `reactions_list` returns a list of reactions that will trigger an action. Each reaction is a tuple with the reaction name and a list of coroutines that must be called when the reaction is triggered.

Notification base structure:
Notification base structure must be as follows:
```python
from data_models.monitor_options import reaction_function_type


class Notification:
min_priority_to_send: int = 5

def reactions_list(self) -> list[tuple[str, list[Coroutine | partial[Coroutine]]]]:
def reactions_list(self) -> list[tuple[str, list[reaction_function_type]]]:
...
```

A notification can have more parameters necessary to control their behavior. An example of a notification implementation is:
An example of a notification implementation is shown bellow, where there are 3 different events with reactions set for them.
```python
from data_models.monitor_options import reaction_function_type


class MyNotification:
min_priority_to_send: int = 5

def reactions_list(self) -> list[tuple[str, list[Coroutine | partial[Coroutine]]]]:
def reactions_list(self) -> list[tuple[str, list[reaction_function_type]]]:
"""Get a list of events that the notification will react to"""
return [
("alert_acknowledged", [handle_event_acknowledged]),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@
"""

import os
from typing import Any, TypedDict, cast
from typing import TypedDict, cast

from databases import query_application
from models import Notification, NotificationStatus
from monitor_utils import (
AgeRule,
AlertOptions,
AlertPriority,
EventPayload,
IssueOptions,
MonitorOptions,
PriorityLevels,
Expand Down Expand Up @@ -69,9 +70,9 @@ def is_solved(issue_data: IssueDataType) -> bool:
# Reactions


async def close_notification(event_payload: dict[str, Any]) -> None:
async def close_notification(event_payload: EventPayload) -> None:
"""Fix the notification by closing it"""
issue_object = event_payload["event_data"]
issue_object = event_payload.event_data
notification = await Notification.get_by_id(issue_object["data"]["notification_id"])
if notification:
await notification.close()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ slack-bolt = "^1.22.0"

[tool.ruff]
include = ["src/**/*.py", "tests/**/*.py", "internal_monitors/**/*.py", "sample_monitors/**/*.py", "tools/**/*.py"]
lint.extend-select = ["E", "F", "Q", "I", "RET"]
lint.extend-select = ["E", "W", "F", "Q", "I", "RET"]
line-length = 100

[tool.ruff.lint.isort]
Expand Down
13 changes: 7 additions & 6 deletions src/components/executor/reaction_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import registry as registry
from base_exception import BaseSentinelaException
from configs import configs
from data_models.event_payload import EventPayload
from models import Monitor

_logger = logging.getLogger("reaction_handler")
Expand All @@ -34,9 +35,9 @@
async def run(message: dict[Any, Any]) -> None:
"""Process a message with type 'event' using the monitor's defined list of reactions for the
event. The execution timeout is for each function individually"""
message_payload = message["payload"]
monitor_id = message_payload["event_source_monitor_id"]
event_name = message_payload["event_name"]
event_payload = EventPayload(**message["payload"])
monitor_id = event_payload.event_source_monitor_id
event_name = event_payload.event_name

monitor = await Monitor.get_by_id(monitor_id)
if monitor is None:
Expand Down Expand Up @@ -68,19 +69,19 @@ async def run(message: dict[Any, Any]) -> None:
reaction_execution_time = prometheus_reaction_execution_time.labels(**prometheus_labels)
try:
with reaction_execution_time.time():
await asyncio.wait_for(reaction(message_payload), configs.executor_reaction_timeout)
await asyncio.wait_for(reaction(event_payload), configs.executor_reaction_timeout)
except asyncio.TimeoutError:
prometheus_reaction_timeout_count.labels(**prometheus_labels).inc()
_logger.error(
f"Timed out executing reaction '{reaction_name}' with payload "
f"'{json.dumps(message_payload)}'"
f"'{json.dumps(event_payload.to_dict())}'"
)
except BaseSentinelaException as e:
raise e
except Exception:
prometheus_reaction_error_count.labels(**prometheus_labels).inc()
_logger.error(
f"Error executing reaction '{reaction_name}' with payload "
f"'{json.dumps(message_payload)}'"
f"'{json.dumps(event_payload.to_dict())}'"
)
_logger.error(traceback.format_exc().strip())
2 changes: 1 addition & 1 deletion src/components/monitors_loader/monitor_module_type.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import Protocol, TypedDict

from data_models.monitor_options import AlertOptions, IssueOptions, MonitorOptions, ReactionOptions
from notifications import BaseNotification
from options import AlertOptions, IssueOptions, MonitorOptions, ReactionOptions


class MonitorModule(Protocol):
Expand Down
2 changes: 1 addition & 1 deletion src/components/monitors_loader/monitors_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@
import registry as registry
import utils.app as app
from configs import configs
from data_models.monitor_options import ReactionOptions
from models import CodeModule, Monitor
from options import ReactionOptions
from utils.exception_handling import catch_exceptions
from utils.time import now, time_since, time_until_next_trigger

Expand Down
File renamed without changes.
3 changes: 3 additions & 0 deletions src/data_models/event_payload/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .event_payload import EventPayload

__all__ = ["EventPayload"]
33 changes: 33 additions & 0 deletions src/data_models/event_payload/event_payload.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from typing import Any

from pydantic.dataclasses import dataclass


@dataclass
class EventPayload:
"""The event payload provided to each reaction function contains structured information about
the event source, details, and any additional context.
- `event_source`: Specifies the model that generated the event (e.g., `monitor`, `issue`,
`alert`).
- `event_source_id`: The unique identifier of the object that triggered the event (e.g.,
`monitor_id`, `issue_id`).
- `event_source_monitor_id`: The monitor ID associated with the object that generated the event.
- `event_name`: Name of the event (e.g., `alert_created`, `issue_solved`).
- `event_data`: Dictionary with detailed information about the event source.
- `extra_payload`: Additional information that may be sent along with the event, providing
further context.
"""

event_source: str
event_source_id: int
event_source_monitor_id: int
event_name: str
event_data: dict[str, Any]
extra_payload: dict[str, Any] | None = None

def to_dict(self) -> dict[str, Any]:
return {
field: value
for field in self.__dataclass_fields__
if (value := getattr(self, field)) is not None
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from .options import (
from .monitor_options import (
AgeRule,
AlertOptions,
CountRule,
Expand All @@ -7,6 +7,7 @@
PriorityLevels,
ReactionOptions,
ValueRule,
reaction_function_type,
)

__all__ = [
Expand All @@ -18,4 +19,5 @@
"PriorityLevels",
"ReactionOptions",
"ValueRule",
"reaction_function_type",
]
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pydantic.dataclasses import dataclass

from configs import configs
from data_models.event_payload import EventPayload


@dataclass
Expand Down Expand Up @@ -120,7 +121,7 @@ class AlertOptions:
dismiss_acknowledge_on_new_issues: bool = False


reaction_function_type = Callable[[dict[str, Any]], Coroutine[Any, Any, Any]]
type reaction_function_type = Callable[[EventPayload], Coroutine[Any, Any, Any]]


@dataclass
Expand All @@ -137,23 +138,6 @@ class ReactionOptions:
event source, details, and any additional context. This allows reaction functions to respond
precisely to specific events.

```python
{
"event_source": "Specifies the model that generated the event (e.g., `monitor`, `issue`,
`alert`)."
"event_source_id": "The unique identifier of the object that triggered the event (e.g.,
`monitor_id`, `issue_id`)."
"event_source_monitor_id": "The monitor ID associated with the object that generated the
event."
"event_name": "Name of the event, such as `alert_created` or `issue_solved`.",
"event_data": {
"Object with detailed information about the event source."
},
"extra_payload": "Additional information that may be sent along with the event, providing
further context.",
}
```

Check the documentation for a more detailed explanation of each event.
"""

Expand Down
2 changes: 1 addition & 1 deletion src/models/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from sqlalchemy.orm import Mapped, mapped_column

import models.utils.priority as priority_utils
from options import AgeRule, AlertOptions, CountRule, IssueOptions, ValueRule
from data_models.monitor_options import AgeRule, AlertOptions, CountRule, IssueOptions, ValueRule
from registry import get_monitor_module
from utils.async_tools import do_concurrently
from utils.time import now
Expand Down
2 changes: 1 addition & 1 deletion src/models/issue.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from sqlalchemy.ext.mutable import MutableDict
from sqlalchemy.orm import Mapped, mapped_column

from data_models.monitor_options import IssueOptions
from internal_database import CallbackSession
from options import IssueOptions
from registry import get_monitor_module
from utils.time import now

Expand Down
2 changes: 1 addition & 1 deletion src/models/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from sqlalchemy.orm import Mapped, mapped_column, reconstructor

import utils.time as time_utils
from options import AlertOptions, IssueOptions, MonitorOptions, ReactionOptions
from data_models.monitor_options import AlertOptions, IssueOptions, MonitorOptions, ReactionOptions
from registry import get_monitor_module

from .alert import Alert, AlertStatus
Expand Down
2 changes: 1 addition & 1 deletion src/models/utils/priority.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import enum
from typing import Callable, Sequence, cast

from data_models.monitor_options import AgeRule, CountRule, ValueRule
from models.issue import Issue
from options import AgeRule, CountRule, ValueRule
from utils.time import time_since

_operators: dict[str, Callable[[int | float, int | float], bool]] = {
Expand Down
2 changes: 1 addition & 1 deletion src/module_loader/checker.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
from types import ModuleType
from typing import Any, Callable, Optional, _TypedDictMeta # type: ignore[attr-defined]

from data_models.monitor_options import AlertOptions, IssueOptions, MonitorOptions, ReactionOptions
from notifications import BaseNotification
from options import AlertOptions, IssueOptions, MonitorOptions, ReactionOptions

_logger = logging.getLogger("module_check")
_logger.setLevel(logging.INFO)
Expand Down
8 changes: 5 additions & 3 deletions src/monitor_utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@
# all these imports should be able to be transported somewhere else, or at least there
# should be a mock for them

from databases import query
from models.utils.priority import AlertPriority
from options import (
from data_models.event_payload import EventPayload
from data_models.monitor_options import (
AgeRule,
AlertOptions,
CountRule,
Expand All @@ -14,6 +13,8 @@
ReactionOptions,
ValueRule,
)
from databases import query
from models.utils.priority import AlertPriority

from .read_file import read_file

Expand All @@ -22,6 +23,7 @@
"AlertOptions",
"AlertPriority",
"CountRule",
"EventPayload",
"IssueOptions",
"MonitorOptions",
"PriorityLevels",
Expand Down
6 changes: 3 additions & 3 deletions src/notifications/base_notification.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from typing import Any, Callable, Coroutine, Protocol, runtime_checkable
from typing import Protocol, runtime_checkable

type async_function = Callable[[dict[str, Any]], Coroutine[Any, Any, Any]]
from data_models.monitor_options import reaction_function_type


@runtime_checkable
class BaseNotification(Protocol):
min_priority_to_send: int = 5

def reactions_list(self) -> list[tuple[str, list[async_function]]]: ...
def reactions_list(self) -> list[tuple[str, list[reaction_function_type]]]: ...
8 changes: 2 additions & 6 deletions src/plugins/slack/actions/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,8 @@ async def _resend_notification(notification: Notification) -> None:
if notification_option.channel == notification.data["channel"]:
# Clear the notification and send it again
await slack_notification.clear_slack_notification(notification)
await slack_notification.slack_notification(
event_payload={
"event_data": {
"id": notification.alert_id,
}
},
await slack_notification._handle_slack_notification(
alert_id=notification.alert_id,
notification_options=notification_option,
)
break
Expand Down
Loading