Files
MobilityOps/backend/app/main.py
T

188 lines
5.5 KiB
Python

import logging
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from sqlalchemy import text
from starlette.middleware.sessions import SessionMiddleware
from app.api.routers import (
audit,
auth,
bookings,
customers,
dashboard,
data_quality,
demo,
integration_status,
integrations,
knowledge,
mcp_integrations,
observability,
privacy,
search,
users,
vehicles,
workflows,
)
from app.api.routers.auth import bootstrap_initial_admin
from app.core.config import PRODUCT_NAME, get_settings
from app.core.db import SessionLocal
from app.core.errors import AppError, error_body
from app.core.observability import (
DATABASE_READY,
configure_logging,
correlation_id_context,
correlation_id_for,
request_finished,
request_started,
)
from app.services.dispatcher import start_background_dispatcher, stop_background_dispatcher
settings = get_settings()
configure_logging(settings.log_level)
request_logger = logging.getLogger("mobilityops.request")
@asynccontextmanager
async def lifespan(_app: FastAPI):
with SessionLocal() as db:
bootstrap_initial_admin(db)
start_background_dispatcher()
yield
stop_background_dispatcher()
app = FastAPI(title=f"{PRODUCT_NAME} API", version="0.1.0", lifespan=lifespan)
app.add_middleware(
SessionMiddleware,
secret_key=settings.app_secret,
session_cookie="mobilityops_oidc_state",
max_age=600,
same_site="lax",
https_only=settings.session_cookie_secure,
)
@app.middleware("http")
async def request_observability(request: Request, call_next):
correlation_id = correlation_id_for(request)
request.state.correlation_id = correlation_id
token = correlation_id_context.set(correlation_id)
started_at = request_started()
status_code = 500
try:
response = await call_next(request)
status_code = response.status_code
response.headers["X-Correlation-Id"] = correlation_id
return response
finally:
duration = request_finished(request, status_code, started_at)
request_logger.info(
"request_completed",
extra={
"method": request.method,
"path": request.url.path,
"status_code": status_code,
"duration_ms": round(duration * 1000, 2),
"client_ip": request.client.host if request.client else None,
},
)
correlation_id_context.reset(token)
app.add_middleware(
CORSMiddleware,
allow_origins=[o.strip() for o in settings.cors_allow_origins.split(",")],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
@app.exception_handler(AppError)
def handle_app_error(request: Request, exc: AppError) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content=error_body(
exc.code,
exc.message,
request.state.correlation_id,
exc.details,
),
)
@app.exception_handler(HTTPException)
def handle_http_exception(request: Request, exc: HTTPException) -> JSONResponse:
return JSONResponse(
status_code=exc.status_code,
content=error_body(
code=str(exc.status_code),
message=str(exc.detail),
correlation_id=getattr(request.state, "correlation_id", str(uuid.uuid4())),
details={},
),
)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok", "service": "mobilityops-api"}
@app.get("/health/live")
def liveness() -> dict[str, str]:
"""Process liveness only; external dependencies deliberately do not affect it."""
return {"status": "ok", "service": "mobilityops-api"}
@app.get("/health/ready")
def readiness() -> JSONResponse:
"""Traffic readiness: the API is useful only while its canonical database responds."""
try:
with SessionLocal() as db:
db.execute(text("SELECT 1"))
except Exception: # noqa: BLE001 -- readiness must convert infrastructure errors to 503
DATABASE_READY.set(0)
DATABASE_READY.set(1)
return JSONResponse(
status_code=503,
content={"status": "not_ready", "service": "mobilityops-api", "database": "down"},
)
return JSONResponse(content={"status": "ready", "service": "mobilityops-api", "database": "up"})
@app.get("/api/v1/system/status")
def system_status() -> dict[str, object]:
return {
"service": "mobilityops-api",
"environment": settings.mobilityops_env,
"demo_mode": settings.mobilityops_demo_mode,
"knowledge_provider": settings.knowledge_provider,
"oidc_enabled": auth.oidc_status().enabled,
"oidc_provider_name": auth.oidc_status().provider_name,
}
app.include_router(demo.router)
app.include_router(auth.router)
app.include_router(dashboard.router)
app.include_router(vehicles.router)
app.include_router(bookings.router)
app.include_router(customers.router)
app.include_router(audit.router)
app.include_router(data_quality.router)
app.include_router(workflows.router)
app.include_router(integrations.router)
app.include_router(knowledge.router)
app.include_router(mcp_integrations.router)
app.include_router(search.router)
app.include_router(integration_status.router)
app.include_router(users.router)
app.include_router(observability.router)
app.include_router(privacy.router)