Add automatic background scheduler for uptime monitoring
- Implemented APScheduler for automatic uptime checks - Each monitor runs on its own configured interval (default 5 minutes) - Scheduler starts automatically on application startup - Real-time job management when monitors are created/updated/deleted - Added scheduler status and control endpoints - Background logging of all check results - Updated documentation with scheduling features
This commit is contained in:
parent
42915817b3
commit
1501f02451
22
README.md
22
README.md
@ -5,6 +5,7 @@ A FastAPI-based uptime monitoring service that allows you to monitor website/end
|
||||
## Features
|
||||
|
||||
- **Monitor Management**: Create, update, delete, and list website monitors
|
||||
- **Automatic Scheduling**: Background scheduler runs uptime checks at configured intervals
|
||||
- **Uptime Checking**: Automated and manual uptime checks with response time tracking
|
||||
- **Statistics**: Get uptime percentage, average response times, and check history
|
||||
- **RESTful API**: Full REST API with OpenAPI documentation
|
||||
@ -31,6 +32,12 @@ A FastAPI-based uptime monitoring service that allows you to monitor website/end
|
||||
- `POST /api/v1/checks/run/{monitor_id}` - Run check for specific monitor
|
||||
- `POST /api/v1/checks/run-all` - Run checks for all active monitors
|
||||
|
||||
### Scheduler Endpoints
|
||||
- `GET /api/v1/scheduler/status` - Get scheduler status and running jobs
|
||||
- `POST /api/v1/scheduler/start` - Start the background scheduler
|
||||
- `POST /api/v1/scheduler/stop` - Stop the background scheduler
|
||||
- `POST /api/v1/scheduler/restart` - Restart scheduler and reschedule all monitors
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
1. Install dependencies:
|
||||
@ -76,6 +83,11 @@ curl -X POST "http://localhost:8000/api/v1/checks/run/1"
|
||||
curl "http://localhost:8000/api/v1/monitors/1/stats"
|
||||
```
|
||||
|
||||
### Check Scheduler Status
|
||||
```bash
|
||||
curl "http://localhost:8000/api/v1/scheduler/status"
|
||||
```
|
||||
|
||||
## Monitor Configuration
|
||||
|
||||
When creating a monitor, you can configure:
|
||||
@ -87,6 +99,16 @@ When creating a monitor, you can configure:
|
||||
- **interval**: Check interval in seconds - defaults to 300 (5 minutes)
|
||||
- **is_active**: Whether the monitor is active - defaults to true
|
||||
|
||||
## Automatic Scheduling
|
||||
|
||||
The API includes a background scheduler that automatically runs uptime checks for all active monitors:
|
||||
|
||||
- **Automatic Start**: The scheduler starts automatically when the application launches
|
||||
- **Individual Intervals**: Each monitor runs on its own configured interval (default 5 minutes)
|
||||
- **Real-time Updates**: Creating, updating, or deleting monitors automatically adjusts the scheduler
|
||||
- **Logging**: All check results are logged with timestamps and status information
|
||||
- **Scheduler Management**: Use the scheduler endpoints to check status, start, stop, or restart the scheduler
|
||||
|
||||
## Database
|
||||
|
||||
The application uses SQLite database located at `/app/storage/db/db.sqlite`. The database contains:
|
||||
|
@ -11,6 +11,7 @@ from app.models.schemas import (
|
||||
UptimeCheckResponse,
|
||||
MonitorStats,
|
||||
)
|
||||
from app.services.scheduler import uptime_scheduler
|
||||
|
||||
router = APIRouter(prefix="/monitors", tags=["monitors"])
|
||||
|
||||
@ -23,6 +24,13 @@ def create_monitor(monitor: MonitorCreate, db: Session = Depends(get_db)):
|
||||
db.add(db_monitor)
|
||||
db.commit()
|
||||
db.refresh(db_monitor)
|
||||
|
||||
# Schedule the monitor if it's active
|
||||
if db_monitor.is_active:
|
||||
uptime_scheduler.add_monitor_job(
|
||||
db_monitor.id, db_monitor.interval, db_monitor.name
|
||||
)
|
||||
|
||||
return db_monitor
|
||||
|
||||
|
||||
@ -56,6 +64,12 @@ def update_monitor(
|
||||
|
||||
db.commit()
|
||||
db.refresh(db_monitor)
|
||||
|
||||
# Update the scheduled job
|
||||
uptime_scheduler.update_monitor_job(
|
||||
db_monitor.id, db_monitor.interval, db_monitor.name, db_monitor.is_active
|
||||
)
|
||||
|
||||
return db_monitor
|
||||
|
||||
|
||||
@ -65,6 +79,9 @@ def delete_monitor(monitor_id: int, db: Session = Depends(get_db)):
|
||||
if not monitor:
|
||||
raise HTTPException(status_code=404, detail="Monitor not found")
|
||||
|
||||
# Remove the scheduled job
|
||||
uptime_scheduler.remove_monitor_job(monitor_id)
|
||||
|
||||
db.delete(monitor)
|
||||
db.commit()
|
||||
return {"message": "Monitor deleted successfully"}
|
||||
|
54
app/routers/scheduler.py
Normal file
54
app/routers/scheduler.py
Normal file
@ -0,0 +1,54 @@
|
||||
from fastapi import APIRouter
|
||||
from app.services.scheduler import uptime_scheduler
|
||||
|
||||
router = APIRouter(prefix="/scheduler", tags=["scheduler"])
|
||||
|
||||
|
||||
@router.get("/status")
|
||||
def get_scheduler_status():
|
||||
"""Get the current status of the scheduler and all running jobs"""
|
||||
if not uptime_scheduler.is_running:
|
||||
return {"status": "stopped", "jobs": []}
|
||||
|
||||
jobs = []
|
||||
for job in uptime_scheduler.scheduler.get_jobs():
|
||||
jobs.append(
|
||||
{
|
||||
"id": job.id,
|
||||
"name": job.name,
|
||||
"next_run_time": job.next_run_time.isoformat()
|
||||
if job.next_run_time
|
||||
else None,
|
||||
"trigger": str(job.trigger),
|
||||
}
|
||||
)
|
||||
|
||||
return {"status": "running", "total_jobs": len(jobs), "jobs": jobs}
|
||||
|
||||
|
||||
@router.post("/start")
|
||||
def start_scheduler():
|
||||
"""Start the scheduler"""
|
||||
if uptime_scheduler.is_running:
|
||||
return {"message": "Scheduler is already running"}
|
||||
|
||||
uptime_scheduler.start()
|
||||
return {"message": "Scheduler started successfully"}
|
||||
|
||||
|
||||
@router.post("/stop")
|
||||
def stop_scheduler():
|
||||
"""Stop the scheduler"""
|
||||
if not uptime_scheduler.is_running:
|
||||
return {"message": "Scheduler is already stopped"}
|
||||
|
||||
uptime_scheduler.stop()
|
||||
return {"message": "Scheduler stopped successfully"}
|
||||
|
||||
|
||||
@router.post("/restart")
|
||||
def restart_scheduler():
|
||||
"""Restart the scheduler and reschedule all monitors"""
|
||||
uptime_scheduler.stop()
|
||||
uptime_scheduler.start()
|
||||
return {"message": "Scheduler restarted successfully"}
|
134
app/services/scheduler.py
Normal file
134
app/services/scheduler.py
Normal file
@ -0,0 +1,134 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.interval import IntervalTrigger
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from app.db.session import engine
|
||||
from app.models.monitor import Monitor
|
||||
from app.services.uptime_checker import UptimeChecker
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UptimeScheduler:
|
||||
def __init__(self):
|
||||
self.scheduler = BackgroundScheduler()
|
||||
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
self.is_running = False
|
||||
|
||||
def start(self):
|
||||
if not self.is_running:
|
||||
self.scheduler.start()
|
||||
self.is_running = True
|
||||
logger.info("Uptime scheduler started")
|
||||
self.schedule_all_monitors()
|
||||
|
||||
def stop(self):
|
||||
if self.is_running:
|
||||
self.scheduler.shutdown()
|
||||
self.is_running = False
|
||||
logger.info("Uptime scheduler stopped")
|
||||
|
||||
def schedule_all_monitors(self):
|
||||
"""Schedule all active monitors based on their individual intervals"""
|
||||
db = self.SessionLocal()
|
||||
try:
|
||||
active_monitors = db.query(Monitor).filter(Monitor.is_active).all()
|
||||
|
||||
for monitor in active_monitors:
|
||||
job_id = f"monitor_{monitor.id}"
|
||||
|
||||
# Remove existing job if it exists
|
||||
if self.scheduler.get_job(job_id):
|
||||
self.scheduler.remove_job(job_id)
|
||||
|
||||
# Add new job with the monitor's interval
|
||||
self.scheduler.add_job(
|
||||
func=self.check_monitor,
|
||||
trigger=IntervalTrigger(seconds=monitor.interval),
|
||||
id=job_id,
|
||||
args=[monitor.id],
|
||||
name=f"Check {monitor.name}",
|
||||
replace_existing=True,
|
||||
next_run_time=datetime.now()
|
||||
+ timedelta(seconds=10), # Start after 10 seconds
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Scheduled monitor '{monitor.name}' (ID: {monitor.id}) to run every {monitor.interval} seconds"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error scheduling monitors: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def check_monitor(self, monitor_id: int):
|
||||
"""Run uptime check for a specific monitor"""
|
||||
db = self.SessionLocal()
|
||||
try:
|
||||
monitor = db.query(Monitor).filter(Monitor.id == monitor_id).first()
|
||||
if monitor and monitor.is_active:
|
||||
checker = UptimeChecker(db)
|
||||
result = checker.check_monitor(monitor)
|
||||
|
||||
status = "UP" if result["is_up"] else "DOWN"
|
||||
logger.info(
|
||||
f"Monitor '{monitor.name}' (ID: {monitor_id}): {status} - Response time: {result['response_time']}ms"
|
||||
)
|
||||
else:
|
||||
# Monitor was deleted or deactivated, remove the job
|
||||
job_id = f"monitor_{monitor_id}"
|
||||
if self.scheduler.get_job(job_id):
|
||||
self.scheduler.remove_job(job_id)
|
||||
logger.info(
|
||||
f"Removed job for inactive/deleted monitor ID: {monitor_id}"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking monitor {monitor_id}: {e}")
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def add_monitor_job(self, monitor_id: int, interval: int, name: str):
|
||||
"""Add a job for a new monitor"""
|
||||
job_id = f"monitor_{monitor_id}"
|
||||
|
||||
# Remove existing job if it exists
|
||||
if self.scheduler.get_job(job_id):
|
||||
self.scheduler.remove_job(job_id)
|
||||
|
||||
# Add new job
|
||||
self.scheduler.add_job(
|
||||
func=self.check_monitor,
|
||||
trigger=IntervalTrigger(seconds=interval),
|
||||
id=job_id,
|
||||
args=[monitor_id],
|
||||
name=f"Check {name}",
|
||||
replace_existing=True,
|
||||
next_run_time=datetime.now() + timedelta(seconds=10),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"Added scheduler job for monitor '{name}' (ID: {monitor_id}) with {interval} second interval"
|
||||
)
|
||||
|
||||
def remove_monitor_job(self, monitor_id: int):
|
||||
"""Remove a job for a deleted monitor"""
|
||||
job_id = f"monitor_{monitor_id}"
|
||||
if self.scheduler.get_job(job_id):
|
||||
self.scheduler.remove_job(job_id)
|
||||
logger.info(f"Removed scheduler job for monitor ID: {monitor_id}")
|
||||
|
||||
def update_monitor_job(
|
||||
self, monitor_id: int, interval: int, name: str, is_active: bool
|
||||
):
|
||||
"""Update a job for a modified monitor"""
|
||||
if is_active:
|
||||
self.add_monitor_job(monitor_id, interval, name)
|
||||
else:
|
||||
self.remove_monitor_job(monitor_id)
|
||||
|
||||
|
||||
# Global scheduler instance
|
||||
uptime_scheduler = UptimeScheduler()
|
23
main.py
23
main.py
@ -1,17 +1,37 @@
|
||||
import logging
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.db.session import engine
|
||||
from app.db.base import Base
|
||||
from app.routers import monitors, checks
|
||||
from app.routers import monitors, checks, scheduler
|
||||
from app.services.scheduler import uptime_scheduler
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Create database tables
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Startup
|
||||
logger.info("Starting Uptime Monitoring API...")
|
||||
uptime_scheduler.start()
|
||||
yield
|
||||
# Shutdown
|
||||
logger.info("Shutting down Uptime Monitoring API...")
|
||||
uptime_scheduler.stop()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Uptime Monitoring API",
|
||||
description="API for monitoring website/endpoint uptime and performance",
|
||||
version="1.0.0",
|
||||
openapi_url="/openapi.json",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
@ -25,6 +45,7 @@ app.add_middleware(
|
||||
# Include routers
|
||||
app.include_router(monitors.router, prefix="/api/v1")
|
||||
app.include_router(checks.router, prefix="/api/v1")
|
||||
app.include_router(scheduler.router, prefix="/api/v1")
|
||||
|
||||
|
||||
@app.get("/")
|
||||
|
@ -6,3 +6,4 @@ pydantic==2.5.0
|
||||
requests==2.31.0
|
||||
python-multipart==0.0.6
|
||||
ruff==0.1.6
|
||||
apscheduler==3.10.4
|
Loading…
x
Reference in New Issue
Block a user