feat: indie status page MVP -- FastAPI + SQLite
- 8 DB models (services, incidents, monitors, subscribers, etc.) - Full CRUD API for services, incidents, monitors - Public status page with live data - Incident detail page with timeline - API key authentication - Uptime monitoring scheduler - 13 tests passing - TECHNICAL_DESIGN.md with full spec
This commit is contained in:
commit
902133edd3
4655 changed files with 1342691 additions and 0 deletions
166
app/api/monitors.py
Normal file
166
app/api/monitors.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
"""Monitors API endpoints."""
|
||||
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.dependencies import get_db, verify_api_key
|
||||
from app.models.models import Monitor, MonitorResult
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class MonitorCreate(BaseModel):
|
||||
service_id: UUID
|
||||
url: str = Field(..., max_length=500)
|
||||
method: str = Field("GET", pattern=r"^(GET|POST|HEAD)$")
|
||||
expected_status: int = 200
|
||||
timeout_seconds: int = Field(10, ge=1, le=60)
|
||||
interval_seconds: int = Field(60, ge=30, le=3600)
|
||||
|
||||
|
||||
class MonitorUpdate(BaseModel):
|
||||
url: str | None = Field(None, max_length=500)
|
||||
method: str | None = Field(None, pattern=r"^(GET|POST|HEAD)$")
|
||||
expected_status: int | None = None
|
||||
timeout_seconds: int | None = Field(None, ge=1, le=60)
|
||||
interval_seconds: int | None = Field(None, ge=30, le=3600)
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
def serialize_monitor(m: Monitor) -> dict:
|
||||
return {
|
||||
"id": m.id,
|
||||
"service_id": m.service_id,
|
||||
"url": m.url,
|
||||
"method": m.method,
|
||||
"expected_status": m.expected_status,
|
||||
"timeout_seconds": m.timeout_seconds,
|
||||
"interval_seconds": m.interval_seconds,
|
||||
"is_active": m.is_active,
|
||||
"created_at": m.created_at.isoformat() if m.created_at else None,
|
||||
"updated_at": m.updated_at.isoformat() if m.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_monitors(db: AsyncSession = Depends(get_db)):
|
||||
"""List all monitors."""
|
||||
result = await db.execute(select(Monitor))
|
||||
monitors = result.scalars().all()
|
||||
return [serialize_monitor(m) for m in monitors]
|
||||
|
||||
|
||||
@router.post("/", status_code=status.HTTP_201_CREATED)
|
||||
async def create_monitor(
|
||||
data: MonitorCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Create a new monitor."""
|
||||
monitor = Monitor(
|
||||
service_id=str(data.service_id),
|
||||
url=data.url,
|
||||
method=data.method,
|
||||
expected_status=data.expected_status,
|
||||
timeout_seconds=data.timeout_seconds,
|
||||
interval_seconds=data.interval_seconds,
|
||||
)
|
||||
db.add(monitor)
|
||||
await db.flush()
|
||||
await db.refresh(monitor)
|
||||
return serialize_monitor(monitor)
|
||||
|
||||
|
||||
@router.get("/{monitor_id}")
|
||||
async def get_monitor(monitor_id: UUID, db: AsyncSession = Depends(get_db)):
|
||||
"""Get a monitor with recent results."""
|
||||
result = await db.execute(select(Monitor).where(Monitor.id == str(monitor_id)))
|
||||
monitor = result.scalar_one_or_none()
|
||||
if not monitor:
|
||||
raise HTTPException(status_code=404, detail="Monitor not found")
|
||||
|
||||
# Query recent results separately
|
||||
results_query = (
|
||||
select(MonitorResult)
|
||||
.where(MonitorResult.monitor_id == str(monitor_id))
|
||||
.order_by(MonitorResult.checked_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
results_result = await db.execute(results_query)
|
||||
recent_results = results_result.scalars().all()
|
||||
|
||||
data = serialize_monitor(monitor)
|
||||
data["recent_results"] = [
|
||||
{
|
||||
"id": r.id,
|
||||
"status": r.status,
|
||||
"response_time_ms": r.response_time_ms,
|
||||
"status_code": r.status_code,
|
||||
"error_message": r.error_message,
|
||||
"checked_at": r.checked_at.isoformat() if r.checked_at else None,
|
||||
}
|
||||
for r in recent_results
|
||||
]
|
||||
return data
|
||||
|
||||
|
||||
@router.patch("/{monitor_id}")
|
||||
async def update_monitor(
|
||||
monitor_id: UUID,
|
||||
data: MonitorUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Update a monitor."""
|
||||
result = await db.execute(select(Monitor).where(Monitor.id == str(monitor_id)))
|
||||
monitor = result.scalar_one_or_none()
|
||||
if not monitor:
|
||||
raise HTTPException(status_code=404, detail="Monitor not found")
|
||||
|
||||
update_data = data.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(monitor, field, value)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(monitor)
|
||||
return serialize_monitor(monitor)
|
||||
|
||||
|
||||
@router.delete("/{monitor_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
async def delete_monitor(
|
||||
monitor_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Delete a monitor."""
|
||||
result = await db.execute(select(Monitor).where(Monitor.id == str(monitor_id)))
|
||||
monitor = result.scalar_one_or_none()
|
||||
if not monitor:
|
||||
raise HTTPException(status_code=404, detail="Monitor not found")
|
||||
await db.delete(monitor)
|
||||
|
||||
|
||||
@router.post("/{monitor_id}/check")
|
||||
async def trigger_check(
|
||||
monitor_id: UUID,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
api_key: str = Depends(verify_api_key),
|
||||
):
|
||||
"""Trigger a manual uptime check for this monitor."""
|
||||
from app.services.uptime import check_monitor
|
||||
|
||||
result = await db.execute(select(Monitor).where(Monitor.id == str(monitor_id)))
|
||||
monitor = result.scalar_one_or_none()
|
||||
if not monitor:
|
||||
raise HTTPException(status_code=404, detail="Monitor not found")
|
||||
|
||||
monitor_result = await check_monitor(monitor, db)
|
||||
return {
|
||||
"status": monitor_result.status,
|
||||
"response_time_ms": monitor_result.response_time_ms,
|
||||
"status_code": monitor_result.status_code,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue