jbi/app.py (106 lines of code) (raw):

""" Core FastAPI app (setup, middleware) """ import logging from contextlib import asynccontextmanager from pathlib import Path from typing import Any, AsyncGenerator import sentry_sdk from dockerflow import checks from dockerflow.fastapi import router as dockerflow_router from dockerflow.fastapi.middleware import ( MozlogRequestSummaryLogger, RequestIdMiddleware, ) from dockerflow.version import get_version from fastapi import FastAPI, Request, Response, status from fastapi.encoders import jsonable_encoder from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles import jbi import jbi.queue from jbi.configuration import get_actions from jbi.environment import get_settings from jbi.log import CONFIG from jbi.router import router SRC_DIR = Path(__file__).parent APP_DIR = Path(__file__).parents[1] ACTIONS = get_actions() settings = get_settings() version_info: dict[str, str] = get_version(APP_DIR) VERSION: str = version_info["version"] logging.config.dictConfig(CONFIG) logger = logging.getLogger(__name__) def traces_sampler(sampling_context: dict[str, Any]) -> float: """Function to dynamically set Sentry sampling rates""" request_path = sampling_context.get("asgi_scope", {}).get("path") if request_path == "/__lbheartbeat__": # Drop all __lbheartbeat__ requests return 0 return settings.sentry_traces_sample_rate sentry_sdk.init( dsn=str(settings.sentry_dsn) if settings.sentry_dsn else None, traces_sampler=traces_sampler, release=VERSION, ) # https://github.com/tiangolo/fastapi/discussions/9241 @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: jira_service = jbi.jira.service.get_service() bugzilla_service = jbi.bugzilla.service.get_service() queue = jbi.queue.get_dl_queue() checks.register(bugzilla_service.check_bugzilla_connection, name="bugzilla.up") checks.register( bugzilla_service.check_bugzilla_webhooks, name="bugzilla.all_webhooks_enabled", ) checks.register(jira_service.check_jira_connection, name="jira.up") checks.register_partial( jira_service.check_jira_all_projects_are_visible, ACTIONS, name="jira.all_projects_are_visible", ) checks.register_partial( jira_service.check_jira_all_projects_have_permissions, ACTIONS, name="jira.all_projects_have_permissions", ) checks.register_partial( jira_service.check_jira_all_project_custom_components_exist, ACTIONS, name="jira.all_project_custom_components_exist", ) checks.register_partial( jira_service.check_jira_all_project_issue_types_exist, ACTIONS, name="jira.all_project_issue_types_exist", ) checks.register(jira_service.check_jira_pandoc_install, name="jira.pandoc_install") checks.register(queue.check_writable, name="queue.writable") checks.register(queue.check_readable, name="queue.readable") yield app = FastAPI( title="Jira Bugzilla Integration (JBI)", description="Platform providing synchronization of Bugzilla bugs to Jira issues.", version=VERSION, debug=settings.app_debug, lifespan=lifespan, ) app.state.APP_DIR = APP_DIR app.state.DOCKERFLOW_HEARTBEAT_FAILED_STATUS_CODE = 503 app.state.DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True app.include_router(router) app.include_router(dockerflow_router) app.add_middleware(RequestIdMiddleware) app.add_middleware(MozlogRequestSummaryLogger) app.mount("/static", StaticFiles(directory=SRC_DIR / "static"), name="static") @app.exception_handler(RequestValidationError) async def validation_exception_handler( request: Request, exc: RequestValidationError ) -> Response: """ Override the default exception handler for validation errors in order to log some information about malformed requests. """ logger.error( "invalid incoming request: %s", exc, extra={ "errors": exc.errors(), }, ) return JSONResponse( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, content={"detail": jsonable_encoder(exc.errors())}, )