Infrastructure·System Monitoring·Intermediate

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.

infrastructuresignal-generation

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

ConceptDescription
Scheduled ExecutionRunning tasks automatically at predefined intervals.
Service PollingRegularly checking the status or health of an application/service.
Signal EvaluationApplying business logic or thresholds to collected data to detect anomalies.
State ManagementMaintaining and updating information about the monitoring process.
Alerting (Simulated)Notifying relevant parties when a signal crosses a threshold.
Metrics TrackingRecording 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.

[1]
pip install requests tenacity pandas matplotlib seaborn
Requirement 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.

[2]
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.
[3]
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 state

Function 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_history and updated metrics.
[12]
@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 state

Function 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 containing check_history and uptime_threshold_percent.

Returns:

  • (dict): The updated state dictionary, including the latest signal status in signal_history.
[5]
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 state

Function 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_history and updated metrics.
[6]
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 state

Function 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 containing metrics.

Returns:

  • (dict): A dictionary containing summarized metrics.
[7]
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 summary

Function 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.
[8]
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 state

5. 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.

[14]
# 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.

[15]
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.

[16]
# 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()
cell output

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:

AspectBest Practice
ScalabilityUse distributed schedulers (e.g., Apache Airflow, Kubernetes CronJobs) for tasks. Leverage message queues for processing checks.
Reliability & RedundancyImplement multiple monitoring agents, across different regions/providers. Ensure failover mechanisms for the scheduler.
Alerting IntegrationsIntegrate with PagerDuty, Slack, email, or incident management systems for real-time notifications.
Metrics StorageStore metrics in time-series databases (e.g., Prometheus, InfluxDB) for long-term analysis and dashboards.
Dashboards & VisualizationBuild custom dashboards with Grafana or similar tools to visualize trends, alerts, and service health.
Configuration ManagementUse tools like Ansible, Terraform, or environment variables for managing monitoring configurations.
Logging & TracingCentralize logs (e.g., ELK Stack, Splunk, Cloud Logging) and implement distributed tracing for debugging.
SecurityEnsure secure access to monitoring endpoints. Use secrets management for API keys and credentials.
Cost OptimizationOptimize resource usage for monitoring agents and data storage. Use serverless functions for event-driven checks.
TestingImplement unit, integration, and end-to-end tests for monitoring logic and alert pathways.
Runbook AutomationDocument 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.