2025-10-06 15:27:01 +02:00
|
|
|
"""
|
|
|
|
|
Demo Operations API - Business operations for demo session management
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Path
|
|
|
|
|
import structlog
|
|
|
|
|
import jwt
|
2025-11-30 16:29:38 +01:00
|
|
|
from datetime import datetime, timezone
|
2025-10-06 15:27:01 +02:00
|
|
|
|
|
|
|
|
from app.api.schemas import DemoSessionResponse, DemoSessionStats
|
|
|
|
|
from app.services import DemoSessionManager, DemoCleanupService
|
2025-10-15 16:12:49 +02:00
|
|
|
from app.core import get_db, get_redis, DemoRedisWrapper
|
2025-10-06 15:27:01 +02:00
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
from shared.routing import RouteBuilder
|
|
|
|
|
|
|
|
|
|
router = APIRouter(tags=["demo-operations"])
|
|
|
|
|
logger = structlog.get_logger()
|
|
|
|
|
|
|
|
|
|
route_builder = RouteBuilder('demo')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
route_builder.build_resource_action_route("sessions", "session_id", "extend", include_tenant_prefix=False),
|
|
|
|
|
response_model=DemoSessionResponse
|
|
|
|
|
)
|
|
|
|
|
async def extend_demo_session(
|
|
|
|
|
session_id: str = Path(...),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-10-15 16:12:49 +02:00
|
|
|
redis: DemoRedisWrapper = Depends(get_redis)
|
2025-10-06 15:27:01 +02:00
|
|
|
):
|
|
|
|
|
"""Extend demo session expiration (BUSINESS OPERATION)"""
|
|
|
|
|
try:
|
|
|
|
|
session_manager = DemoSessionManager(db, redis)
|
|
|
|
|
session = await session_manager.extend_session(session_id)
|
|
|
|
|
|
|
|
|
|
session_token = jwt.encode(
|
|
|
|
|
{
|
|
|
|
|
"session_id": session.session_id,
|
|
|
|
|
"virtual_tenant_id": str(session.virtual_tenant_id),
|
|
|
|
|
"demo_account_type": session.demo_account_type,
|
|
|
|
|
"exp": session.expires_at.timestamp()
|
|
|
|
|
},
|
|
|
|
|
"demo-secret-key",
|
|
|
|
|
algorithm="HS256"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"session_id": session.session_id,
|
|
|
|
|
"virtual_tenant_id": str(session.virtual_tenant_id),
|
|
|
|
|
"demo_account_type": session.demo_account_type,
|
|
|
|
|
"status": session.status.value,
|
|
|
|
|
"created_at": session.created_at,
|
|
|
|
|
"expires_at": session.expires_at,
|
2025-10-07 07:15:07 +02:00
|
|
|
"demo_config": session.session_metadata.get("demo_config", {}),
|
2025-10-06 15:27:01 +02:00
|
|
|
"session_token": session_token
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except ValueError as e:
|
|
|
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to extend session", error=str(e))
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
route_builder.build_base_route("stats", include_tenant_prefix=False),
|
|
|
|
|
response_model=DemoSessionStats
|
|
|
|
|
)
|
|
|
|
|
async def get_demo_stats(
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-10-15 16:12:49 +02:00
|
|
|
redis: DemoRedisWrapper = Depends(get_redis)
|
2025-10-06 15:27:01 +02:00
|
|
|
):
|
|
|
|
|
"""Get demo session statistics (BUSINESS OPERATION)"""
|
|
|
|
|
session_manager = DemoSessionManager(db, redis)
|
|
|
|
|
stats = await session_manager.get_session_stats()
|
|
|
|
|
return stats
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
route_builder.build_operations_route("cleanup", include_tenant_prefix=False),
|
|
|
|
|
response_model=dict
|
|
|
|
|
)
|
|
|
|
|
async def run_cleanup(
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
2025-10-15 16:12:49 +02:00
|
|
|
redis: DemoRedisWrapper = Depends(get_redis)
|
2025-10-06 15:27:01 +02:00
|
|
|
):
|
2025-11-30 16:29:38 +01:00
|
|
|
"""
|
|
|
|
|
Trigger session cleanup via background worker (async via Redis queue)
|
|
|
|
|
|
|
|
|
|
Returns immediately after enqueuing work - does not block
|
|
|
|
|
"""
|
|
|
|
|
from datetime import timedelta
|
|
|
|
|
from sqlalchemy import select
|
|
|
|
|
from app.models.demo_session import DemoSession, DemoSessionStatus
|
|
|
|
|
import uuid
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
logger.info("Starting demo session cleanup enqueue")
|
|
|
|
|
|
|
|
|
|
now = datetime.now(timezone.utc)
|
|
|
|
|
stuck_threshold = now - timedelta(minutes=5)
|
|
|
|
|
|
|
|
|
|
# Find expired sessions
|
|
|
|
|
result = await db.execute(
|
|
|
|
|
select(DemoSession).where(
|
|
|
|
|
DemoSession.status.in_([
|
|
|
|
|
DemoSessionStatus.PENDING,
|
|
|
|
|
DemoSessionStatus.READY,
|
|
|
|
|
DemoSessionStatus.PARTIAL,
|
|
|
|
|
DemoSessionStatus.FAILED,
|
|
|
|
|
DemoSessionStatus.ACTIVE
|
|
|
|
|
]),
|
|
|
|
|
DemoSession.expires_at < now
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
expired_sessions = result.scalars().all()
|
|
|
|
|
|
|
|
|
|
# Find stuck sessions
|
|
|
|
|
stuck_result = await db.execute(
|
|
|
|
|
select(DemoSession).where(
|
|
|
|
|
DemoSession.status == DemoSessionStatus.PENDING,
|
|
|
|
|
DemoSession.created_at < stuck_threshold
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
stuck_sessions = stuck_result.scalars().all()
|
|
|
|
|
|
|
|
|
|
all_sessions = list(expired_sessions) + list(stuck_sessions)
|
|
|
|
|
|
|
|
|
|
if not all_sessions:
|
|
|
|
|
return {
|
|
|
|
|
"status": "no_sessions",
|
|
|
|
|
"message": "No sessions to cleanup",
|
|
|
|
|
"total_expired": 0,
|
|
|
|
|
"total_stuck": 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Create cleanup job
|
|
|
|
|
job_id = str(uuid.uuid4())
|
|
|
|
|
session_ids = [s.session_id for s in all_sessions]
|
|
|
|
|
|
|
|
|
|
job_data = {
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
"session_ids": session_ids,
|
|
|
|
|
"created_at": now.isoformat(),
|
|
|
|
|
"retry_count": 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Enqueue job
|
|
|
|
|
client = await redis.get_client()
|
|
|
|
|
await client.lpush("cleanup:queue", json.dumps(job_data))
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
"Cleanup job enqueued",
|
|
|
|
|
job_id=job_id,
|
|
|
|
|
session_count=len(session_ids),
|
|
|
|
|
expired_count=len(expired_sessions),
|
|
|
|
|
stuck_count=len(stuck_sessions)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"status": "enqueued",
|
|
|
|
|
"job_id": job_id,
|
|
|
|
|
"session_count": len(session_ids),
|
|
|
|
|
"total_expired": len(expired_sessions),
|
|
|
|
|
"total_stuck": len(stuck_sessions),
|
|
|
|
|
"message": f"Cleanup job enqueued for {len(session_ids)} sessions"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get(
|
|
|
|
|
route_builder.build_operations_route("cleanup/{job_id}", include_tenant_prefix=False),
|
|
|
|
|
response_model=dict
|
|
|
|
|
)
|
|
|
|
|
async def get_cleanup_status(
|
|
|
|
|
job_id: str,
|
|
|
|
|
redis: DemoRedisWrapper = Depends(get_redis)
|
|
|
|
|
):
|
|
|
|
|
"""Get status of cleanup job"""
|
|
|
|
|
import json
|
|
|
|
|
|
|
|
|
|
client = await redis.get_client()
|
|
|
|
|
status_key = f"cleanup:job:{job_id}:status"
|
|
|
|
|
|
|
|
|
|
status_data = await client.get(status_key)
|
|
|
|
|
if not status_data:
|
|
|
|
|
return {
|
|
|
|
|
"status": "not_found",
|
|
|
|
|
"message": "Job not found or expired (jobs expire after 1 hour)"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return json.loads(status_data)
|
2025-11-27 15:52:40 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post(
|
|
|
|
|
"/demo/sessions/{session_id}/seed-alerts",
|
|
|
|
|
response_model=dict
|
|
|
|
|
)
|
|
|
|
|
async def seed_demo_alerts(
|
|
|
|
|
session_id: str = Path(...),
|
|
|
|
|
db: AsyncSession = Depends(get_db),
|
|
|
|
|
redis: DemoRedisWrapper = Depends(get_redis)
|
|
|
|
|
):
|
|
|
|
|
"""Seed enriched demo alerts for a demo session (DEMO OPERATION)"""
|
|
|
|
|
try:
|
|
|
|
|
import subprocess
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
# Get session to validate and get tenant_id
|
|
|
|
|
session_manager = DemoSessionManager(db, redis)
|
|
|
|
|
session = await session_manager.get_session(session_id)
|
|
|
|
|
|
|
|
|
|
if not session:
|
|
|
|
|
raise HTTPException(status_code=404, detail="Demo session not found")
|
|
|
|
|
|
|
|
|
|
# Set environment variables for seeding script
|
|
|
|
|
env = os.environ.copy()
|
|
|
|
|
env['DEMO_TENANT_ID'] = str(session.virtual_tenant_id)
|
|
|
|
|
|
|
|
|
|
# Determine script path based on environment
|
|
|
|
|
# In container: /app/scripts/seed_enriched_alert_demo.py
|
|
|
|
|
# In development: services/demo_session/scripts/seed_enriched_alert_demo.py
|
|
|
|
|
script_path = '/app/scripts/seed_enriched_alert_demo.py' if os.path.exists('/app/scripts') else 'services/demo_session/scripts/seed_enriched_alert_demo.py'
|
|
|
|
|
|
|
|
|
|
# Run the seeding script
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
['python3', script_path],
|
|
|
|
|
env=env,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
timeout=30
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
if result.returncode != 0:
|
|
|
|
|
logger.error("Alert seeding failed",
|
|
|
|
|
stdout=result.stdout,
|
|
|
|
|
stderr=result.stderr)
|
|
|
|
|
raise HTTPException(status_code=500, detail=f"Alert seeding failed: {result.stderr}")
|
|
|
|
|
|
|
|
|
|
logger.info("Demo alerts seeded successfully", session_id=session_id)
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
"status": "success",
|
|
|
|
|
"session_id": session_id,
|
|
|
|
|
"tenant_id": str(session.virtual_tenant_id),
|
|
|
|
|
"alerts_seeded": 5,
|
|
|
|
|
"message": "Demo alerts published and will be enriched automatically"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
except subprocess.TimeoutExpired:
|
|
|
|
|
raise HTTPException(status_code=504, detail="Alert seeding timeout")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error("Failed to seed alerts", error=str(e), session_id=session_id)
|
|
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|