diff --git a/README.md b/README.md
index 41ad2e24..5ddf3924 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ A Sentinela Monitor is configured through 3 main parts, along some basic setting
These implementations are enough for Sentinela to autonomously execute monitoring logic and automatically manages the issues.
-
+
## Example scenario: Pending orders with completed shipments
Consider an online store where an order is expected to transition to `completed` as soon as its shipment is marked `completed`. Occasionally, inconsistencies arise: the shipment finishes but the order status remains stuck as `awaiting_delivery` or other intermediate state.
@@ -93,28 +93,29 @@ Sentinela provides a web dashboard, by default at port `8000`, with 2 sections:
2. a monitor editor, where you can create and edit monitors directly from the browser
**Overview**
-
+
**Editor**
-
+
# Documentation
-1. [Overview](./docs/overview.md)
-2. [Building a Monitor](./docs/monitor.md)
- 1. [Sample Monitor](./docs/sample_monitor.md)
-3. [Querying data from databases](./docs/querying.md)
-4. [Validating a monitor](./docs/monitor_validating.md)
-5. [Registering a monitor](./docs/monitor_registering.md)
+1. [Overview](/docs/overview.md)
+2. [Building a Monitor](/docs/monitor.md)
+ 1. [Monitor lifecycle](/docs/monitor_lifecycle.md)
+ 2. [Example Monitors](/docs/example_monitors.md)
+3. [Querying data from databases](/docs/querying.md)
+4. [Validating a monitor](/docs/monitor_validating.md)
+5. [Registering a monitor](/docs/monitor_registering.md)
6. Deployment
- 1. [Configuration](./docs/configuration.md)
- 2. [Configuration file](./docs/configuration_file.md)
- 3. [How to run](./docs/how_to_run.md)
-7. [Monitoring Sentinela](./docs/monitoring_sentinela.md)
-8. [Plugins](./docs/plugins/plugins.md)
- 1. [AWS](./docs/plugins/aws.md)
- 2. [Postgres](./docs/plugins/postgres.md)
- 3. [Slack](./docs/plugins/slack.md)
+ 1. [Configuration](/docs/configuration.md)
+ 2. [Configuration file](/docs/configuration_file.md)
+ 3. [How to run](/docs/how_to_run.md)
+7. [Monitoring Sentinela](/docs/monitoring_sentinela.md)
+8. [Plugins](/docs/plugins/plugins.md)
+ 1. [AWS](/docs/plugins/aws.md)
+ 2. [Postgres](/docs/plugins/postgres.md)
+ 3. [Slack](/docs/plugins/slack.md)
9. Interacting with Sentinela
- 1. [HTTP server](./docs/http_server.md)
+ 1. [HTTP server](/docs/http_server.md)
10. Special cases
- 1. [Dropping issues](./docs/dropping_issues.md)
+ 1. [Dropping issues](/docs/dropping_issues.md)
diff --git a/configs/configs-scalable.yaml b/configs/configs-scalable.yaml
index 116f98e1..8cd0ba47 100644
--- a/configs/configs-scalable.yaml
+++ b/configs/configs-scalable.yaml
@@ -4,8 +4,8 @@ plugins:
- postgres
- slack
-load_sample_monitors: true
-sample_monitors_path: sample_monitors
+load_example_monitors: true
+example_monitors_path: example_monitors
internal_monitors_path: internal_monitors
internal_monitors_notification:
enabled: true
diff --git a/configs/configs.yaml b/configs/configs.yaml
index fc102cfb..103dca67 100644
--- a/configs/configs.yaml
+++ b/configs/configs.yaml
@@ -4,8 +4,8 @@ plugins:
- postgres
- slack
-load_sample_monitors: true
-sample_monitors_path: sample_monitors
+load_example_monitors: true
+example_monitors_path: example_monitors
internal_monitors_path: internal_monitors
internal_monitors_notification:
enabled: true
diff --git a/docker/Dockerfile b/docker/Dockerfile
index b90359aa..fffed9f7 100644
--- a/docker/Dockerfile
+++ b/docker/Dockerfile
@@ -12,7 +12,7 @@ COPY . /app/
RUN python3 -m venv $VIRTUAL_ENV \
&& pip install --no-cache-dir --upgrade pip \
- && pip install poetry --no-cache-dir \
+ && pip install --no-cache-dir poetry \
&& sh tools/install_dependencies.sh
diff --git a/docker/docker-compose-dev.yaml b/docker/docker-compose-dev.yaml
index ecf85281..113d6443 100644
--- a/docker/docker-compose-dev.yaml
+++ b/docker/docker-compose-dev.yaml
@@ -29,8 +29,8 @@ services:
- 8000:8000
environment:
CONFIGS_FILE: configs/configs.yaml
- SAMPLE_SLACK_CHANNEL: C07NCL94SDT
- SAMPLE_SLACK_MENTION: U07NFGGMB98
+ EXAMPLE_SLACK_CHANNEL: C07NCL94SDT
+ EXAMPLE_SLACK_MENTION: U07NFGGMB98
SLACK_WEBSOCKET_ENABLED: true
SLACK_MAIN_CHANNEL: C07NCL94SDT
SLACK_MAIN_MENTION: U07NFGGMB98
diff --git a/docker/docker-compose-local.yaml b/docker/docker-compose-local.yaml
index 148bb0f0..0aecd4d3 100644
--- a/docker/docker-compose-local.yaml
+++ b/docker/docker-compose-local.yaml
@@ -24,8 +24,8 @@ services:
start_period: 2s
environment:
CONFIGS_FILE: configs/configs.yaml
- SAMPLE_SLACK_CHANNEL: C07NCL94SDT
- SAMPLE_SLACK_MENTION: U07NFGGMB98
+ EXAMPLE_SLACK_CHANNEL: C07NCL94SDT
+ EXAMPLE_SLACK_MENTION: U07NFGGMB98
SLACK_WEBSOCKET_ENABLED: true
SLACK_MAIN_CHANNEL: C07NCL94SDT
SLACK_MAIN_MENTION: U07NFGGMB98
diff --git a/docker/docker-compose-scalable.yaml b/docker/docker-compose-scalable.yaml
index d49f5ac8..1449d924 100644
--- a/docker/docker-compose-scalable.yaml
+++ b/docker/docker-compose-scalable.yaml
@@ -35,8 +35,8 @@ services:
start_period: 2s
environment:
CONFIGS_FILE: configs/configs-scalable.yaml
- SAMPLE_SLACK_CHANNEL: C07NCL94SDT
- SAMPLE_SLACK_MENTION: U07NFGGMB98
+ EXAMPLE_SLACK_CHANNEL: C07NCL94SDT
+ EXAMPLE_SLACK_MENTION: U07NFGGMB98
SLACK_WEBSOCKET_ENABLED: true
SLACK_MAIN_CHANNEL: C07NCL94SDT
SLACK_MAIN_MENTION: U07NFGGMB98
@@ -68,8 +68,8 @@ services:
start_period: 2s
environment:
CONFIGS_FILE: configs/configs-scalable.yaml
- SAMPLE_SLACK_CHANNEL: C07NCL94SDT
- SAMPLE_SLACK_MENTION: U07NFGGMB98
+ EXAMPLE_SLACK_CHANNEL: C07NCL94SDT
+ EXAMPLE_SLACK_MENTION: U07NFGGMB98
SLACK_WEBSOCKET_ENABLED: true
SLACK_MAIN_CHANNEL: C07NCL94SDT
SLACK_MAIN_MENTION: U07NFGGMB98
diff --git a/docs/configuration.md b/docs/configuration.md
index 247a4833..bc29b777 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1,7 +1,7 @@
# Configuration
-The basic configs are set through the `configs.yaml` file. This file is read when the application starts and all the settings will be loaded. The documentation found at [Configuration file](./configuration_file.md) provides an overview of the configuration parameters available.
+The basic configs are set through the `configs.yaml` file. This file is read when the application starts and all the settings will be loaded. The documentation found at [Configuration file](configuration_file.md) provides an overview of the configuration parameters available.
-The monitors path is also defined in the `configs.yaml` file. By default, it's set to the `sample_monitors` folder, but it can be changed to another folder if desired. The `configs.yaml` file also have other configurations that can be adjusted.
+The monitors path is also defined in the `configs.yaml` file. By default, it's set to the `example_monitors` folder, but it can be changed to another folder if desired. The `configs.yaml` file also have other configurations that can be adjusted.
# Environment variables
> [!IMPORTANT]
diff --git a/docs/configuration_file.md b/docs/configuration_file.md
index f67ae079..93610048 100644
--- a/docs/configuration_file.md
+++ b/docs/configuration_file.md
@@ -5,8 +5,8 @@ This document provides an overview of the configuration parameters available in
- `plugins`: List of strings. Plugins to be used by Sentinela. Check each plugin documentation to learn how to enable them.
## Monitors
-- `load_sample_monitors`: Boolean. Flag to enable the sample monitors.
-- `sample_monitors_path`: String. Path relative to the project root, where the sample monitors are stored.
+- `load_example_monitors`: Boolean. Flag to enable the example monitors.
+- `example_monitors_path`: String. Path relative to the project root, where the example monitors are stored.
- `internal_monitors_path`: String. Path relative to the project root, where the internal monitors are stored.
- `internal_monitors_notification`: Map. Settings for the notification to be sent by the internal monitors.
- `enabled`: Boolean. Flag to enable the internal monitors notification.
diff --git a/docs/diagrams/diagrams.drawio b/docs/diagrams/diagrams.drawio
index 7342427d..e481da1e 100644
--- a/docs/diagrams/diagrams.drawio
+++ b/docs/diagrams/diagrams.drawio
@@ -236,7 +236,7 @@
-
+
diff --git a/docs/dropping_issues.md b/docs/dropping_issues.md
index e2a01128..8250d00b 100644
--- a/docs/dropping_issues.md
+++ b/docs/dropping_issues.md
@@ -1,7 +1,7 @@
# Dropping issues
In some cases, a monitor may encounter edge cases where certain issues cannot be resolved automatically. While these edge cases should be considered during monitor development, dropping issues manually can serve as a solution for unavoidable situations.
-Issues can be dropped either via an [HTTP request](./http_server.md) or a [Slack message](./slack_commands.md).
+Issues can be dropped either via an [HTTP request](http_server.md) or a [Slack message](slack_commands.md).
> [!IMPORTANT]
> Since dropping issues is intended only for specific scenarios, issues IDs should be manually retrieved by querying the Sentinela application database.
diff --git a/docs/example_monitors.md b/docs/example_monitors.md
new file mode 100644
index 00000000..bc8d6782
--- /dev/null
+++ b/docs/example_monitors.md
@@ -0,0 +1,74 @@
+# Example Monitors
+This page describes all available example monitors that demonstrate different features and patterns.
+
+The described behaviors can be visualized in the dashboard and are useful for learning how to implement various monitoring scenarios using Sentinela.
+
+## Alert Options - Age Rule Monitor
+Demonstrates the `AgeRule`. The alert priority is determined by the age of the oldest active issue. Issues age over time, and older issues trigger higher priority alerts.
+
+**How it works**: The monitor creates a new issue every 5 minutes and measures its age in seconds. As issues get older, they trigger higher priority alerts according to the configured thresholds. Issues are automatically resolved after 5 minutes have passed since creation.
+
+**Monitor code**: [Age Rule Monitor](/example_monitors/alert_options/age_rule_monitor/age_rule_monitor.py)
+
+## Alert Options - Count Rule Monitor
+Demonstrates the `CountRule`. The alert priority is determined by the number of active issues. More active issues trigger higher priority alerts.
+
+**How it works**: The monitor creates 5 random issues every search cycle. The alert priority increases based on the total count of active issues linked to the alert. Issues can be automatically solved based on a severity field that fluctuates randomly.
+
+**Monitor code**: [Count Rule Monitor](/example_monitors/alert_options/count_rule_monitor/count_rule_monitor.py)
+
+## Alert Options - Value Rule Greater Than Monitor
+Demonstrates the `ValueRule` with the `greater_than` operation. The alert priority is determined by a specific numerical value from the issue data.
+
+**How it works**: The monitor tracks a single issue with an `error_rate` that oscillates from 0 to 100, back and forth. Alert priority increases when the error rate exceeds configured thresholds. The issue is never automatically solved, demonstrating continuous monitoring of a metric.
+
+**Monitor code**: [Value Rule Greater Than Monitor](/example_monitors/alert_options/value_rule_greater_than_monitor/value_rule_greater_than_monitor.py)
+
+## Alert Options - Value Rule Less Than Monitor
+Demonstrates the `ValueRule` with the `less_than` operation. The alert priority is determined by a specific numerical value from the issue data.
+
+**How it works**: Similar to the Greater Than Monitor but in reverse. This monitor tracks a single issue with a `success_rate` that oscillates from 0 to 100, back and forth. Alert priority increases when the success rate drops below thresholds, demonstrating monitoring for degraded performance.
+
+**Monitor code**: [Value Rule Less Than Monitor](/example_monitors/alert_options/value_rule_lesser_than_monitor/value_rule_lesser_than_monitor.py)
+
+## Blocking Operations Monitor
+Demonstrates how to handle blocking operations in search and update functions without blocking the async event loop.
+
+**How it works**: The monitor simulates a long blocking operation that would typically block the entire application. Using `asyncio.to_thread()`, the blocking call is executed in a separate thread, allowing the async event loop to remain responsive. Both `search()` and `update()` demonstrate this pattern, showing how to safely integrate synchronous blocking code into async monitor functions.
+
+**Monitor code**: [Blocking Operations Monitor](/example_monitors/blocking_operations_monitor/blocking_operations_monitor.py)
+
+## Non-Solvable Issues Monitor
+Demonstrates configuring issues as non-solvable. Non-solvable issues require manual intervention to be solved and cannot be automatically resolved by the monitor logic.
+
+**How it works**: The monitor simulates finding deactivated users and creates issues for them. With `solvable=False` and `unique=True`, only one issue per user is created. If the same user appears in subsequent searches, no new issue is generated. These issues can only be solved manually through the dashboard or notifications, when available.
+
+**Monitor code**: [Non-Solvable Issues Monitor](/example_monitors/non_solvable_issues_monitor/non_solvable_issues_monitor.py)
+
+## Plugin Slack Notification Monitor
+Demonstrates how to configure Slack notifications for alerts.
+
+**How it works**: This monitor is similar to the Count Rule Monitor but includes Slack notification configuration. It sends alerts to a configured Slack channel with customizable fields and optional mentions, showing how to integrate Sentinela alerts with Slack.
+
+**Monitor code**: [Plugin Slack Notification Monitor](/example_monitors/plugin_slack_notification_monitor/plugin_slack_notification_monitor.py)
+
+## Query Monitor
+Demonstrates using the `query` function to fetch data from a database. Shows how to connect to and execute queries against configured databases.
+
+**How it works**: The monitor executes a simple `SELECT current_timestamp;` query on the 'local' database. In `search()`, it creates a single non-solvable issue with the database timestamp. In `update()`, it refreshes the timestamp field with the latest database value. The actual query can be replaced with real data retrieval for production monitoring.
+
+**Monitor code**: [Query Monitor](/example_monitors/query_monitor/query_monitor.py)
+
+## Reactions Monitor
+Demonstrates how to configure reactions. Reactions are async callbacks triggered by specific events during monitor execution.
+
+**How it works**: Reactions are async functions that execute in response to specific monitor events (search completion, update completion, issue creation, etc.). They receive event payloads containing monitor and issue data. This example shows the available reactions with comments explaining when each runs and what data is available.
+
+**Monitor code**: [Reactions Monitor](/example_monitors/reactions_monitor/reactions_monitor.py)
+
+## Variables Monitor
+Demonstrates the variables feature for maintaining monitor-level state. Variables store information about the monitor's execution, not about individual issues.
+
+**How it works**: The monitor uses a variable to bookmark the last timestamp processed. This prevents reprocessing the same events across multiple monitor executions and makes searches more efficient. Variables are persisted across monitor runs and can store any data needed for monitor-level state management.
+
+**Monitor code**: [Variables Monitor](/example_monitors/variables_monitor/variables_monitor.py)
diff --git a/docs/how_to_run.md b/docs/how_to_run.md
index 06c7df2f..1d0c53c4 100644
--- a/docs/how_to_run.md
+++ b/docs/how_to_run.md
@@ -54,7 +54,7 @@ Local execution is recommended for developing monitors. It should not be used in
When running the application locally, it is recommended to use the internal queue instead of the SQS queue for faster and smoother operation. However, it is also possible to use the AWS queue mock or a real SQS queue.
-1. Set the secrets in the `.env.secrets` file and environment variables in the `docker/docker-compose-local.yml` file, as specified in the [Configuration](./configuration.md) documentation.
+1. Set the secrets in the `.env.secrets` file and environment variables in the `docker/docker-compose-local.yml` file, as specified in the [Configuration](configuration.md) documentation.
2. Migrate the database to the latest version. This is only necessary when running for the first time or after updates.
```shell
make migrate-local
@@ -81,7 +81,7 @@ For a more scalable deployment, it is recommended to use separate containers for
The `docker-compose` file for this setup includes a SQS queue mock, which is used by default. However, it is also possible to use the internal queue or a real SQS queue.
-1. Set the secrets in the `.env.secrets` file and environment variables in the `docker/docker-compose-scalable.yml` file, as specified in the [Configuration](./configuration.md) documentation.
+1. Set the secrets in the `.env.secrets` file and environment variables in the `docker/docker-compose-scalable.yml` file, as specified in the [Configuration](configuration.md) documentation.
2. Set the `replicas` parameter in the `docker/docker-compose-scalable.yml` file to the desired number of executors.
3. Migrate the database to the latest version. This is only necessary when running for the first time or after updates.
```shell
@@ -107,19 +107,25 @@ For production deployment, it is recommended to use a more complex setup with mu
- Requires an external database and message queue.
### Building the Image
-The [Dockerfile](../Dockerfile) is a starting point for building the application image. This file implements the logic to install all dependencies for the enabled plugins.
+The [Dockerfile](/docker/Dockerfile) is a starting point for building the application image. This file implements the logic to install all dependencies for the enabled plugins.
1. Install the dependencies for the application and enabled plugins.
```shell
- poetry install --no-root --only $(get_plugins_list)
+ poetry install --only main
+
+ plugins=$(get_plugins_list)
+
+ if ! [ "x$plugins" = "x" ]; then
+ poetry install --only $plugins
+ fi
```
### Deploying the Application
In production deployment, it is recommended to deploy the controller and executors in separate containers or pods (in the case of a Kubernetes deployment). This method requires an external queue to allow communication between the controller and executors. A persistent database is also recommended to prevent data loss.
-The files provided in the [Kubernetes template](../resources/kubernetes_template) directory can be used as a reference for a Kubernetes deployment.
+The files provided in the [Kubernetes template](/resources/kubernetes_template) directory can be used as a reference for a Kubernetes deployment.
-All services must have the environment variables set as specified in the [Configuration](./configuration.md) documentation.
+All services must have the environment variables set as specified in the [Configuration](configuration.md) documentation.
Controllers and executors can be run by specifying them as parameters when starting the application:
1. Run the controller.
@@ -130,3 +136,10 @@ Controllers and executors can be run by specifying them as parameters when start
```shell
sentinela executor
```
+
+# Gracefully Stopping Sentinela
+Sentinela can be gracefully stopped by sending a `SIGINT` or `SIGTERM` signal to the process. This allows the application to finish processing any ongoing tasks before shutting down.
+
+It's recommended to not forcefully kill the application because a monitor execution might be in progress, and killing the application would interrupt it, potentially leaving the monitor in an inconsistent state.
+
+Sentinela has it's own internal process to check if there are any monitors in this inconsistent state and fixes them. However, it's still recommended to allow the application to gracefully stop.
diff --git a/docs/http_server.md b/docs/http_server.md
index 11bb02f5..27903ace 100644
--- a/docs/http_server.md
+++ b/docs/http_server.md
@@ -83,7 +83,7 @@ Enable the monitor with the provided `monitor_name`.
Validate the monitor code provided without registering it.
-For more information, check the [Validating a monitor](./monitor_validating.md) documentation.
+For more information, check the [Validating a monitor](monitor_validating.md) documentation.
Request body example:
```json
@@ -104,7 +104,7 @@ Response example:
Register the monitor with the provided `monitor_name`.
-For more information, check the [Registering a monitor](./monitor_registering.md) documentation.
+For more information, check the [Registering a monitor](monitor_registering.md) documentation.
Request body example:
```json
diff --git a/docs/monitor.md b/docs/monitor.md
index b2217cd9..ee8037b4 100644
--- a/docs/monitor.md
+++ b/docs/monitor.md
@@ -1,5 +1,5 @@
# Creating a new monitor
-This guide will walk through the steps to set up a new Monitor. The file [monitor_template.py](../resources/monitor_template.py) serves as a template for creating a new monitor. It includes all the necessary imports and settings to get started.
+This guide will walk through the steps to set up a new Monitor. The file [monitor_template.py](/resources/monitor_template.py) serves as a template for creating a new monitor. It includes all the necessary imports and settings to get started.
As a demonstration, the Monitor that will be designed is intended to **search for users with invalid registration data**, specifically when their name is empty.
@@ -290,7 +290,7 @@ result = await asyncio.to_thread(blocking_function)
# Notifications
Notifications are optional and can be configured to send notifications to different targets without needing extensive settings for ech monitor. Configure notifications by creating the `notification_options` variable with a list of the desired notifications. Each notification has it's own settings and behaviors.
-Notifications are provided as plugins. Check the [plugins documentation](./plugins/plugins.md) for more information.
+Notifications are provided as plugins. Check the [plugins documentation](plugins/plugins.md) for more information.
# Reactions
Reactions are optional and can be configured reactions to specific events by creating a `reaction_options` variable with an instance of the `ReactionOptions` class, available in the `monitor_utils` module.
@@ -358,7 +358,7 @@ The available events are:
The monitor utils module also provides useful functions for developing a monitor.
## Query
-The `query` function allows querying data from available databases. For more details, refer to the [Querying Databases](./querying.md) documentation.
+The `query` function allows querying data from available databases. For more details, refer to the [Querying Databases](querying.md) documentation.
## Read file
The `read_file` function reads files in the same directory as the monitor code, making it useful for accessing other resources that the monitor relies on, such as SQL query files.
@@ -379,9 +379,9 @@ content = read_file("search_query.sql")
```
## Variables
-The `variables` module allows storing and retrieving variables that can be used across executions of the monitor. This is useful for maintaining state or configuration information.
+The `variables` module allows storing and retrieving variables that persist across monitor executions. Variables store **monitor-level state** , not issue-specific data. When an information is exclusively related to the issue they should be stored in the issue data.
-Available functions are:
+### Available Functions
**`get_variable`**
```python
@@ -393,7 +393,7 @@ async def get_variable(
The function takes one parameter:
- `name`: The name of the variable to retrieve.
-Return the value of a variable. If the variable does not exist, returns `None`.
+Returns the value of a variable. If the variable does not exist, returns `None`.
**`set_variable`**
```python
@@ -411,15 +411,21 @@ Sets the value of a variable. If the variable does not exist yet, it's created.
Both functions must be called from functions defined in the monitor base code. If they're called from any other Python file, they will raise an error as they won't be able to identify the monitor that's calling it.
+### Example: Using Variables for Pagination
```python
from monitor_utils import variables
async def search() -> list[IssueDataType] | None:
- # Set a variable
- await variables.set_variable("my_var", "some_value")
+ # Get the last timestamp we processed from variables.
+ last_timestamp = await variables.get_variable("last_processed_timestamp")
-async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
- value = await variables.get_variable("my_var")
+ # Query for events newer than last_timestamp
+ events = await query_data_source(since=last_timestamp)
+
+ # Update the bookmark for the next execution
+ await variables.set_variable("last_processed_timestamp", datetime.now().isoformat())
+
+ return events
```
# Registering
diff --git a/docs/sample_monitor.md b/docs/monitor_lifecycle.md
similarity index 85%
rename from docs/sample_monitor.md
rename to docs/monitor_lifecycle.md
index 4e2b1c35..8ecd6f6d 100644
--- a/docs/sample_monitor.md
+++ b/docs/monitor_lifecycle.md
@@ -1,5 +1,7 @@
-# Sample monitor
-This documentation explains the processes executed by the Sentinela monitoring platform, focusing on searching for issues, updating their data, and identifying when issues are solved. The provided [Sample Monitor](../sample_monitors/test_monitor/test_monitor.py) serves as an example implementation to understand these processes.
+# Monitor lifecycle
+This documentation explains the processes executed by the Sentinela monitoring platform, focusing on searching for issues, updating their data, and identifying when issues are solved. The provided [Example Monitors](example_monitors.md) implement various scenarios to help understand these processes.
+
+In this example, the monitor [Count Rule Monitor](/example_monitors/alert_options/count_rule_monitor/count_rule_monitor.py) is used to illustrate the monitor lifecycle. This monitor creates multiple issues with a `value` field that fluctuates randomly, demonstrating how issues are created, updated, and resolved based on their data.
## Issue options and issue data type
Each issue is expected to have two fields:
diff --git a/docs/monitor_registering.md b/docs/monitor_registering.md
index 82392f37..f5ed2adf 100644
--- a/docs/monitor_registering.md
+++ b/docs/monitor_registering.md
@@ -1,7 +1,7 @@
# Registering a monitor
Once the monitor code has been created, it needs to be registered on Sentinela. The process for registering a new monitor or updating an existing one is the same.
-
+
## Monitor Composition
A monitor consists of:
diff --git a/docs/overview.md b/docs/overview.md
index 0bf6d0db..081ae2b7 100644
--- a/docs/overview.md
+++ b/docs/overview.md
@@ -33,21 +33,21 @@ The Sentinela platform comprises four main components:
- HTTP Server
- Monitors Loader
-
+
## Controller
The controller orchestrates the processing of monitors, queuing them for execution as needed.
The controller execution is demonstrated in the following diagram.
-
+
## Executor
The executor handles the actual execution of monitor routines, as well as responses to requests and events. It performs the platform’s core processing tasks and can be scaled horizontally to enhance processing capacity.
The executor execution is demonstrated in the following diagram.
-
+
The executor will wait for a message in the queue and do nothing while none are available. When a message is received, the executor will process it using the correct handler.
@@ -61,18 +61,18 @@ During the handling of a message, the executor will continuously reset the messa
## HTTP Server
As the name suggests, the HTTP server serves as an entry point for user requests and actions on the platform. While not all interactions must occur through the HTTP server, it provides a central access point for user actions.
-More information can be found in the [HTTP Server](./docs/http_server.md) documentation.
+More information can be found in the [HTTP Server](http_server.md) documentation.
## Monitors Loader
The monitors loader loads the monitors from the database and registers them to be used by the other components.
-
+
The monitors loader execution is demonstrated in the following diagram.
-
+
-The monitors loader registers internal and sample monitors (if configured as such) to the database. This process is only executed when the controller is enabled in the execution to make sure only one instance is trying to register the same monitors.
+The monitors loader registers internal and example monitors (if configured as such) to the database. This process is only executed when the controller is enabled in the execution to make sure only one instance is trying to register the same monitors.
While waiting for the next loading cycle, some components can request for the monitors to be loaded again before waiting for the next cycle. One example where this might happen is if the executor receives a message for a monitor that is not loaded yet. This issue can occur if the monitor already loaded new monitors from the database but the executor is behind in the loading process.
diff --git a/docs/plugins/plugins.md b/docs/plugins/plugins.md
index 5cebebc8..708936d3 100644
--- a/docs/plugins/plugins.md
+++ b/docs/plugins/plugins.md
@@ -82,7 +82,7 @@ class MyNotification:
]
```
-The reaction functions must follow the same structure presented in the [Monitor](../monitor.md) documentation.
+The reaction functions must follow the same structure presented in the [Monitor](/docs/monitor.md) documentation.
## Services
Services are used when the plugin has some initialization or running service. An example of a running service is a websocket connection to an external provider.
@@ -139,7 +139,7 @@ __all__ = ["queues"]
```
## Pools
-Plugins can provide different database pools to be used by Sentinela. Pools are a way to provide connections to databases to the monitors through a simple interface using the `query` function. More information about querying databases can be found in the [querying](../querying.md) documentation.
+Plugins can provide different database pools to be used by Sentinela. Pools are a way to provide connections to databases to the monitors through a simple interface using the `query` function. More information about querying databases can be found in the [querying](/docs/querying.md) documentation.
An example of a plugin that provides the a database pool is shown below:
@@ -179,9 +179,9 @@ When the databases are being initialized, Sentinela will search for pools provid
## Built-in plugins
Sentinela comes with some built-in plugins that can be used to extend the application's functionality.
-- [AWS](./aws.md)
-- [Postgres](./postgres.md)
-- [Slack](./slack.md)
+- [AWS](aws.md)
+- [Postgres](postgres.md)
+- [Slack](slack.md)
## Enabling plugins
To enable a plugin, set the `plugins` field in the configuration file with the name of the desired plugins.
diff --git a/docs/plugins/slack.md b/docs/plugins/slack.md
index ac7f8226..24a51158 100644
--- a/docs/plugins/slack.md
+++ b/docs/plugins/slack.md
@@ -5,15 +5,15 @@ The Slack plugin offers an interface to interact with Sentinela through Slack. I
To enable the Slack plugin, add `slack` to the `plugins` list in the configuration file.
## Create the Slack app
-First create a Slack app in your workspace. The provided app manifest [template](../resources/slack_app_manifest.yaml) has the basic configuration and permissions needed for the Sentinela Slack app.
+First create a Slack app in your workspace. The provided app manifest [template](/resources/slack_app_manifest.yaml) has the basic configuration and permissions needed for the Sentinela Slack app.
## Environment variables
The following environment variables are used by the Slack plugin:
- `SLACK_TOKEN`: The token used to send messages to Slack. This token is generated when you create your Slack app and install it in your workspace. Example: `xoxb-1234567890-1234567890123-12345678901234567890abcdef`.
- `SLACK_WEBSOCKET_ENABLED`: A flag to enable or disable the websocket connection for receiving events from Slack. Set this to `true` to enable the websocket or `false` to disable it. Defaults to `false`.
- `SLACK_APP_TOKEN`: The token used to start the websocket connection for receiving events from interactions with the Sentinela Slack app. This token is generated when you create your Slack app and enable the Socket Mode. Example: `xapp-1234567890-1234567890123-12345678901234567890abcdef`.
-- `SLACK_MAIN_CHANNEL`: The Slack channel where notifications for **internal monitors** and **sample monitors** will be sent. Example: `C0011223344`.
-- `SLACK_MAIN_MENTION`: The Slack user or group to mention in notifications for **internal monitors** and **sample monitors**. Example: `U0011223344`.
+- `SLACK_MAIN_CHANNEL`: The Slack channel where notifications for **internal monitors** and **example monitors** will be sent. If using the provided docker compose implementations, these variables must be configured accordingly. They are located at the `docker/` directory. Example: `C0011223344`.
+- `SLACK_MAIN_MENTION`: The Slack user or group to mention in notifications for **internal monitors** and **example monitors**. If using the provided docker compose implementations, these variables must be configured accordingly. They are located at the `docker/` directory. Example: `U0011223344`.
## Slack commands
Sentinela provides two main ways to interact through Slack:
@@ -28,7 +28,7 @@ Possible buttons:
- **Lock**: Lock the alert. Visible if the alert is not already locked.
- **Solve**: Solves the alert. Visible only if the monitor’s issue settings is set as **not solvable**.
-
+
### Messages mentioning Sentinela
As a Slack app, Sentinela can also respond to direct commands sent in a message. To interact this way, mention the Sentinela app, followed by the desired action.
@@ -109,7 +109,7 @@ To use the Slack notification for internal monitors, the settings for the `inter
- `title` and `issues_fields` are specific to each monitor and are already defined in the internal monitors. If configured in the `params` field, they will be ignored.
- `min_priority_to_send`, `min_priority_to_mention`, `mention_on_update`, and `issue_show_limit` can be set in the `params` field to customize the notification behavior. If not set, the default values will be used.
-The provided settings will be applied to every internal and sample monitors.
+The provided settings will be applied to every internal and example monitors.
## Services
The Slack plugin includes a service that connects to the Slack websocket API to receive mentions and button press events. Any event received will queue an action to be processed by Sentinela.
diff --git a/example_monitors/alert_options/age_rule_monitor/age_rule_monitor.py b/example_monitors/alert_options/age_rule_monitor/age_rule_monitor.py
new file mode 100644
index 00000000..0bcc8c46
--- /dev/null
+++ b/example_monitors/alert_options/age_rule_monitor/age_rule_monitor.py
@@ -0,0 +1,68 @@
+"""
+Age Rule Monitor
+
+This monitor demonstrates the AgeRule.
+The alert priority is determined by the age of the oldest active issue.
+Issues age over time, and older issues trigger higher priority alerts.
+"""
+
+import time
+from datetime import datetime
+from typing import TypedDict
+
+from monitor_utils import AgeRule, AlertOptions, IssueOptions, MonitorOptions, PriorityLevels
+
+
+class IssueDataType(TypedDict):
+ id: int
+ created_at: str
+
+
+monitor_options = MonitorOptions(
+ search_cron="* * * * *",
+ update_cron="* * * * *",
+)
+
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=True,
+)
+
+# AgeRule: Priority is based on issue age in seconds
+# An alert's priority is determined by the age of the oldest active issue
+alert_options = AlertOptions(
+ rule=AgeRule(
+ priority_levels=PriorityLevels(
+ low=0, # 0 seconds
+ moderate=60, # 1 minute
+ high=120, # 2 minutes
+ critical=180, # 3 minutes
+ )
+ )
+)
+
+
+async def search() -> list[IssueDataType] | None:
+ # Every 5 minutes, a new ID is generated, creating a new issue
+ # This allows observing the alert priority increasing as the issue ages,
+ # with a new issue appearing every 5 minutes
+ issue_id = int(time.time() // 300)
+ return [
+ {
+ "id": issue_id,
+ "created_at": datetime.now().isoformat(),
+ }
+ ]
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ # Keep the original issue data
+ return issues_data
+
+
+def is_solved(issue_data: IssueDataType) -> bool:
+ # Issue is solved after 5 minutes have passed since its creation
+ # This demonstrates automatic resolution based on issue age
+ created_at = datetime.fromisoformat(issue_data["created_at"])
+ age_seconds = (datetime.now() - created_at).total_seconds()
+ return age_seconds >= 290 # The issue will be solved just before completing 5 minutes
diff --git a/example_monitors/alert_options/count_rule_monitor/count_rule_monitor.py b/example_monitors/alert_options/count_rule_monitor/count_rule_monitor.py
new file mode 100644
index 00000000..d944680e
--- /dev/null
+++ b/example_monitors/alert_options/count_rule_monitor/count_rule_monitor.py
@@ -0,0 +1,73 @@
+"""
+Count Rule Monitor
+
+This monitor demonstrates the CountRule.
+The alert priority is determined by the number of active issues.
+More active issues trigger higher priority alerts.
+"""
+
+import random
+import time
+from typing import TypedDict
+
+from monitor_utils import AlertOptions, CountRule, IssueOptions, MonitorOptions, PriorityLevels
+
+
+class IssueDataType(TypedDict):
+ id: int
+ value: int
+
+
+monitor_options = MonitorOptions(
+ search_cron="* * * * *",
+ update_cron="* * * * *",
+)
+
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=True,
+)
+
+# CountRule: Priority is based on the number of active issues
+# An alert's priority increases as more issues are linked to it
+alert_options = AlertOptions(
+ rule=CountRule(
+ priority_levels=PriorityLevels(
+ low=0, # more than 0 active issues
+ moderate=5, # more than 5 active issues
+ high=10, # more than 10 active issues
+ critical=15, # more than 15 active issues
+ )
+ )
+)
+
+
+async def search() -> list[IssueDataType] | None:
+ # Return 5 issues to demonstrate how the count of active issues affects
+ # the alert priority level
+ return [
+ {
+ "id": random.randrange(1, 100000),
+ "value": random.randrange(1, 10),
+ }
+ for _ in range(5)
+ ]
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ # Every 5 minutes, there's a 90% chance of issues being solved
+ # This demonstrates how the count of active issues fluctuates over time
+ is_solving_window = (time.time() // 60) % 5 == 0
+
+ for issue_data in issues_data:
+ if is_solving_window and random.random() < 0.9:
+ issue_data["value"] = 1
+ else:
+ issue_data["value"] = random.randrange(1, 10)
+
+ return issues_data
+
+
+def is_solved(issue_data: IssueDataType) -> bool:
+ # Issue is solved when value equals to 1
+ return issue_data["value"] == 1
diff --git a/example_monitors/alert_options/value_rule_greater_than_monitor/value_rule_greater_than_monitor.py b/example_monitors/alert_options/value_rule_greater_than_monitor/value_rule_greater_than_monitor.py
new file mode 100644
index 00000000..3e0a6296
--- /dev/null
+++ b/example_monitors/alert_options/value_rule_greater_than_monitor/value_rule_greater_than_monitor.py
@@ -0,0 +1,89 @@
+"""
+Value Rule Greater Than Monitor
+
+This monitor demonstrates the ValueRule with the "greater_than" operation.
+The alert priority is determined by a specific numerical value from the issue data.
+A single issue's error rate oscillates from 0 to 100 and back, triggering different
+priority levels as the value changes.
+"""
+
+import random
+from typing import TypedDict
+
+from monitor_utils import AlertOptions, IssueOptions, MonitorOptions, PriorityLevels, ValueRule
+
+
+class IssueDataType(TypedDict):
+ id: str
+ error_rate: float
+ trend: str
+
+
+monitor_options = MonitorOptions(
+ search_cron="* * * * *",
+ update_cron="* * * * *",
+)
+
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=True,
+)
+
+# ValueRule: Priority is based on a value from the issue data
+# The alert's priority is determined by comparing the 'error_rate' field
+# against the priority level thresholds using the 'greater_than' operation
+alert_options = AlertOptions(
+ rule=ValueRule(
+ value_key="error_rate",
+ operation="greater_than",
+ priority_levels=PriorityLevels(
+ low=10, # error_rate > 10%
+ moderate=25, # error_rate > 25%
+ high=50, # error_rate > 50%
+ critical=75, # error_rate > 75%
+ ),
+ )
+)
+
+
+async def search() -> list[IssueDataType] | None:
+ # Return a single issue with error rate starting at 0 and trend rising
+ return [
+ {
+ "id": "sample issue",
+ "error_rate": 0.0,
+ "trend": "rising",
+ }
+ ]
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ # Update the single issue's error rate by a random value between 10 and 25
+ # Use the trend stored in the issue to determine direction
+ # Trend flips when reaching 0 or 100
+ issue_data = issues_data[0]
+
+ direction = 1 if issue_data["trend"] == "rising" else -1
+ change = random.uniform(10, 25) * direction
+ new_error_rate = issue_data["error_rate"] + change
+
+ # Flip trend and clamp to boundaries
+ if new_error_rate >= 95:
+ if new_error_rate > 100:
+ new_error_rate = 100
+ new_trend = "falling"
+ elif new_error_rate <= 5:
+ if new_error_rate < 0:
+ new_error_rate = 0
+ new_trend = "rising"
+ else:
+ new_trend = issue_data["trend"]
+
+ issue_data["error_rate"] = new_error_rate
+ issue_data["trend"] = new_trend
+ return [issue_data]
+
+
+def is_solved(issue_data: IssueDataType) -> bool:
+ # This issue never solves
+ return False
diff --git a/example_monitors/alert_options/value_rule_lesser_than_monitor/value_rule_lesser_than_monitor.py b/example_monitors/alert_options/value_rule_lesser_than_monitor/value_rule_lesser_than_monitor.py
new file mode 100644
index 00000000..a0636cee
--- /dev/null
+++ b/example_monitors/alert_options/value_rule_lesser_than_monitor/value_rule_lesser_than_monitor.py
@@ -0,0 +1,89 @@
+"""
+Value Rule Less Than Monitor
+
+This monitor demonstrates the ValueRule with the "less_than" operation.
+The alert priority is determined by a specific numerical value from the issue data.
+A single issue's success rate oscillates from 100 to 0 and back, triggering different
+priority levels as the value changes.
+"""
+
+import random
+from typing import TypedDict
+
+from monitor_utils import AlertOptions, IssueOptions, MonitorOptions, PriorityLevels, ValueRule
+
+
+class IssueDataType(TypedDict):
+ id: str
+ success_rate: float
+ trend: str
+
+
+monitor_options = MonitorOptions(
+ search_cron="* * * * *",
+ update_cron="* * * * *",
+)
+
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=True,
+)
+
+# ValueRule: Priority is based on a value from the issue data
+# The alert's priority is determined by comparing the 'success_rate' field
+# against the priority level thresholds using the 'less_than' operation
+alert_options = AlertOptions(
+ rule=ValueRule(
+ value_key="success_rate",
+ operation="lesser_than",
+ priority_levels=PriorityLevels(
+ low=90, # success_rate < 90%
+ moderate=75, # success_rate < 75%
+ high=50, # success_rate < 50%
+ critical=25, # success_rate < 25%
+ ),
+ )
+)
+
+
+async def search() -> list[IssueDataType] | None:
+ # Return a single issue with success rate starting at 100 and trend falling
+ return [
+ {
+ "id": "sample issue",
+ "success_rate": 100.0,
+ "trend": "falling",
+ }
+ ]
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ # Update the single issue's success rate by a random value between 10 and 25
+ # Use the trend stored in the issue to determine direction
+ # Trend flips when reaching 0 or 100
+ issue_data = issues_data[0]
+
+ direction = 1 if issue_data["trend"] == "rising" else -1
+ change = random.uniform(10, 25) * direction
+ new_success_rate = issue_data["success_rate"] + change
+
+ # Flip trend and clamp to boundaries
+ if new_success_rate >= 95:
+ if new_success_rate > 100:
+ new_success_rate = 100
+ new_trend = "falling"
+ elif new_success_rate <= 5:
+ if new_success_rate < 0:
+ new_success_rate = 0
+ new_trend = "rising"
+ else:
+ new_trend = issue_data["trend"]
+
+ issue_data["success_rate"] = new_success_rate
+ issue_data["trend"] = new_trend
+ return [issue_data]
+
+
+def is_solved(issue_data: IssueDataType) -> bool:
+ # This issue never solves
+ return False
diff --git a/example_monitors/blocking_operations_monitor/blocking_operations_monitor.py b/example_monitors/blocking_operations_monitor/blocking_operations_monitor.py
new file mode 100644
index 00000000..4539c935
--- /dev/null
+++ b/example_monitors/blocking_operations_monitor/blocking_operations_monitor.py
@@ -0,0 +1,67 @@
+"""
+Blocking Operations Monitor
+
+This monitor demonstrates how to handle blocking operations in search and
+update functions without affecting the entire application.
+"""
+
+import asyncio
+import time
+from typing import TypedDict
+
+from monitor_utils import AlertOptions, CountRule, IssueOptions, MonitorOptions, PriorityLevels
+
+
+class IssueDataType(TypedDict):
+ id: int
+ value: int
+
+
+monitor_options = MonitorOptions(
+ search_cron="* * * * *",
+ update_cron="* * * * *",
+)
+
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=True,
+)
+
+alert_options = AlertOptions(
+ rule=CountRule(
+ priority_levels=PriorityLevels(
+ low=0,
+ )
+ )
+)
+
+
+def find() -> int:
+ # Simulates a long blocking operation
+ time.sleep(2)
+ return int(time.time())
+
+
+async def search() -> list[IssueDataType] | None:
+ # Get the value from a long blocking operation in a non-blocking way
+ # using 'asyncio.to_thread'
+ value = await asyncio.to_thread(find)
+ return [
+ {
+ "id": 1,
+ "value": value,
+ }
+ ]
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ # Get the value from a long blocking operation in a non-blocking way
+ # using 'asyncio.to_thread'
+ value = await asyncio.to_thread(find)
+ issues_data[0]["value"] = value
+ return issues_data
+
+
+def is_solved(issue_data: IssueDataType) -> bool:
+ # Issue will never be solved
+ return False
diff --git a/example_monitors/non_solvable_issues_monitor/non_solvable_issues_monitor.py b/example_monitors/non_solvable_issues_monitor/non_solvable_issues_monitor.py
new file mode 100644
index 00000000..fbb9fd8c
--- /dev/null
+++ b/example_monitors/non_solvable_issues_monitor/non_solvable_issues_monitor.py
@@ -0,0 +1,76 @@
+"""
+Non-Solvable Issues Monitor
+
+This monitor demonstrates configuring issues as non-solvable.
+Non-solvable issues require manual intervention to be solved and cannot be
+automatically resolved by the monitor logic. This is useful for final states
+that would result in issues never being resolved.
+"""
+
+import random
+import string
+from typing import TypedDict, cast
+
+from monitor_utils import (
+ AlertOptions,
+ CountRule,
+ IssueOptions,
+ MonitorOptions,
+ PriorityLevels,
+)
+
+
+class IssueDataType(TypedDict):
+ id: int
+ username: str
+ deactivated: bool
+
+
+monitor_options = MonitorOptions(
+ search_cron="*/5 * * * *",
+ update_cron="*/5 * * * *",
+)
+
+# Non-solvable issues: set solvable=False and unique=True
+# unique=True ensures only one issue per deactivated user If the same user
+# appears in subsequent searches the issue won't be created again because it
+# was already created before
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=False,
+ unique=True,
+)
+
+alert_options = AlertOptions(
+ rule=CountRule(
+ priority_levels=PriorityLevels(
+ low=1,
+ moderate=3,
+ high=5,
+ critical=8,
+ )
+ )
+)
+
+
+async def search() -> list[IssueDataType] | None:
+ # Simulate finding deactivated users (a permanent state)
+ # These users won't be automatically "solved" by monitor logic—
+ # they require the user to manually solve the alert and its issues
+ # through the dashboard or one of the notifications, when available
+ return cast(
+ list[IssueDataType],
+ [
+ {
+ "id": random.randrange(1, 100000),
+ "username": "".join(random.choices(string.ascii_lowercase, k=16)),
+ "deactivated": True,
+ }
+ ],
+ )
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ # Keep the issue data unchanged
+ # A deactivated user remains deactivated unless manually reactivated
+ return issues_data
diff --git a/example_monitors/plugin_slack_notification_monitor/plugin_slack_notification_monitor.py b/example_monitors/plugin_slack_notification_monitor/plugin_slack_notification_monitor.py
new file mode 100644
index 00000000..7f0f8497
--- /dev/null
+++ b/example_monitors/plugin_slack_notification_monitor/plugin_slack_notification_monitor.py
@@ -0,0 +1,77 @@
+"""
+Slack Notification Monitor
+
+This monitor demonstrates how to configure Slack notifications.
+"""
+
+import os
+import random
+import time
+from typing import TypedDict
+
+from monitor_utils import AlertOptions, CountRule, IssueOptions, MonitorOptions, PriorityLevels
+from plugins.slack.notifications import SlackNotification
+
+
+class IssueDataType(TypedDict):
+ id: int
+ severity: int
+
+
+monitor_options = MonitorOptions(
+ search_cron="* * * * *",
+ update_cron="* * * * *",
+)
+
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=True,
+)
+
+alert_options = AlertOptions(
+ rule=CountRule(
+ priority_levels=PriorityLevels(
+ low=0,
+ moderate=5,
+ high=10,
+ critical=15,
+ )
+ )
+)
+
+
+async def search() -> list[IssueDataType] | None:
+ return [
+ {
+ "id": random.randrange(1, 100000),
+ "severity": random.randrange(1, 10),
+ }
+ for _ in range(5)
+ ]
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ is_solving_window = (time.time() // 60) % 5 == 0
+
+ for issue_data in issues_data:
+ if is_solving_window and random.random() < 0.9:
+ issue_data["severity"] = 1
+ else:
+ issue_data["severity"] = random.randrange(1, 10)
+
+ return issues_data
+
+
+def is_solved(issue_data: IssueDataType) -> bool:
+ return issue_data["severity"] == 1
+
+
+# Slack notifications for this monitor
+notification_options = [
+ SlackNotification(
+ channel=os.environ.get("SLACK_MAIN_CHANNEL", ""),
+ title="Slack Notification Monitor",
+ issues_fields=["id", "severity"],
+ mention=os.environ.get("SLACK_MAIN_MENTION"),
+ )
+]
diff --git a/example_monitors/query_monitor/query_monitor.py b/example_monitors/query_monitor/query_monitor.py
new file mode 100644
index 00000000..8c7457b3
--- /dev/null
+++ b/example_monitors/query_monitor/query_monitor.py
@@ -0,0 +1,85 @@
+"""
+Query Monitor
+
+This monitor demonstrates using the query function to fetch data from a database.
+The monitor executes a simple query to illustrate the pattern for connecting to
+databases. In a real-world scenario, you would replace the example query with
+one that retrieves meaningful data for your monitoring needs.
+"""
+
+from typing import TypedDict
+
+from monitor_utils import (
+ AlertOptions,
+ CountRule,
+ IssueOptions,
+ MonitorOptions,
+ PriorityLevels,
+ query,
+)
+
+
+class IssueDataType(TypedDict):
+ id: str
+ current_timestamp: str
+
+
+monitor_options = MonitorOptions(
+ search_cron="* * * * *",
+ update_cron="* * * * *",
+)
+
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=False,
+)
+
+alert_options = AlertOptions(
+ rule=CountRule(
+ priority_levels=PriorityLevels(
+ low=0,
+ moderate=1,
+ high=2,
+ critical=3,
+ )
+ )
+)
+
+
+async def search() -> list[IssueDataType] | None:
+ # Execute a simple query on the 'local' database to demonstrate
+ # the query function usage In a real monitor, replace this with
+ # a meaningful query that retrieves data relevant to your use case
+ result = await query(
+ name="local",
+ sql="select current_timestamp;",
+ )
+ if not result:
+ return None
+ current_timestamp = result[0]["current_timestamp"]
+
+ if result and len(result) > 0:
+ return [
+ {
+ "id": "database_connection_check",
+ "current_timestamp": current_timestamp,
+ }
+ ]
+
+ return None
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ # Update each issue with the current database timestamp
+ result = await query(
+ name="local",
+ sql="SELECT current_timestamp;",
+ )
+ if not result:
+ return None
+ current_timestamp = result[0]["current_timestamp"]
+
+ for issue in issues_data:
+ issue["current_timestamp"] = current_timestamp
+
+ return issues_data
diff --git a/example_monitors/reactions_monitor/reactions_monitor.py b/example_monitors/reactions_monitor/reactions_monitor.py
new file mode 100644
index 00000000..1e01e745
--- /dev/null
+++ b/example_monitors/reactions_monitor/reactions_monitor.py
@@ -0,0 +1,109 @@
+"""
+Reactions Monitor
+
+This monitor demonstrates how to configure reactions.
+Reactions are async callbacks triggered by specific events during monitor execution.
+The reaction functions here do nothing, but their comments explain when they run
+and what data is available.
+"""
+
+import json
+import logging
+import random
+from typing import TypedDict
+
+from monitor_utils import (
+ AlertOptions,
+ CountRule,
+ EventPayload,
+ IssueOptions,
+ MonitorOptions,
+ PriorityLevels,
+ ReactionOptions,
+)
+
+
+class IssueDataType(TypedDict):
+ id: int
+ value: int
+
+
+logger = logging.getLogger("reactions_monitor")
+
+
+monitor_options = MonitorOptions(
+ search_cron="* * * * *",
+ update_cron="* * * * *",
+)
+
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=True,
+)
+
+alert_options = AlertOptions(
+ rule=CountRule(
+ priority_levels=PriorityLevels(
+ low=1,
+ moderate=3,
+ high=5,
+ critical=8,
+ )
+ )
+)
+
+
+async def search() -> list[IssueDataType] | None:
+ # Create a small, random set of issues to trigger reactions
+ count = random.randrange(0, 4)
+ return [
+ {
+ "id": random.randrange(1, 100000),
+ "value": random.randrange(1, 10),
+ }
+ for _ in range(count)
+ ]
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ # Update each issue with a new random value
+ for issue_data in issues_data:
+ issue_data["value"] = random.randrange(1, 10)
+
+ return issues_data
+
+
+def is_solved(issue_data: IssueDataType) -> bool:
+ # Issues are solved when value reaches 1
+ return issue_data["value"] == 1
+
+
+async def reaction_issue_created(event_payload: EventPayload) -> None:
+ # Called when an issue is created
+ # Example use: post a message or call an API
+ # This log message will appear in light blue
+ json_payload = json.dumps(event_payload.to_dict())
+ logger.info(f"\033[94mReaction: issue_created. Event payload: {json_payload}\033[0m")
+
+
+async def reaction_issue_solved(event_payload: EventPayload) -> None:
+ # Called when an issue is solved
+ # Example use: notify a team or close related tickets
+ # This log message will appear in light blue
+ json_payload = json.dumps(event_payload.to_dict())
+ logger.info(f"\033[94mReaction: issue_solved. Event payload: {json_payload}\033[0m")
+
+
+async def reaction_alert_priority_increased(event_payload: EventPayload) -> None:
+ # Called when an alert priority increases
+ # Example use: page on-call or escalate a notification channel
+ # This log message will appear in light blue
+ json_payload = json.dumps(event_payload.to_dict())
+ logger.info(f"\033[94mReaction: alert_priority_increased. Event payload: {json_payload}\033[0m")
+
+
+reaction_options = ReactionOptions(
+ issue_created=[reaction_issue_created],
+ issue_solved=[reaction_issue_solved],
+ alert_priority_increased=[reaction_alert_priority_increased],
+)
diff --git a/example_monitors/variables_monitor/variables_monitor.py b/example_monitors/variables_monitor/variables_monitor.py
new file mode 100644
index 00000000..1707475b
--- /dev/null
+++ b/example_monitors/variables_monitor/variables_monitor.py
@@ -0,0 +1,93 @@
+"""
+Variables Monitor
+
+This monitor demonstrates the variables feature for maintaining monitor-level state.
+Variables store information about the monitor's execution, not about individual issues.
+This example uses a variable to bookmark the last timestamp processed, avoiding
+reprocessing the same events and making searches more efficient.
+"""
+
+import random
+import time
+from typing import TypedDict
+
+from monitor_utils import (
+ AlertOptions,
+ CountRule,
+ IssueOptions,
+ MonitorOptions,
+ PriorityLevels,
+ variables,
+)
+
+
+class IssueDataType(TypedDict):
+ id: int
+ event_timestamp: int
+ error_message: str
+
+
+monitor_options = MonitorOptions(
+ search_cron="* * * * *",
+ update_cron="* * * * *",
+)
+
+issue_options = IssueOptions(
+ model_id_key="id",
+ solvable=True,
+)
+
+alert_options = AlertOptions(
+ rule=CountRule(
+ priority_levels=PriorityLevels(
+ low=0,
+ moderate=2,
+ high=4,
+ critical=6,
+ )
+ )
+)
+
+
+async def search() -> list[IssueDataType] | None:
+ # Get the last timestamp we processed from variables
+ # This allows the monitor to only process new events since the last run
+ last_timestamp = await variables.get_variable("last_processed_timestamp")
+
+ # Simulate fetching events from a data source (database, API, log file, etc)
+ # In real scenarios, you'd filter events where timestamp > last_timestamp
+ events: list[IssueDataType] = []
+ now = int(time.time())
+
+ # Generate random events with timestamps in the last 5 minutes
+ # In production, 'event_time' would come from the data source
+ # Here we forge it with random values to keep the example simple
+ for i in range(random.randrange(0, 6)):
+ event_time = now - random.randrange(0, 300)
+
+ # Only include events newer than the last processed timestamp
+ if last_timestamp is None or event_time > int(last_timestamp):
+ events.append(
+ {
+ "id": random.randrange(1, 100000),
+ "event_timestamp": event_time,
+ "error_message": f"Error event {i}",
+ }
+ )
+
+ # Update the monitor bookmark to the current time
+ await variables.set_variable("last_processed_timestamp", str(now))
+
+ return events
+
+
+async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
+ # Keep the original event data unchanged
+ # Variables don't change issue data; issue updates come from the data source
+ return issues_data
+
+
+def is_solved(issue_data: IssueDataType) -> bool:
+ # Every 10 minutes, there's a 90% chance of issues being solved
+ is_solving_window = (time.time() // 60) % 10 == 0
+ return is_solving_window and random.random() < 0.9
diff --git a/poetry.lock b/poetry.lock
index 368d10bb..70b9af2f 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 2.3.1 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand.
[[package]]
name = "aiobotocore"
@@ -1685,6 +1685,18 @@ files = [
{file = "types_croniter-6.0.0.20250626.tar.gz", hash = "sha256:c32243b16d4dfa7c9989a5eadc6762459d093dded023f3c363fdee6b96578a77"},
]
+[[package]]
+name = "types-pymysql"
+version = "1.1.0.20251220"
+description = "Typing stubs for PyMySQL"
+optional = false
+python-versions = ">=3.9"
+groups = ["dev"]
+files = [
+ {file = "types_pymysql-1.1.0.20251220-py3-none-any.whl", hash = "sha256:fa1082af7dea6c53b6caa5784241924b1296ea3a8d3bd060417352c5e10c0618"},
+ {file = "types_pymysql-1.1.0.20251220.tar.gz", hash = "sha256:ae1c3df32a777489431e2e9963880a0df48f6591e0aa2fd3a6fabd9dee6eca54"},
+]
+
[[package]]
name = "types-pytz"
version = "2025.2.0.20250516"
@@ -1993,4 +2005,4 @@ propcache = ">=0.2.1"
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
-content-hash = "a90f6ebddbcbdb8a17a64d35d2fcb0a0a50619af808860a1db5152c37e2276b0"
+content-hash = "984b5f7e70c2c681432c6fb763de963da107b47ae95b4423a65af8437636eb3b"
diff --git a/pyproject.toml b/pyproject.toml
index ea8effa1..4434b02b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -42,10 +42,11 @@ pytest-cov = "*"
pytest-mock = "*"
pytest-rerunfailures = "*"
ruff = "*"
-types-requests = "*"
-types-pyyaml = "*"
-types-pytz = "*"
types-croniter = "*"
+types-pymysql = "*"
+types-pytz = "*"
+types-pyyaml = "*"
+types-requests = "*"
[tool.poetry.group.plugin-aws.dependencies]
aiobotocore = "^2.19.0"
@@ -65,7 +66,7 @@ tabulate = "^0.9.0"
types-tabulate = "*"
[tool.ruff]
-include = ["src/**/*.py", "tests/**/*.py", "internal_monitors/**/*.py", "sample_monitors/**/*.py", "tools/**/*.py"]
+include = ["src/**/*.py", "tests/**/*.py", "internal_monitors/**/*.py", "example_monitors/**/*.py", "tools/**/*.py"]
lint.extend-select = ["E", "W", "F", "Q", "I", "RET", "C4", "PERF"]
line-length = 100
@@ -74,7 +75,7 @@ case-sensitive = true
[tool.mypy]
mypy_path = "src"
-files = ["src", "tests", "internal_monitors", "sample_monitors", "tools"]
+files = ["src", "tests", "internal_monitors", "example_monitors", "tools"]
ignore_missing_imports = true
warn_return_any = true
check_untyped_defs = true
diff --git a/resources/kubernetes_template/config_map.yaml b/resources/kubernetes_template/config_map.yaml
index 7aaa9ea8..39830f87 100644
--- a/resources/kubernetes_template/config_map.yaml
+++ b/resources/kubernetes_template/config_map.yaml
@@ -10,8 +10,8 @@ data:
- postgres
- slack
- load_sample_monitors: true
- sample_monitors_path: sample_monitors
+ load_example_monitors: true
+ example_monitors_path: example_monitors
internal_monitors_path: internal_monitors
internal_monitors_notification:
enabled: true
diff --git a/resources/kubernetes_template/controller.yaml b/resources/kubernetes_template/controller.yaml
index c9e0ee2d..6d5d0856 100644
--- a/resources/kubernetes_template/controller.yaml
+++ b/resources/kubernetes_template/controller.yaml
@@ -24,9 +24,9 @@ spec:
env:
- name: CONFIGS_FILE
value: configs_sqs.yaml
- - name: SAMPLE_SLACK_CHANNEL
+ - name: EXAMPLE_SLACK_CHANNEL
value: C07NCL94SDT
- - name: SAMPLE_SLACK_MENTION
+ - name: EXAMPLE_SLACK_MENTION
value: U07NFGGMB98
- name: SLACK_WEBSOCKET_ENABLED
value: "true"
diff --git a/resources/kubernetes_template/executor.yaml b/resources/kubernetes_template/executor.yaml
index 26a1168f..e24e1924 100644
--- a/resources/kubernetes_template/executor.yaml
+++ b/resources/kubernetes_template/executor.yaml
@@ -24,9 +24,9 @@ spec:
env:
- name: CONFIGS_FILE
value: configs_sqs.yaml
- - name: SAMPLE_SLACK_CHANNEL
+ - name: EXAMPLE_SLACK_CHANNEL
value: C07NCL94SDT
- - name: SAMPLE_SLACK_MENTION
+ - name: EXAMPLE_SLACK_MENTION
value: U07NFGGMB98
- name: SLACK_WEBSOCKET_ENABLED
value: "true"
diff --git a/sample_monitors/test_monitor/test_monitor.py b/sample_monitors/test_monitor/test_monitor.py
deleted file mode 100644
index 90ecc048..00000000
--- a/sample_monitors/test_monitor/test_monitor.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import random
-from typing import TypedDict
-
-from monitor_utils import AlertOptions, CountRule, IssueOptions, MonitorOptions, PriorityLevels
-from notifications.internal_monitor_notification import internal_monitor_notification
-
-
-class IssueDataType(TypedDict):
- id: int
- value: int
-
-
-monitor_options = MonitorOptions(
- search_cron="* * * * *",
- update_cron="* * * * *",
-)
-
-issue_options = IssueOptions(
- model_id_key="id",
- solvable=True,
-)
-
-alert_options = AlertOptions(
- rule=CountRule(
- priority_levels=PriorityLevels(
- low=0,
- moderate=10,
- high=20,
- critical=30,
- )
- )
-)
-
-
-async def search() -> list[IssueDataType] | None:
- # Return 5 issues data with a random 'id' and a random 'value' between 1
- # and 10
- # As an issue is considered 'solved' (check the 'is_solved' function) if
- # their 'value' equals to 1, only issues data where the 'value' is greater
- # than 1 will be created
- # The issues created will have their 'model_id' equal to the 'id' returned
- # by this function (as defined in the 'issue_options' settings)
- return [
- {
- "id": random.randrange(1, 100000),
- "value": random.randrange(1, 10),
- }
- for _ in range(5)
- ]
-
-
-async def update(issues_data: list[IssueDataType]) -> list[IssueDataType] | None:
- # Each existing issue will have their 'value' field updated to a random
- # value between 1 and 10
- for issue_data in issues_data:
- issue_data["value"] = random.randrange(1, 10)
-
- return issues_data
-
-
-def is_solved(issue_data: IssueDataType) -> bool:
- # At the issues check stage, each issue data will be validated against this
- # rule and, if true, will be considered as solved
- return issue_data["value"] == 1
-
-
-notification_options = internal_monitor_notification(
- name="Test monitor", issues_fields=["id", "value"]
-)
diff --git a/src/components/monitors_loader/monitors_loader.py b/src/components/monitors_loader/monitors_loader.py
index 325f5973..c1559556 100644
--- a/src/components/monitors_loader/monitors_loader.py
+++ b/src/components/monitors_loader/monitors_loader.py
@@ -165,13 +165,13 @@ async def _register_monitors_from_path(
async def _register_monitors() -> None:
- """Register internal monitors and sample monitors, if enabled"""
+ """Register internal monitors and example monitors, if enabled"""
await _register_monitors_from_path(
configs.internal_monitors_path, internal=True, additional_file_extensions=["sql"]
)
- if configs.load_sample_monitors:
- await _register_monitors_from_path(configs.sample_monitors_path)
+ if configs.load_example_monitors:
+ await _register_monitors_from_path(configs.example_monitors_path)
def _configure_monitor(
@@ -327,7 +327,7 @@ async def run() -> None:
async def init(controller_enabled: bool) -> None:
- """Load the internal monitors and sample monitors if controller is enabled, and start the
+ """Load the internal monitors and example monitors if controller is enabled, and start the
monitors load task"""
if controller_enabled:
await _register_monitors()
diff --git a/src/configs/configs_loader.py b/src/configs/configs_loader.py
index 9e4fd8dc..70c80fd8 100644
--- a/src/configs/configs_loader.py
+++ b/src/configs/configs_loader.py
@@ -47,8 +47,8 @@ class ControllerProcedureConfig:
class Configs:
plugins: list[str]
- load_sample_monitors: bool
- sample_monitors_path: str
+ load_example_monitors: bool
+ example_monitors_path: str
internal_monitors_path: str
internal_monitors_notification: InternalMonitorsNotificationConfig
diff --git a/tests/commands/test_requests.py b/tests/commands/test_requests.py
index 887d0823..be4798ad 100644
--- a/tests/commands/test_requests.py
+++ b/tests/commands/test_requests.py
@@ -16,7 +16,7 @@ async def test_monitor_code_validate(mocker):
"""'monitor_code_validate' function should validate a monitor code"""
check_monitor_spy: MagicMock = mocker.spy(monitors_loader, "check_monitor")
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
await requests.monitor_code_validate(monitor_code)
@@ -36,7 +36,7 @@ async def test_monitor_register(mocker):
register_monitor_spy: AsyncMock = mocker.spy(monitors_loader, "register_monitor")
monitor_name = "test_monitor_register"
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
monitor = await requests.monitor_register(monitor_name, monitor_code, {})
@@ -54,7 +54,7 @@ async def test_monitor_register_additional_files(mocker):
register_monitor_spy: AsyncMock = mocker.spy(monitors_loader, "register_monitor")
monitor_name = "test_monitor_register"
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
monitor = await requests.monitor_register(monitor_name, monitor_code, {"file.sql": "SELECT 1;"})
diff --git a/tests/components/controller/test_controller.py b/tests/components/controller/test_controller.py
index 458b492e..c416d36f 100644
--- a/tests/components/controller/test_controller.py
+++ b/tests/components/controller/test_controller.py
@@ -325,9 +325,9 @@ async def test_create_process_task_semaphore_wait(caplog, sample_monitor: Monito
async def test_run(monkeypatch, clear_queue, clear_database):
"""Integration test of the 'run' method. It should loop through all monitors, create them in
the database and process them accordingly. When the loop stops, it should stop automatically"""
- monkeypatch.setattr(configs, "load_sample_monitors", True)
- monkeypatch.setattr(configs, "internal_monitors_path", "tests/sample_monitors/internal")
- monkeypatch.setattr(configs, "sample_monitors_path", "tests/sample_monitors/others")
+ monkeypatch.setattr(configs, "load_example_monitors", True)
+ monkeypatch.setattr(configs, "internal_monitors_path", "tests/example_monitors/internal")
+ monkeypatch.setattr(configs, "example_monitors_path", "tests/example_monitors/others")
run_procedures_mock = AsyncMock()
monkeypatch.setattr(controller, "run_procedures", run_procedures_mock)
@@ -395,9 +395,9 @@ async def test_run(monkeypatch, clear_queue, clear_database):
async def test_run_no_sleep(mocker, monkeypatch):
"""'run' should not sleep if the controller completes the loop and another one is already
triggered"""
- monkeypatch.setattr(configs, "load_sample_monitors", True)
- monkeypatch.setattr(configs, "internal_monitors_path", "tests/sample_monitors/internal")
- monkeypatch.setattr(configs, "sample_monitors_path", "tests/sample_monitors/others")
+ monkeypatch.setattr(configs, "load_example_monitors", True)
+ monkeypatch.setattr(configs, "internal_monitors_path", "tests/example_monitors/internal")
+ monkeypatch.setattr(configs, "example_monitors_path", "tests/example_monitors/others")
run_procedures_mock = AsyncMock()
monkeypatch.setattr(controller, "run_procedures", run_procedures_mock)
@@ -430,9 +430,9 @@ async def test_run_current_task_error(caplog, monkeypatch):
async def test_run_monitors_not_ready(caplog, monkeypatch, mocker):
"""'run' should loop until the monitors are ready, logging warning messages if they are not"""
- monkeypatch.setattr(configs, "load_sample_monitors", True)
- monkeypatch.setattr(configs, "internal_monitors_path", "tests/sample_monitors/internal")
- monkeypatch.setattr(configs, "sample_monitors_path", "tests/sample_monitors/others")
+ monkeypatch.setattr(configs, "load_example_monitors", True)
+ monkeypatch.setattr(configs, "internal_monitors_path", "tests/example_monitors/internal")
+ monkeypatch.setattr(configs, "example_monitors_path", "tests/example_monitors/others")
monkeypatch.setattr(registry.registry, "MONITORS_READY_TIMEOUT", 0.1)
# Run the controller for a while then stop it
@@ -449,9 +449,9 @@ async def test_run_monitors_not_ready(caplog, monkeypatch, mocker):
async def test_run_monitors_not_registered(caplog, monkeypatch, mocker):
"""'run' should handle monitors that are not registered"""
- monkeypatch.setattr(configs, "load_sample_monitors", True)
- monkeypatch.setattr(configs, "internal_monitors_path", "tests/sample_monitors/internal")
- monkeypatch.setattr(configs, "sample_monitors_path", "tests/sample_monitors/others")
+ monkeypatch.setattr(configs, "load_example_monitors", True)
+ monkeypatch.setattr(configs, "internal_monitors_path", "tests/example_monitors/internal")
+ monkeypatch.setattr(configs, "example_monitors_path", "tests/example_monitors/others")
monkeypatch.setattr(registry, "is_monitor_registered", lambda monitor_id: False)
@@ -468,9 +468,9 @@ async def test_run_monitors_not_registered(caplog, monkeypatch, mocker):
async def test_run_error(caplog, monkeypatch, clear_queue, clear_database):
"""'run' should handle errors and don't break the loop if they happen"""
- monkeypatch.setattr(configs, "load_sample_monitors", False)
- monkeypatch.setattr(configs, "internal_monitors_path", "tests/sample_monitors/internal")
- monkeypatch.setattr(configs, "sample_monitors_path", "tests/sample_monitors/others")
+ monkeypatch.setattr(configs, "load_example_monitors", False)
+ monkeypatch.setattr(configs, "internal_monitors_path", "tests/example_monitors/internal")
+ monkeypatch.setattr(configs, "example_monitors_path", "tests/example_monitors/others")
def error(*args):
raise ValueError("Not able to get the monitors")
diff --git a/tests/components/http_server/test_monitor_routes.py b/tests/components/http_server/test_monitor_routes.py
index c379781a..26604d9e 100644
--- a/tests/components/http_server/test_monitor_routes.py
+++ b/tests/components/http_server/test_monitor_routes.py
@@ -297,7 +297,7 @@ async def test_monitor_validate(mocker):
"""The 'monitor validate' route should validate the provided module code"""
monitor_code_validate_spy: AsyncMock = mocker.spy(commands, "monitor_code_validate")
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
request_payload = {"monitor_code": monitor_code}
@@ -461,7 +461,7 @@ async def test_monitor_register(mocker, clear_database, monitor_name):
monitor = await Monitor.get(Monitor.name == monitor_name)
assert monitor is None
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
request_payload = {"monitor_code": monitor_code}
@@ -489,7 +489,7 @@ async def test_monitor_register(mocker, clear_database, monitor_name):
async def test_monitor_register_batch(mocker, clear_database):
"""The 'monitor register' route should register a batch of monitors when multiple requests are
received in a small time frame"""
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
request_payload = {"monitor_code": monitor_code}
@@ -521,7 +521,7 @@ async def test_monitor_register_additional_files(mocker, clear_database):
monitor = await Monitor.get(Monitor.name == monitor_name)
assert monitor is None
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
request_payload = {
diff --git a/tests/components/monitors_loader/test_monitors_loader.py b/tests/components/monitors_loader/test_monitors_loader.py
index 9158adca..1ba38776 100644
--- a/tests/components/monitors_loader/test_monitors_loader.py
+++ b/tests/components/monitors_loader/test_monitors_loader.py
@@ -40,7 +40,7 @@ async def test_check_monitor(caplog):
"""'check_monitor' function should check a monitor code without registering it"""
monitor_name = "test_check_monitor"
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
monitors_loader.check_monitor(monitor_name, monitor_code)
@@ -55,7 +55,7 @@ async def test_check_monitor_validation_error(caplog, log_error):
does not pass the validation"""
monitor_name = f"test_check_monitor_validation_error_{log_error}"
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
# Add errors that should be caught by the validation
@@ -96,7 +96,7 @@ async def test_register_monitor(additional_files):
code"""
monitor_name = "test_register_monitor"
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
monitor = await monitors_loader.register_monitor(
@@ -127,7 +127,7 @@ async def test_register_monitor_monitor_already_exists(additional_files):
monitor_name = "test_register_monitor_monitor_already_exists"
timestamp = time_utils.now()
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
monitor = await monitors_loader.register_monitor(
@@ -173,7 +173,7 @@ async def test_register_monitor_monitor_already_exists_error():
monitor_name = "test_register_monitor_monitor_already_exists_error"
timestamp = time_utils.now()
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
monitor = await monitors_loader.register_monitor(
@@ -229,7 +229,7 @@ async def test_register_monitor_validation_error():
does not pass the validation"""
monitor_name = "test_register_monitor_validation_error"
- with open("tests/sample_monitors/others/monitor_1/monitor_1.py", "r") as file:
+ with open("tests/example_monitors/others/monitor_1/monitor_1.py", "r") as file:
monitor_code = file.read()
# Add errors that should be caught by the validation
@@ -252,24 +252,24 @@ async def test_register_monitor_validation_error():
async def test_get_monitors_files_from_path():
"""'_get_monitors_files_from_path' should return all the monitors files from a path"""
- monitors_files = list(monitors_loader._get_monitors_files_from_path("tests/sample_monitors"))
+ monitors_files = list(monitors_loader._get_monitors_files_from_path("tests/example_monitors"))
assert len(monitors_files) == 3
monitors_files = sorted(monitors_files, key=lambda monitor_files: monitor_files.monitor_name)
assert monitors_files == [
monitors_loader.MonitorFiles(
monitor_name="monitor_1",
- monitor_path=Path("tests/sample_monitors/others/monitor_1/monitor_1.py"),
+ monitor_path=Path("tests/example_monitors/others/monitor_1/monitor_1.py"),
additional_files=[],
),
monitors_loader.MonitorFiles(
monitor_name="monitor_2",
- monitor_path=Path("tests/sample_monitors/internal/monitor_2/monitor_2.py"),
+ monitor_path=Path("tests/example_monitors/internal/monitor_2/monitor_2.py"),
additional_files=[],
),
monitors_loader.MonitorFiles(
monitor_name="monitor_3",
- monitor_path=Path("tests/sample_monitors/internal/monitor_3/monitor_3.py"),
+ monitor_path=Path("tests/example_monitors/internal/monitor_3/monitor_3.py"),
additional_files=[],
),
]
@@ -280,7 +280,7 @@ async def test_get_monitors_files_from_path_with_additional_files():
their additional files"""
monitors_files = list(
monitors_loader._get_monitors_files_from_path(
- "tests/sample_monitors/internal", additional_file_extensions=["sql"]
+ "tests/example_monitors/internal", additional_file_extensions=["sql"]
)
)
@@ -293,20 +293,20 @@ async def test_get_monitors_files_from_path_with_additional_files():
assert monitors_files == [
monitors_loader.MonitorFiles(
monitor_name="monitor_2",
- monitor_path=Path("tests/sample_monitors/internal/monitor_2/monitor_2.py"),
+ monitor_path=Path("tests/example_monitors/internal/monitor_2/monitor_2.py"),
additional_files=[],
),
monitors_loader.MonitorFiles(
monitor_name="monitor_3",
- monitor_path=Path("tests/sample_monitors/internal/monitor_3/monitor_3.py"),
+ monitor_path=Path("tests/example_monitors/internal/monitor_3/monitor_3.py"),
additional_files=[
monitors_loader.AdditionalFile(
name="other_file.sql",
- path=Path("tests/sample_monitors/internal/monitor_3/other_file.sql"),
+ path=Path("tests/example_monitors/internal/monitor_3/other_file.sql"),
),
monitors_loader.AdditionalFile(
name="some_file.sql",
- path=Path("tests/sample_monitors/internal/monitor_3/some_file.sql"),
+ path=Path("tests/example_monitors/internal/monitor_3/some_file.sql"),
),
],
),
@@ -327,7 +327,7 @@ async def test_register_monitors_from_path(clear_database):
assert len(registered_monitors) == 0
- await monitors_loader._register_monitors_from_path("tests/sample_monitors")
+ await monitors_loader._register_monitors_from_path("tests/example_monitors")
registered_monitors = await Monitor.get_all()
monitors_ids = [monitor.id for monitor in registered_monitors]
@@ -347,7 +347,7 @@ async def test_register_monitors_from_path_additional_files(clear_database):
assert len(registered_monitors) == 0
await monitors_loader._register_monitors_from_path(
- "tests/sample_monitors", additional_file_extensions=["sql"]
+ "tests/example_monitors", additional_file_extensions=["sql"]
)
registered_monitors = await Monitor.get_all()
@@ -375,7 +375,7 @@ async def test_register_monitors_from_path_internal(clear_database):
assert len(registered_monitors) == 0
- await monitors_loader._register_monitors_from_path("tests/sample_monitors", internal=True)
+ await monitors_loader._register_monitors_from_path("tests/example_monitors", internal=True)
registered_monitors = await Monitor.get_all()
@@ -397,7 +397,7 @@ async def register_monitor_error_mock(monitor_name, monitor_code, additional_fil
assert len(registered_monitors) == 0
- await monitors_loader._register_monitors_from_path("tests/sample_monitors")
+ await monitors_loader._register_monitors_from_path("tests/example_monitors")
registered_monitors = await Monitor.get_all()
assert len(registered_monitors) == 0
@@ -420,7 +420,7 @@ async def register_monitor_error_mock(monitor_name, monitor_code, additional_fil
assert len(registered_monitors) == 0
- await monitors_loader._register_monitors_from_path("tests/sample_monitors")
+ await monitors_loader._register_monitors_from_path("tests/example_monitors")
registered_monitors = await Monitor.get_all()
assert len(registered_monitors) == 0
@@ -431,9 +431,9 @@ async def register_monitor_error_mock(monitor_name, monitor_code, additional_fil
async def test_register_monitors(monkeypatch, clear_database):
"""'register_monitors' should register all the internal and sample monitors if enabled,
including their additional files"""
- monkeypatch.setattr(configs, "load_sample_monitors", True)
- monkeypatch.setattr(configs, "internal_monitors_path", "tests/sample_monitors/internal")
- monkeypatch.setattr(configs, "sample_monitors_path", "tests/sample_monitors/others")
+ monkeypatch.setattr(configs, "load_example_monitors", True)
+ monkeypatch.setattr(configs, "internal_monitors_path", "tests/example_monitors/internal")
+ monkeypatch.setattr(configs, "example_monitors_path", "tests/example_monitors/others")
registered_monitors = await Monitor.get_all()
@@ -460,12 +460,12 @@ async def test_register_monitors(monkeypatch, clear_database):
assert code_modules_dict[monitor.id].additional_files == {}
-async def test_register_monitors_no_sample_monitors(monkeypatch, clear_database):
+async def test_register_monitors_no_example_monitors(monkeypatch, clear_database):
"""'register_monitors' should register all the internal monitors but not the sample monitors if
it's disabled"""
- monkeypatch.setattr(configs, "load_sample_monitors", False)
- monkeypatch.setattr(configs, "internal_monitors_path", "tests/sample_monitors/internal")
- monkeypatch.setattr(configs, "sample_monitors_path", "tests/sample_monitors/others")
+ monkeypatch.setattr(configs, "load_example_monitors", False)
+ monkeypatch.setattr(configs, "internal_monitors_path", "tests/example_monitors/internal")
+ monkeypatch.setattr(configs, "example_monitors_path", "tests/example_monitors/others")
registered_monitors = await Monitor.get_all()
@@ -808,9 +808,9 @@ async def test_run_as_controller(mocker, monkeypatch, clear_database):
When the app stops, the service's task should stop automatically"""
# Disable the monitor loading schedule to control using the 'monitors_pending' event
monkeypatch.setattr(configs, "monitors_load_schedule", "0 0 1 1 1")
- monkeypatch.setattr(configs, "load_sample_monitors", False)
- monkeypatch.setattr(configs, "internal_monitors_path", "tests/sample_monitors/internal")
- monkeypatch.setattr(configs, "sample_monitors_path", "tests/sample_monitors/others")
+ monkeypatch.setattr(configs, "load_example_monitors", False)
+ monkeypatch.setattr(configs, "internal_monitors_path", "tests/example_monitors/internal")
+ monkeypatch.setattr(configs, "example_monitors_path", "tests/example_monitors/others")
monkeypatch.setattr(monitors_loader, "COOL_DOWN_TIME", 0)
_load_monitors_spy: AsyncMock = mocker.spy(monitors_loader, "_load_monitors")
diff --git a/tests/sample_monitors/internal/monitor_2/monitor_2.py b/tests/example_monitors/internal/monitor_2/monitor_2.py
similarity index 100%
rename from tests/sample_monitors/internal/monitor_2/monitor_2.py
rename to tests/example_monitors/internal/monitor_2/monitor_2.py
diff --git a/tests/sample_monitors/internal/monitor_3/monitor_3.py b/tests/example_monitors/internal/monitor_3/monitor_3.py
similarity index 100%
rename from tests/sample_monitors/internal/monitor_3/monitor_3.py
rename to tests/example_monitors/internal/monitor_3/monitor_3.py
diff --git a/tests/sample_monitors/internal/monitor_3/other_file.sql b/tests/example_monitors/internal/monitor_3/other_file.sql
similarity index 100%
rename from tests/sample_monitors/internal/monitor_3/other_file.sql
rename to tests/example_monitors/internal/monitor_3/other_file.sql
diff --git a/tests/sample_monitors/internal/monitor_3/some_file.sql b/tests/example_monitors/internal/monitor_3/some_file.sql
similarity index 100%
rename from tests/sample_monitors/internal/monitor_3/some_file.sql
rename to tests/example_monitors/internal/monitor_3/some_file.sql
diff --git a/tests/sample_monitors/others/monitor_1/monitor_1.py b/tests/example_monitors/others/monitor_1/monitor_1.py
similarity index 100%
rename from tests/sample_monitors/others/monitor_1/monitor_1.py
rename to tests/example_monitors/others/monitor_1/monitor_1.py
diff --git a/tests/sample_monitors/others/not_a_monitor/python_code.py b/tests/example_monitors/others/not_a_monitor/python_code.py
similarity index 100%
rename from tests/sample_monitors/others/not_a_monitor/python_code.py
rename to tests/example_monitors/others/not_a_monitor/python_code.py
diff --git a/tools/install_dependencies.sh b/tools/install_dependencies.sh
index 5f0ab443..0edec647 100644
--- a/tools/install_dependencies.sh
+++ b/tools/install_dependencies.sh
@@ -1,4 +1,3 @@
-pip install poetry --no-cache-dir
poetry install --only main
plugins=$(get_plugins_list)
diff --git a/tools/install_tests_dependencies.sh b/tools/install_tests_dependencies.sh
index 292da1ef..d647aaf9 100644
--- a/tools/install_tests_dependencies.sh
+++ b/tools/install_tests_dependencies.sh
@@ -1,6 +1,7 @@
-plugins=$(get_plugins_list)
+plugins=$(get_plugins_list tests)
-poetry install --only dev
if ! [ "x$plugins" = "x" ]; then
- poetry install --only $plugins
+ poetry install --only dev,$plugins
+else
+ poetry install --only dev
fi