-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathmain.py
More file actions
220 lines (175 loc) · 8.15 KB
/
main.py
File metadata and controls
220 lines (175 loc) · 8.15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
"""KillrVideo monolithic FastAPI application.
This file assembles **all** routers from the various domain services into a
single FastAPI instance. It is ideal for local development and testing or for
deployments that prefer a unified 'monolith' instead of separate micro-services.
If you intend to run the new micro-service containers, use their dedicated
entry-points (e.g. ``app.main_user:service_app``) instead.
"""
import logging
from http import HTTPStatus
from fastapi import FastAPI, HTTPException, Request, status, APIRouter
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from fastapi.middleware.cors import CORSMiddleware
from app.core.config import settings
from app.db.astra_client import init_astra_db
from app.models.common import ProblemDetail
from app.api.v1.endpoints import (
account_management,
video_catalog,
search_catalog,
comments_ratings,
recommendations_feed,
reco_internal,
flags,
moderation,
user_activity,
)
# --------------------------------------------------------------
# Observability must be configured as early as possible so that
# instrumentation picks up all subsequent application events.
# --------------------------------------------------------------
from app.utils.observability import configure_observability # noqa: E402
logger = logging.getLogger(__name__)
app = FastAPI(title="KillrVideo v2 - Monolith Backend", version=settings.APP_VERSION)
# ---------------------------------------------------------------------------
# CORS middleware
#
# See: https://fastapi.tiangolo.com/tutorial/cors/
# ---------------------------------------------------------------------------
logger.debug(f"CORS origins: {settings.parsed_cors_origins}")
app.add_middleware(
CORSMiddleware,
allow_origins=settings.parsed_cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# API router for v1
api_router_v1 = APIRouter(prefix=settings.API_V1_STR)
api_router_v1.include_router(account_management.router)
api_router_v1.include_router(video_catalog.router)
api_router_v1.include_router(search_catalog.router)
api_router_v1.include_router(comments_ratings.router)
api_router_v1.include_router(recommendations_feed.router)
api_router_v1.include_router(reco_internal.router)
api_router_v1.include_router(flags.router)
api_router_v1.include_router(moderation.router)
api_router_v1.include_router(user_activity.router)
app.include_router(api_router_v1)
# ---------------------------------------------------------------------------
# Initialise observability (metrics / tracing / log shipping) if enabled.
# This must run *after* the FastAPI instance is created so that instrumentors
# can attach middleware and routes.
# ---------------------------------------------------------------------------
configure_observability(app)
# Attempt to import httpx & httpcore connection error classes for fine-grained
# exception handling. They may not be present in some lightweight test
# environments – fall back gracefully.
try:
import httpx # type: ignore
HttpxConnectError = httpx.ConnectError
except ModuleNotFoundError: # pragma: no cover
httpx = None # type: ignore
HttpxConnectError = None # type: ignore
try:
from httpcore import ConnectError as HttpcoreConnectError # type: ignore
except ModuleNotFoundError: # pragma: no cover
HttpcoreConnectError = None # type: ignore
@app.on_event("startup")
async def startup_event():
await init_astra_db()
# ------------------------------------------------------------------
# Debug – dump env vars and settings so we can verify flags like
# INLINE_METADATA_DISABLED / ENABLE_BACKGROUND_PROCESSING are picked up.
# Sensitive values containing keywords are masked to avoid leaking
# secrets in logs.
# ------------------------------------------------------------------
def _mask(value: str) -> str: # noqa: D401
SENSITIVE_KEYWORDS = {"KEY", "TOKEN", "SECRET", "PASSWORD"}
return "***" if any(k in value.upper() for k in SENSITIVE_KEYWORDS) else value
import os # noqa: E402
import json # noqa: E402
env_dump = {k: _mask(v) for k, v in os.environ.items()}
logger.debug("Environment variables at startup: %s", json.dumps(env_dump, indent=2))
settings_dump = settings.model_dump()
logger.debug(
"Pydantic settings at startup: %s", json.dumps(settings_dump, indent=2)
)
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content=ProblemDetail(
type="about:blank",
title=HTTPStatus(exc.status_code).phrase,
status=exc.status_code,
detail=exc.detail,
instance=str(request.url),
).model_dump(exclude_none=True),
)
async def _problem_response(request: Request, status_code: int, detail: str):
"""Helper to build RFC7807-style JSON error bodies."""
return JSONResponse(
status_code=status_code,
content=ProblemDetail(
type="about:blank",
title=HTTPStatus(status_code).phrase,
status=status_code,
detail=detail,
instance=str(request.url),
).model_dump(exclude_none=True),
)
if HttpxConnectError is not None:
@app.exception_handler(HttpxConnectError) # type: ignore[arg-type]
async def httpx_connect_error_handler(request: Request, exc: Exception): # noqa: D401
logger.warning("AstraDB connectivity problem: %s", exc)
# Provide more context for troubleshooting: log the failed request (if
# available) and the full stack trace at DEBUG level.
if hasattr(exc, "request") and exc.request is not None: # pragma: no cover
logger.debug(
"Failed AstraDB request – %s %s", exc.request.method, exc.request.url
)
logger.debug("Detailed stack trace for connectivity issue:", exc_info=True)
return await _problem_response(
request,
status.HTTP_503_SERVICE_UNAVAILABLE,
"Unable to reach data store. Please try again later.",
)
if HttpcoreConnectError is not None:
@app.exception_handler(HttpcoreConnectError) # type: ignore[arg-type]
async def httpcore_connect_error_handler(request: Request, exc: Exception): # noqa: D401
logger.warning("AstraDB connectivity problem: %s", exc)
# httpcore.ConnectError does not expose the original request object, but we
# still output the stack trace to aid debugging.
logger.debug("Detailed stack trace for connectivity issue:", exc_info=True)
return await _problem_response(
request,
status.HTTP_503_SERVICE_UNAVAILABLE,
"Unable to reach data store. Please try again later.",
)
@app.exception_handler(Exception)
async def generic_exception_handler(request: Request, exc: Exception):
logger.error("Unhandled exception: %s", exc, exc_info=True)
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content=ProblemDetail(
type="about:blank",
title=HTTPStatus.INTERNAL_SERVER_ERROR.phrase,
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="An unexpected internal server error occurred.",
instance=str(request.url),
).model_dump(exclude_none=True),
)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
"""Log validation errors and return a default 422 response."""
logger.error("Request validation failed: %s", exc.errors())
# Re-raise the original exception to let FastAPI handle the default response
# This keeps the response format consistent with FastAPI's built-in validation.
# For a custom response, you would build and return a JSONResponse here.
from fastapi.exception_handlers import request_validation_exception_handler
return await request_validation_exception_handler(request, exc)
@app.get("/", summary="Health check")
async def root():
return {"message": f"Welcome to {settings.PROJECT_NAME}!"}