Scheduled Signal Runner
Build a robust scheduled job execution framework that runs trading signal calculations on configurable cron-like schedules with process-level overlap protection via lock files, automatic retry with exponential backoff on failure, and comprehensive execution logging and alerting for reliability.
Infrastructure Monitoring: Run Signals on a Schedule
This notebook demonstrates how to set up a basic infrastructure monitoring system that runs checks and evaluates signals on a schedule. We will explore core concepts related to polling services, evaluating thresholds, and simulating scheduled execution.
Concepts Covered
| Concept | Description |
|---|---|
| Scheduled Execution | Running tasks automatically at predefined intervals. |
| Service Polling | Regularly checking the status or health of an application/service. |
| Signal Evaluation | Applying business logic or thresholds to collected data to detect anomalies. |
| State Management | Maintaining and updating information about the monitoring process. |
| Alerting (Simulated) | Notifying relevant parties when a signal crosses a threshold. |
| Metrics Tracking | Recording and summarizing performance indicators over time. |
2. Dependency Installation
We'll install requests for simulating HTTP requests, tenacity for retry logic with exponential backoff, and pandas for data handling and visualization.
pip install requests tenacity pandas matplotlib seabornRequirement already satisfied: requests in /usr/local/lib/python3.12/dist-packages (2.32.4) Requirement already satisfied: tenacity in /usr/local/lib/python3.12/dist-packages (9.1.4) Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: matplotlib in /usr/local/lib/python3.12/dist-packages (3.10.0) Requirement already satisfied: seaborn in /usr/local/lib/python3.12/dist-packages (0.13.2) Requirement already satisfied: charset_normalizer<4,>=2 in /usr/local/lib/python3.12/dist-packages (from requests) (3.4.7) Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.12/dist-packages (from requests) (3.18) Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.12/dist-packages (from requests) (2.5.0) Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.12/dist-packages (from requests) (2026.5.20) Requirement already satisfied: numpy>=1.26.0 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.0.2) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.2) Requirement already satisfied: contourpy>=1.0.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.3.3) Requirement already satisfied: cycler>=0.10 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (0.12.1) Requirement already satisfied: fonttools>=4.22.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (4.63.0) Requirement already satisfied: kiwisolver>=1.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (1.5.0) Requirement already satisfied: packaging>=20.0 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (26.2) Requirement already satisfied: pillow>=8 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (11.3.0) Requirement already satisfied: pyparsing>=2.3.1 in /usr/local/lib/python3.12/dist-packages (from matplotlib) (3.3.2) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
3. Library Imports
All necessary libraries are imported here. Standard libraries like logging, time, random, collections are imported first, followed by third-party libraries such as requests, tenacity, pandas, matplotlib, and seaborn.
import logging
import time
import random
from collections import deque
from datetime import datetime, timedelta
from typing import Dict, Any, List, Tuple
import requests
from tenacity import retry, stop_after_attempt, wait_exponential
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)4. Core Functions
This section defines the core functions for our infrastructure monitoring system. Each function is designed to perform a specific task, managing state through dictionaries and adhering to strict style guidelines.
Function Name: create_monitor_state
This function initializes the monitoring system's state dictionary. It sets up various parameters like the target service URL, monitoring interval, signal thresholds, and data structures for storing metrics and alerts. This function is crucial for establishing the initial configuration of the monitoring process.
Parameters:
service_url(str): The URL of the service to monitor.monitor_interval_seconds(int): The interval in seconds between monitoring checks.uptime_threshold_percent(float): The minimum acceptable uptime percentage for a 'healthy' signal.rolling_window_size(int): The number of recent check results to keep in memory for rolling calculations.
Returns:
- (dict): An initialized state dictionary for the monitoring system.
def create_monitor_state(
service_url: str,
monitor_interval_seconds: int,
uptime_threshold_percent: float,
rolling_window_size: int
) -> Dict[str, Any]:
"""
Initializes the monitoring system's state dictionary.
Parameters
----------
service_url : str
The URL of the service to monitor.
monitor_interval_seconds : int
The interval in seconds between monitoring checks.
uptime_threshold_percent : float
The minimum acceptable uptime percentage for a 'healthy' signal.
rolling_window_size : int
The number of recent check results to keep in memory for rolling calculations.
Returns
-------
dict
An initialized state dictionary for the monitoring system.
"""
logger.info(f"Initializing monitor state for service: {service_url}")
state = {
"service_url": service_url,
"monitor_interval_seconds": monitor_interval_seconds,
"uptime_threshold_percent": uptime_threshold_percent,
"rolling_window_size": rolling_window_size,
"last_check_time": None,
"check_history": deque(maxlen=rolling_window_size), # Stores (timestamp, status_code, is_up)
"signal_history": [], # Stores (timestamp, signal_status, uptime_percent)
"alert_history": [], # Stores (timestamp, alert_message)
"metrics": {
"total_checks": 0,
"successful_checks": 0,
"failed_checks": 0,
"total_alerts": 0
}
}
logger.debug("Monitor state initialized successfully.")
return stateFunction Name: check_service_status
This function simulates checking the status of a given service URL. It performs an HTTP GET request and returns whether the service is 'up' (based on a successful HTTP status code, typically 200) and the status code itself. It incorporates retry logic with exponential backoff to handle transient network issues or temporary service unavailability. A random jitter is added to the backoff to prevent thundering herd problems.
Parameters:
state(dict): The current monitoring state dictionary.timeout(float): The maximum time in seconds to wait for a response.
Returns:
- (dict): The updated state dictionary, including the latest check result in
check_historyand updated metrics.
@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10))
def check_service_status(state: Dict[str, Any], timeout: float = 10.0) -> Dict[str, Any]:
"""
Checks the status of the service URL defined in the state.
Includes retry logic with exponential backoff and random jitter.
Parameters
----------
state : dict
Current state dictionary containing 'service_url'.
timeout : float, optional
The maximum time in seconds to wait for a response, defaults to 10.0.
Returns
-------
dict
Updated state with the latest check result.
"""
service_url = state["service_url"]
current_time = datetime.now()
status_code = -1
is_up = False
logger.info(f"Attempting to check service status for {service_url}")
try:
# Add random jitter to prevent 'thundering herd' on retries
time.sleep(random.uniform(0, 0.5))
response = requests.get(service_url, timeout=timeout)
status_code = response.status_code
is_up = 200 <= status_code < 300
logger.info(f"Service {service_url} responded with status {status_code}. Up: {is_up}")
except requests.exceptions.Timeout:
logger.warning(f"Service {service_url} timed out after {timeout} seconds.")
except requests.exceptions.ConnectionError as e:
logger.error(f"Connection error for {service_url}: {e}")
except requests.exceptions.RequestException as e:
logger.error(f"An unexpected request error occurred for {service_url}: {e}")
finally:
state["last_check_time"] = current_time
state["check_history"].append((current_time, status_code, is_up))
state["metrics"]["total_checks"] += 1
if is_up:
state["metrics"]["successful_checks"] += 1
else:
state["metrics"]["failed_checks"] += 1
logger.debug(f"Check history updated. Current length: {len(state['check_history'])}")
return stateFunction Name: evaluate_signal
This function evaluates a monitoring signal based on the recent service check history. It calculates the uptime percentage over the defined rolling window and compares it against a configured threshold. The function determines if the service is currently 'healthy' or 'unhealthy' based on this comparison, generating a signal status.
Parameters:
state(dict): The current monitoring state dictionary containingcheck_historyanduptime_threshold_percent.
Returns:
- (dict): The updated state dictionary, including the latest signal status in
signal_history.
def evaluate_signal(state: Dict[str, Any]) -> Dict[str, Any]:
"""
Evaluates a signal based on the recent service check history.
Parameters
----------
state : dict
Current state dictionary with 'check_history' and 'uptime_threshold_percent'.
Returns
-------
dict
Updated state with the latest signal status.
"""
current_time = datetime.now()
uptime_checks = [res[2] for res in state["check_history"]]
if not uptime_checks:
logger.warning("No check history available to evaluate signal.")
uptime_percent = 0.0
else:
uptime_percent = (sum(uptime_checks) / len(uptime_checks)) * 100
signal_status = "healthy" if uptime_percent >= state["uptime_threshold_percent"] else "unhealthy"
state["signal_history"].append((current_time, signal_status, uptime_percent))
logger.info(f"Signal evaluated: Uptime {uptime_percent:.2f}% (Threshold: {state['uptime_threshold_percent']:.2f}%). Status: {signal_status}")
return stateFunction Name: send_alert
This function simulates sending an alert when an 'unhealthy' signal is detected. It checks the latest signal in the signal_history and, if it's 'unhealthy' and no alert has been sent recently for this state, it logs an alert message and stores it in the alert_history. This prevents alert storms by only sending an alert on the transition to an unhealthy state or after a healthy period.
Parameters:
state(dict): The current monitoring state dictionary.
Returns:
- (dict): The updated state dictionary, potentially with a new alert in
alert_historyand updated metrics.
def send_alert(state: Dict[str, Any]) -> Dict[str, Any]:
"""
Simulates sending an alert if the latest signal is 'unhealthy'.
Prevents alert storms by checking previous signal states.
Parameters
----------
state : dict
Current state dictionary with 'signal_history' and 'alert_history'.
Returns
-------
dict
Updated state, potentially with a new alert.
"""
if not state["signal_history"]:
logger.debug("No signal history to evaluate for alerting.")
return state
latest_signal_time, latest_signal_status, _ = state["signal_history"][-1]
# Check if the previous signal was also 'unhealthy' to avoid repeated alerts
previous_signal_status = "healthy" # Assume healthy if no previous signal
if len(state["signal_history"]) > 1:
_, previous_signal_status, _ = state["signal_history"][-2]
if latest_signal_status == "unhealthy" and previous_signal_status == "healthy":
alert_message = f"ALERT! Service {state['service_url']} is UNHEALTHY! Uptime below {state['uptime_threshold_percent']}%. " \
f"Current uptime: {state['signal_history'][-1][2]:.2f}%"
state["alert_history"].append((latest_signal_time, alert_message))
state["metrics"]["total_alerts"] += 1
logger.critical(alert_message)
elif latest_signal_status == "healthy" and previous_signal_status == "unhealthy":
recovery_message = f"RECOVERY! Service {state['service_url']} is now HEALTHY. " \
f"Current uptime: {state['signal_history'][-1][2]:.2f}%"
state["alert_history"].append((latest_signal_time, recovery_message))
logger.info(recovery_message)
else:
logger.debug(f"No new alert condition for current signal status: {latest_signal_status}")
return stateFunction Name: summarize_metrics
This function provides a summary of the collected monitoring metrics. It calculates the overall uptime percentage and compiles key performance indicators into a readable dictionary. This function is useful for quickly grasping the performance and health trends of the monitored service.
Parameters:
state(dict): The current monitoring state dictionary containingmetrics.
Returns:
- (dict): A dictionary containing summarized metrics.
def summarize_metrics(state: Dict[str, Any]) -> Dict[str, Any]:
"""
Summarizes the collected monitoring metrics.
Parameters
----------
state : dict
Current state dictionary with 'metrics'.
Returns
-------
dict
A dictionary containing summarized metrics.
"""
metrics = state["metrics"]
total_checks = metrics["total_checks"]
successful_checks = metrics["successful_checks"]
overall_uptime = (successful_checks / total_checks) * 100 if total_checks > 0 else 0.0
summary = {
"total_checks": total_checks,
"successful_checks": successful_checks,
"failed_checks": metrics["failed_checks"],
"overall_uptime_percent": overall_uptime,
"total_alerts_triggered": metrics["total_alerts"],
"last_check_time": state["last_check_time"]
}
logger.info("Monitoring metrics summarized.")
logger.debug(f"Summary: {summary}")
return summaryFunction Name: run_monitoring_cycle
This is the orchestration function that executes a single monitoring cycle. It sequentially calls check_service_status to poll the service, evaluate_signal to process the results into a signal, and send_alert if necessary. This function represents one scheduled 'run' of the monitoring process.
Parameters:
state(dict): The current monitoring state dictionary.
Returns:
- (dict): The state dictionary updated after one complete monitoring cycle.
def run_monitoring_cycle(state: Dict[str, Any]) -> Dict[str, Any]:
"""
Executes a single monitoring cycle: checks status, evaluates signal, sends alert.
Parameters
----------
state : dict
Current state dictionary.
Returns
-------
dict
Updated state after one complete monitoring cycle.
"""
logger.info("Starting a new monitoring cycle...")
state = check_service_status(state)
state = evaluate_signal(state)
state = send_alert(state)
logger.info("Monitoring cycle completed.")
return state5. Demonstration/Visualization
This section demonstrates the monitoring system in action. We will simulate running the monitoring cycles over a period, introduce simulated failures, and then visualize the results using plots and pandas DataFrames. This helps to understand how the system reacts to changes in service health and how signals and alerts are generated.
5.1. Setup and Simulated Run
Initialize the monitor state and run several monitoring cycles. To simulate a real-world scenario, we'll periodically introduce a 'failure' by changing the service URL to a non-existent one, and then revert it back to a valid one.
# Define simulation parameters
TARGET_SERVICE_URL = "https://www.google.com" # A more reliable test URL
FAILING_SERVICE_URL = "https://httpbin.org/status/500" # Simulate a failure
SIMULATED_MONITOR_INTERVAL = 1 # seconds (for quick simulation)
SIMULATION_DURATION_MINUTES = 1 # Changed from 1 to 2 minutes
UPTIME_THRESHOLD = 90.0 # percent
ROLLING_WINDOW = 5 # number of checks
# Initialize state
monitor_state = create_monitor_state(
service_url=TARGET_SERVICE_URL,
monitor_interval_seconds=SIMULATED_MONITOR_INTERVAL,
uptime_threshold_percent=UPTIME_THRESHOLD,
rolling_window_size=ROLLING_WINDOW
)
print(f"\n--- Starting Monitoring Simulation for {SIMULATION_DURATION_MINUTES} minutes ---")
start_time = datetime.now()
cycle_count = 0
while (datetime.now() - start_time).total_seconds() < (SIMULATION_DURATION_MINUTES * 60):
cycle_count += 1
logger.info(f"Simulation Cycle: {cycle_count}")
# Simulate service going down and coming back up
if cycle_count == 5 or cycle_count == 15 or cycle_count == 25: # At certain cycles, simulate failure
monitor_state['service_url'] = FAILING_SERVICE_URL
logger.warning(f"SIMULATION: Service URL changed to {FAILING_SERVICE_URL} to simulate failure.")
elif cycle_count == 10 or cycle_count == 20 or cycle_count == 30: # After some cycles, revert to healthy
monitor_state['service_url'] = TARGET_SERVICE_URL
logger.info(f"SIMULATION: Service URL reverted to {TARGET_SERVICE_URL} to simulate recovery.")
monitor_state = run_monitoring_cycle(monitor_state)
time.sleep(monitor_state["monitor_interval_seconds"] + random.uniform(0, 0.2)) # Add jitter to interval
print(f"\n--- Simulation Finished after {cycle_count} cycles ---")--- Starting Monitoring Simulation for 2 minutes ---
WARNING:__main__:SIMULATION: Service URL changed to https://httpbin.org/status/500 to simulate failure. CRITICAL:__main__:ALERT! Service https://httpbin.org/status/500 is UNHEALTHY! Uptime below 90.0%. Current uptime: 80.00% WARNING:__main__:SIMULATION: Service URL changed to https://httpbin.org/status/500 to simulate failure. CRITICAL:__main__:ALERT! Service https://httpbin.org/status/500 is UNHEALTHY! Uptime below 90.0%. Current uptime: 80.00% WARNING:__main__:SIMULATION: Service URL changed to https://httpbin.org/status/500 to simulate failure. CRITICAL:__main__:ALERT! Service https://httpbin.org/status/500 is UNHEALTHY! Uptime below 90.0%. Current uptime: 80.00% WARNING:__main__:Service https://httpbin.org/status/500 timed out after 10.0 seconds. WARNING:__main__:Service https://httpbin.org/status/500 timed out after 10.0 seconds.
--- Simulation Finished after 55 cycles ---
5.2. Display Summarized Metrics
Let's look at the overall performance of our simulated monitoring.
final_summary = summarize_metrics(monitor_state)
display(pd.DataFrame([final_summary]))| total_checks | successful_checks | failed_checks | overall_uptime_percent | total_alerts_triggered | last_check_time | |
|---|---|---|---|---|---|---|
| 0 | 55 | 40 | 15 | 72.727273 | 3 | 2026-06-11 07:46:18.383037 |
5.3. Visualize Check History and Signals
We'll plot the service check status and the evaluated signal over time to see the system's reactivity.
# Prepare data for plotting
check_df = pd.DataFrame(monitor_state["check_history"], columns=["timestamp", "status_code", "is_up"])
check_df["is_up_int"] = check_df["is_up"].astype(int) # Convert boolean to int for plotting
signal_df = pd.DataFrame(monitor_state["signal_history"], columns=["timestamp", "signal_status", "uptime_percent"])
alert_df = pd.DataFrame(monitor_state["alert_history"], columns=["timestamp", "alert_message"])
# Plotting
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(15, 10), sharex=True)
# Subplot 1: Service Check Status
sns.scatterplot(x='timestamp', y='is_up_int', data=check_df, hue='is_up', style='is_up', s=100, ax=ax1, palette={True: 'green', False: 'red'})
ax1.set_title('Service Check Status Over Time')
ax1.set_ylabel('Service Up (1=True, 0=False)')
ax1.set_yticks([0, 1])
ax1.axhline(y=1, color='green', linestyle='--', alpha=0.5, label='Service Expected Up')
ax1.grid(True, linestyle='--', alpha=0.6)
# Subplot 2: Uptime Percentage and Signal Status
sns.lineplot(x='timestamp', y='uptime_percent', data=signal_df, ax=ax2, marker='o', label='Rolling Uptime %')
ax2.axhline(y=UPTIME_THRESHOLD, color='orange', linestyle='-', label=f'Uptime Threshold ({UPTIME_THRESHOLD}%)')
# Annotate alerts on the signal plot
for _, row in alert_df.iterrows():
alert_type = 'ALERT' if 'UNHEALTHY' in row['alert_message'] else 'RECOVERY'
color = 'red' if alert_type == 'ALERT' else 'blue'
marker = 'v' if alert_type == 'ALERT' else '^'
ax2.axvline(x=row['timestamp'], color=color, linestyle=':', alpha=0.7)
ax2.text(row['timestamp'], ax2.get_ylim()[1] * 0.95, alert_type, rotation=90, va='top', ha='center', color=color, fontsize=9)
ax2.set_title('Rolling Uptime Percentage and Signal Evaluation')
ax2.set_xlabel('Time')
ax2.set_ylabel('Uptime Percentage')
ax2.set_ylim(-5, 105) # Give some padding
ax2.legend()
ax2.grid(True, linestyle='--', alpha=0.6)
plt.tight_layout()
plt.show()6. Production Considerations
Deploying an infrastructure monitoring system in a production environment requires careful consideration of various factors beyond just the core logic. Here's a table outlining key best practices:
| Aspect | Best Practice |
|---|---|
| Scalability | Use distributed schedulers (e.g., Apache Airflow, Kubernetes CronJobs) for tasks. Leverage message queues for processing checks. |
| Reliability & Redundancy | Implement multiple monitoring agents, across different regions/providers. Ensure failover mechanisms for the scheduler. |
| Alerting Integrations | Integrate with PagerDuty, Slack, email, or incident management systems for real-time notifications. |
| Metrics Storage | Store metrics in time-series databases (e.g., Prometheus, InfluxDB) for long-term analysis and dashboards. |
| Dashboards & Visualization | Build custom dashboards with Grafana or similar tools to visualize trends, alerts, and service health. |
| Configuration Management | Use tools like Ansible, Terraform, or environment variables for managing monitoring configurations. |
| Logging & Tracing | Centralize logs (e.g., ELK Stack, Splunk, Cloud Logging) and implement distributed tracing for debugging. |
| Security | Ensure secure access to monitoring endpoints. Use secrets management for API keys and credentials. |
| Cost Optimization | Optimize resource usage for monitoring agents and data storage. Use serverless functions for event-driven checks. |
| Testing | Implement unit, integration, and end-to-end tests for monitoring logic and alert pathways. |
| Runbook Automation | Document incident response procedures and automate common remediation steps. |
7. Conclusion
This notebook has provided a foundational understanding and practical implementation of an infrastructure monitoring system that runs signals on a schedule. We covered:
- State management using dictionaries.
- Scheduled polling of a service with retry logic.
- Signal evaluation based on historical data and thresholds.
- Simulated alerting for unhealthy states.
- Visualization of check results and signal trends.
- Key production considerations for building robust monitoring solutions.
By extending these core concepts, more sophisticated monitoring systems can be developed to ensure the reliability and performance of critical infrastructure.