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())},
)