ee348643f8
services/correlation.py was moved to services/shared/correlation.py, services/event_bus.py to services/instance/event_bus.py, and services/health_monitor.py to services/instance/health_monitor.py but main.py was still importing from the old flat paths. Updated main.py to import from the new subpackage paths via __init__.py re-exports. Quality gates: py_compile passed, ruff passed.
179 lines
5.4 KiB
Python
179 lines
5.4 KiB
Python
import logging
|
||
import os
|
||
|
||
from fastapi import FastAPI, Request
|
||
from fastapi.exceptions import RequestValidationError
|
||
from fastapi.middleware.cors import CORSMiddleware
|
||
from fastapi.responses import JSONResponse
|
||
from fastapi.staticfiles import StaticFiles
|
||
|
||
from src.api.config import config_profiles_router, user_config_router
|
||
from src.api.project import git_repositories_router, projects_router
|
||
from src.api.system import (
|
||
dashboard_router,
|
||
events_router,
|
||
health_router,
|
||
instance_proxy_router,
|
||
notifications_router,
|
||
terminal_router,
|
||
)
|
||
from src.api.tool import (
|
||
sessions_router,
|
||
tool_definitions_router,
|
||
tool_instances_router,
|
||
tool_types_router,
|
||
)
|
||
from src.api.user import auth_router, ssh_keys_router, users_router
|
||
from src.api.workspace import (
|
||
all_workspaces_router,
|
||
workspace_files_router,
|
||
workspace_git_router,
|
||
workspace_instances_router,
|
||
workspaces_router,
|
||
)
|
||
from src.config import Settings
|
||
from src.models import Notification # noqa: F401 – Alembic model discovery
|
||
from src.models import TerminalSessionModel # noqa: F401 – Alembic model discovery
|
||
from src.database import init_database
|
||
from src.logging_config import (
|
||
ExceptionLoggingMiddleware,
|
||
RequestLoggingMiddleware,
|
||
configure_logging,
|
||
)
|
||
from src.seeds.builtin_tool_types import seed_builtin_tool_types
|
||
from src.services.instance import InstanceEventBus, HealthMonitor
|
||
from src.services.shared import CorrelationIdMiddleware
|
||
|
||
# Configure logging early
|
||
log_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
||
configure_logging(level=getattr(logging, log_level, logging.INFO))
|
||
|
||
logger = logging.getLogger(__name__)
|
||
settings = Settings()
|
||
app = FastAPI(title="Headquarter API")
|
||
|
||
# Configure CORS - must be before other middleware
|
||
# Build allowed origins list including web and api domains
|
||
cors_origins = [settings.web_base_url]
|
||
if settings.api_base_url != settings.web_base_url:
|
||
cors_origins.append(settings.api_base_url)
|
||
logger.info("CORS configured with origins: %s", cors_origins)
|
||
|
||
app.add_middleware(
|
||
CORSMiddleware,
|
||
allow_origins=cors_origins,
|
||
allow_credentials=True,
|
||
allow_methods=["*"],
|
||
allow_headers=["*"],
|
||
)
|
||
|
||
app.add_middleware(CorrelationIdMiddleware)
|
||
app.add_middleware(RequestLoggingMiddleware)
|
||
app.add_middleware(ExceptionLoggingMiddleware)
|
||
|
||
|
||
def _sanitize_validation_errors(errors):
|
||
"""Convert validation errors to JSON-safe format."""
|
||
sanitized = []
|
||
for error in errors:
|
||
safe_error = {
|
||
"type": error.get("type"),
|
||
"loc": error.get("loc"),
|
||
"msg": error.get("msg"),
|
||
"input": str(error.get("input"))
|
||
if error.get("input") is not None
|
||
else None,
|
||
}
|
||
# Convert ctx to safe format
|
||
ctx = error.get("ctx")
|
||
if ctx:
|
||
safe_ctx = {}
|
||
for key, value in ctx.items():
|
||
if isinstance(value, Exception):
|
||
safe_ctx[key] = str(value)
|
||
elif isinstance(value, (str, int, float, bool, type(None))):
|
||
safe_ctx[key] = value
|
||
else:
|
||
safe_ctx[key] = str(value)
|
||
safe_error["ctx"] = safe_ctx
|
||
sanitized.append(safe_error)
|
||
return sanitized
|
||
|
||
|
||
@app.exception_handler(RequestValidationError)
|
||
async def validation_exception_handler(request: Request, exc: RequestValidationError):
|
||
"""Log validation errors and return detailed response."""
|
||
errors = exc.errors()
|
||
logger.warning(
|
||
"Validation error for %s %s: %s",
|
||
request.method,
|
||
request.url.path,
|
||
errors,
|
||
)
|
||
safe_errors = _sanitize_validation_errors(errors)
|
||
return JSONResponse(
|
||
status_code=422,
|
||
content={"detail": safe_errors},
|
||
)
|
||
|
||
|
||
# Global services
|
||
_event_bus = InstanceEventBus()
|
||
_health_monitor = HealthMonitor(_event_bus)
|
||
|
||
|
||
@app.on_event("startup")
|
||
async def on_startup():
|
||
logger.info("Starting up Headquarter API...")
|
||
|
||
# Initialize database (run migrations)
|
||
db_ready = await init_database()
|
||
if not db_ready:
|
||
logger.error("Database initialization failed. Shutting down.")
|
||
import sys
|
||
|
||
sys.exit(1)
|
||
|
||
# Start background health monitor
|
||
_health_monitor.start()
|
||
logger.info("Health monitor started")
|
||
|
||
# Seed built-in tool types
|
||
await seed_builtin_tool_types()
|
||
logger.info("Built-in tool types seeded")
|
||
|
||
logger.info("Startup complete.")
|
||
|
||
|
||
@app.on_event("shutdown")
|
||
async def on_shutdown():
|
||
logger.info("Shutting down Headquarter API...")
|
||
_health_monitor.stop()
|
||
logger.info("Health monitor stopped")
|
||
logger.info("Shutdown complete.")
|
||
|
||
|
||
app.include_router(health_router)
|
||
app.include_router(auth_router)
|
||
app.include_router(dashboard_router)
|
||
app.include_router(projects_router)
|
||
app.include_router(users_router)
|
||
app.include_router(ssh_keys_router)
|
||
app.include_router(git_repositories_router)
|
||
app.include_router(user_config_router)
|
||
app.include_router(tool_types_router)
|
||
app.include_router(tool_definitions_router)
|
||
app.include_router(config_profiles_router)
|
||
app.include_router(tool_instances_router)
|
||
app.include_router(sessions_router)
|
||
app.include_router(instance_proxy_router)
|
||
app.include_router(terminal_router)
|
||
app.include_router(events_router)
|
||
app.include_router(notifications_router)
|
||
app.include_router(all_workspaces_router)
|
||
app.include_router(workspaces_router)
|
||
app.include_router(workspace_files_router)
|
||
app.include_router(workspace_git_router)
|
||
app.include_router(workspace_instances_router)
|
||
app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")
|