All checks were successful
Run linters on applied template / Python 3.13 lint and build (push) Successful in 1m40s
Changes: - put ObservabilityMiddleware before ExceptionHandlerMiddleware to avoid repetative code - add application startup and last metrics update metrics along with CPU usage metric and threads count - move host and port to new uvicorn section at config along with new reload and forwarded_allow_ips - add request_id and remove trace_id/span_id generation if tracing is disabled - move logging logic from utils to observability - pass trace_id/span_id in HEX form
162 lines
5.8 KiB
Django/Jinja
162 lines
5.8 KiB
Django/Jinja
"""FastAPI application initialization is performed here."""
|
|
|
|
import os
|
|
from contextlib import asynccontextmanager
|
|
from typing import NoReturn
|
|
|
|
from fastapi import FastAPI, Request
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.middleware.gzip import GZipMiddleware
|
|
from fastapi.openapi.docs import get_swagger_ui_html
|
|
from fastapi.responses import JSONResponse
|
|
|
|
from {{project_slug}}.config import {{ProjectName}}Config
|
|
from {{project_slug}}.db.connection.manager import PostgresConnectionManager
|
|
from {{project_slug}}.dependencies import connection_manager_dep, logger_dep, metrics_dep
|
|
from {{project_slug}}.exceptions.mapper import ExceptionMapper
|
|
from {{project_slug}}.handlers.debug import DebugException, DebugExceptionWithParams
|
|
from {{project_slug}}.middlewares.exception_handler import ExceptionHandlerMiddleware, HandlerNotFoundError
|
|
from {{project_slug}}.middlewares.observability import ObservabilityMiddleware
|
|
from {{project_slug}}.observability.logging import configure_logging
|
|
from {{project_slug}}.observability.metrics import setup_metrics
|
|
from {{project_slug}}.observability.otel_agent import OpenTelemetryAgent
|
|
from {{project_slug}}.observability.utils import URLsMapper
|
|
|
|
from .handlers import list_of_routers
|
|
from .version import LAST_UPDATE, VERSION
|
|
|
|
|
|
def bind_routes(application: FastAPI, prefix: str, debug: bool) -> None:
|
|
"""Bind all routes to application."""
|
|
for router in list_of_routers:
|
|
if not debug:
|
|
to_remove = []
|
|
for i, route in enumerate(router.routes):
|
|
if "debug" in route.path:
|
|
to_remove.append(i)
|
|
for i in to_remove[::-1]:
|
|
del router.routes[i]
|
|
if len(router.routes) > 0:
|
|
application.include_router(router, prefix=(prefix if "/" not in {r.path for r in router.routes} else ""))
|
|
|
|
|
|
def get_app(prefix: str = "/api") -> FastAPI:
|
|
"""Create application and all dependable objects."""
|
|
|
|
if "CONFIG_PATH" not in os.environ:
|
|
raise ValueError("CONFIG_PATH environment variable is not set")
|
|
app_config: {{ProjectName}}Config = {{ProjectName}}Config.from_file(os.getenv("CONFIG_PATH"))
|
|
|
|
description = "{{project_description}}"
|
|
|
|
application = FastAPI(
|
|
title="{{project_name}}",
|
|
description=description,
|
|
docs_url=None,
|
|
openapi_url=f"{prefix}/openapi",
|
|
version=f"{VERSION} ({LAST_UPDATE})",
|
|
terms_of_service="http://swagger.io/terms/",
|
|
contact={"email": "idu@itmo.ru"},
|
|
license_info={"name": "Apache 2.0", "url": "http://www.apache.org/licenses/LICENSE-2.0.html"},
|
|
lifespan=lifespan,
|
|
)
|
|
bind_routes(application, prefix, app_config.app.debug)
|
|
|
|
@application.get(f"{prefix}/docs", include_in_schema=False)
|
|
async def custom_swagger_ui_html():
|
|
return get_swagger_ui_html(
|
|
openapi_url=application.openapi_url,
|
|
title=application.title + " - Swagger UI",
|
|
oauth2_redirect_url=app.swagger_ui_oauth2_redirect_url,
|
|
swagger_js_url="https://unpkg.com/swagger-ui-dist@5.11.7/swagger-ui-bundle.js",
|
|
swagger_css_url="https://unpkg.com/swagger-ui-dist@5.11.7/swagger-ui.css",
|
|
)
|
|
|
|
@application.exception_handler(404)
|
|
async def handle_404(request: Request, exc: Exception) -> NoReturn:
|
|
raise HandlerNotFoundError() from exc
|
|
|
|
application.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=app_config.app.cors.allow_origins,
|
|
allow_credentials=app_config.app.cors.allow_credentials,
|
|
allow_methods=app_config.app.cors.allow_methods,
|
|
allow_headers=app_config.app.cors.allow_headers,
|
|
)
|
|
application.add_middleware(GZipMiddleware, minimum_size=1000, compresslevel=5)
|
|
|
|
application.state.config = app_config
|
|
|
|
logger = configure_logging(
|
|
app_config.observability.logging,
|
|
tracing_enabled=app_config.observability.jaeger is not None,
|
|
)
|
|
metrics = setup_metrics()
|
|
|
|
exception_mapper = ExceptionMapper()
|
|
_register_exceptions(exception_mapper, debug=app_config.app.debug)
|
|
connection_manager = PostgresConnectionManager(
|
|
master=app_config.db.master,
|
|
replicas=app_config.db.replicas,
|
|
logger=logger,
|
|
application_name=f"{{project_slug}}_{VERSION}",
|
|
)
|
|
urls_mapper = URLsMapper()
|
|
urls_mapper.add_routes(application.routes)
|
|
|
|
connection_manager_dep.init_dispencer(application, connection_manager)
|
|
metrics_dep.init_dispencer(application, metrics)
|
|
logger_dep.init_dispencer(application, logger)
|
|
|
|
application.add_middleware(
|
|
ExceptionHandlerMiddleware,
|
|
debug=app_config.app.debug,
|
|
exception_mapper=exception_mapper,
|
|
urls_mapper=urls_mapper,
|
|
errors_metric=metrics.http.errors,
|
|
)
|
|
application.add_middleware(
|
|
ObservabilityMiddleware,
|
|
metrics=metrics,
|
|
urls_mapper=urls_mapper,
|
|
)
|
|
|
|
return application
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(application: FastAPI):
|
|
"""Lifespan function.
|
|
|
|
Initializes database connection in pass_services_dependencies middleware.
|
|
"""
|
|
app_config: {{ProjectName}}Config = application.state.config
|
|
logger = logger_dep.from_app(application)
|
|
|
|
await logger.ainfo("application is starting", config=app_config.to_order_dict())
|
|
|
|
otel_agent = OpenTelemetryAgent(
|
|
app_config.observability.prometheus,
|
|
app_config.observability.jaeger,
|
|
)
|
|
|
|
yield
|
|
|
|
otel_agent.shutdown()
|
|
|
|
|
|
def _register_exceptions(mapper: ExceptionMapper, debug: bool) -> None:
|
|
if debug:
|
|
mapper.register_simple(DebugException, 506, "That's how a debug exception look like")
|
|
mapper.register_func(
|
|
DebugExceptionWithParams,
|
|
lambda exc: JSONResponse(
|
|
{"error": "That's how a debug exception with params look like", "message": exc.message},
|
|
status_code=exc.status_code,
|
|
),
|
|
)
|
|
return mapper
|
|
|
|
|
|
app = get_app()
|