Skip to content

Commit 8330775

Browse files
authored
Break up src/app/api.py module (#174)
The goal of this commit is to move some of the code currently present in `src/app/api.py` into more appropriate modules. The result is that the three modules involved have more obvious "jobs": - `log.py`: logging config and any helpers - `router.py`: all route handling logic - `main.py`: building the FastAPI app (middleware, adding static files, registering the router)
1 parent e9c29c2 commit 8330775

File tree

12 files changed

+335
-336
lines changed

12 files changed

+335
-336
lines changed

asgi.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
if __name__ == "__main__":
1010
server = uvicorn.Server(
1111
uvicorn.Config(
12-
"src.app.api:app",
12+
"src.app.main:app",
1313
host=settings.host,
1414
port=settings.port,
1515
reload=settings.app_reload,

src/app/api.py

Lines changed: 0 additions & 143 deletions
This file was deleted.

src/app/log.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
import logging
55
import logging.config
66
import sys
7+
import time
8+
from datetime import datetime
9+
from typing import Dict
10+
11+
from fastapi import Request
712

813
from src.app.environment import get_settings
914

@@ -37,3 +42,23 @@
3742
"uvicorn.access": {"handlers": ["null"], "propagate": False},
3843
},
3944
}
45+
46+
47+
def format_request_summary_fields(
48+
request: Request, request_time: float, status_code: int
49+
) -> Dict:
50+
"""Prepare Fields for Mozlog request summary"""
51+
52+
current_time = time.time()
53+
fields = {
54+
"agent": request.headers.get("User-Agent"),
55+
"path": request.url.path,
56+
"method": request.method,
57+
"lang": request.headers.get("Accept-Language"),
58+
"querystring": dict(request.query_params),
59+
"errno": 0,
60+
"t": int((current_time - request_time) * 1000.0),
61+
"time": datetime.fromtimestamp(current_time).isoformat(),
62+
"status_code": status_code,
63+
}
64+
return fields

src/app/main.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""
2+
Core FastAPI app (setup, middleware)
3+
"""
4+
import logging
5+
import time
6+
from pathlib import Path
7+
8+
import sentry_sdk
9+
from fastapi import FastAPI, Request
10+
from fastapi.staticfiles import StaticFiles
11+
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
12+
13+
from src.app.environment import get_settings
14+
from src.app.log import format_request_summary_fields
15+
from src.app.router import router
16+
17+
SRC_DIR = Path(__file__).parents[1]
18+
19+
settings = get_settings()
20+
21+
22+
app = FastAPI(
23+
title="Jira Bugzilla Integration (JBI)",
24+
description="JBI v2 Platform",
25+
version="3.1.0",
26+
debug=settings.app_debug,
27+
)
28+
29+
app.include_router(router)
30+
app.mount("/static", StaticFiles(directory=SRC_DIR / "static"), name="static")
31+
32+
sentry_sdk.init( # pylint: disable=abstract-class-instantiated # noqa: E0110
33+
dsn=settings.sentry_dsn
34+
)
35+
app.add_middleware(SentryAsgiMiddleware)
36+
37+
38+
@app.middleware("http")
39+
async def request_summary(request: Request, call_next):
40+
"""Middleware to log request info"""
41+
summary_logger = logging.getLogger("request.summary")
42+
request_time = time.time()
43+
try:
44+
response = await call_next(request)
45+
log_fields = format_request_summary_fields(
46+
request, request_time, status_code=response.status_code
47+
)
48+
summary_logger.info("", extra=log_fields)
49+
return response
50+
except Exception as exc:
51+
log_fields = format_request_summary_fields(
52+
request, request_time, status_code=500
53+
)
54+
summary_logger.info(exc, extra=log_fields)
55+
raise

src/app/monitor.py

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/app/router.py

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
"""
2+
Core FastAPI app (setup, middleware)
3+
"""
4+
from pathlib import Path
5+
from typing import Dict, List, Optional
6+
7+
from fastapi import APIRouter, Body, Depends, Request, Response
8+
from fastapi.encoders import jsonable_encoder
9+
from fastapi.responses import HTMLResponse
10+
from fastapi.templating import Jinja2Templates
11+
12+
from src.app.configuration import get_actions
13+
from src.app.environment import Settings, get_settings, get_version
14+
from src.jbi.bugzilla import BugzillaWebhookRequest
15+
from src.jbi.models import Actions
16+
from src.jbi.runner import IgnoreInvalidRequestError, execute_action
17+
from src.jbi.services import jbi_service_health_map, jira_visible_projects
18+
19+
router = APIRouter()
20+
21+
22+
@router.get("/", include_in_schema=False)
23+
def root(request: Request, settings: Settings = Depends(get_settings)):
24+
"""Expose key configuration"""
25+
return {
26+
"title": request.app.title,
27+
"description": request.app.description,
28+
"version": request.app.version,
29+
"documentation": request.app.docs_url,
30+
"configuration": {
31+
"jira_base_url": settings.jira_base_url,
32+
"bugzilla_base_url": settings.bugzilla_base_url,
33+
},
34+
}
35+
36+
37+
@router.get("/__heartbeat__")
38+
@router.head("/__heartbeat__")
39+
def heartbeat(response: Response, actions: Actions = Depends(get_actions)):
40+
"""Return status of backing services, as required by Dockerflow."""
41+
health_map = jbi_service_health_map(actions)
42+
health_checks = []
43+
for health in health_map.values():
44+
health_checks.extend(health.values())
45+
if not all(health_checks):
46+
response.status_code = 503
47+
return health_map
48+
49+
50+
@router.get("/__lbheartbeat__")
51+
@router.head("/__lbheartbeat__")
52+
def lbheartbeat():
53+
"""Dockerflow API for lbheartbeat: HEAD"""
54+
return {"status": "OK"}
55+
56+
57+
@router.get("/__version__")
58+
def version(version_json=Depends(get_version)):
59+
"""Return version.json, as required by Dockerflow."""
60+
return version_json
61+
62+
63+
@router.post("/bugzilla_webhook")
64+
def bugzilla_webhook(
65+
request: BugzillaWebhookRequest = Body(..., embed=False),
66+
actions: Actions = Depends(get_actions),
67+
settings: Settings = Depends(get_settings),
68+
):
69+
"""API endpoint that Bugzilla Webhook Events request"""
70+
try:
71+
result = execute_action(request, actions, settings)
72+
return result
73+
except IgnoreInvalidRequestError as exception:
74+
return {"error": str(exception)}
75+
76+
77+
@router.get("/whiteboard_tags/")
78+
def get_whiteboard_tags(
79+
whiteboard_tag: Optional[str] = None,
80+
actions: Actions = Depends(get_actions),
81+
):
82+
"""API for viewing whiteboard_tags and associated data"""
83+
if existing := actions.get(whiteboard_tag):
84+
return {whiteboard_tag: existing}
85+
return actions.by_tag
86+
87+
88+
@router.get("/jira_projects/")
89+
def get_jira_projects():
90+
"""API for viewing projects that are currently accessible by API"""
91+
visible_projects: List[Dict] = jira_visible_projects()
92+
return [project["key"] for project in visible_projects]
93+
94+
95+
SRC_DIR = Path(__file__).parents[1]
96+
templates = Jinja2Templates(directory=SRC_DIR / "templates")
97+
98+
99+
@router.get("/powered_by_jbi/", response_class=HTMLResponse)
100+
def powered_by_jbi(
101+
request: Request,
102+
enabled: Optional[bool] = None,
103+
actions: Actions = Depends(get_actions),
104+
):
105+
"""API for `Powered By` endpoint"""
106+
context = {
107+
"request": request,
108+
"title": "Powered by JBI",
109+
"actions": jsonable_encoder(actions),
110+
"enable_query": enabled,
111+
}
112+
return templates.TemplateResponse("powered_by_template.html", context)

0 commit comments

Comments
 (0)