Skip to content

Commit 3d13206

Browse files
Leverage dockerflow 2024.3.0 (#874)
* Remove redundant code * Set APP_DIR explicitly Without this, the APP_DIR is `/app` which makes tests fail. * python-dockerflow does not log querystrings (see mozilla-services/python-dockerflow#97) * Include dockerflow submodules in overrides * Include fastapi subdeps * Reorder imports * Register checks in lifespan (#878) * Register checks in lifespan * Add bugzilla checks * Ignore lifespan type * Add type to lifespan * Prepare for new features of python-dockerflow * Prepare for dockerflow-2024.3.0 * Revert "python-dockerflow does not log querystrings (see mozilla-services/python-dockerflow#97)" This reverts commit fbfe9e3. --------- Co-authored-by: grahamalama <[email protected]>
1 parent 43bd4d3 commit 3d13206

File tree

12 files changed

+338
-472
lines changed

12 files changed

+338
-472
lines changed

jbi/app.py

Lines changed: 62 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,35 @@
33
"""
44

55
import logging
6-
import time
6+
from contextlib import asynccontextmanager
77
from pathlib import Path
8-
from secrets import token_hex
9-
from typing import Any, Awaitable, Callable
8+
from typing import Any, AsyncGenerator
109

1110
import sentry_sdk
12-
from asgi_correlation_id import CorrelationIdMiddleware
11+
from dockerflow import checks
12+
from dockerflow.fastapi import router as dockerflow_router
13+
from dockerflow.fastapi.middleware import (
14+
MozlogRequestSummaryLogger,
15+
RequestIdMiddleware,
16+
)
17+
from dockerflow.version import get_version
1318
from fastapi import FastAPI, Request, Response, status
1419
from fastapi.encoders import jsonable_encoder
1520
from fastapi.exceptions import RequestValidationError
1621
from fastapi.responses import JSONResponse
1722
from fastapi.staticfiles import StaticFiles
1823

19-
from jbi.environment import get_settings, get_version
20-
from jbi.log import CONFIG, format_request_summary_fields
24+
import jbi.jira
25+
from jbi.configuration import ACTIONS
26+
from jbi.environment import get_settings
27+
from jbi.log import CONFIG
2128
from jbi.router import router
2229

2330
SRC_DIR = Path(__file__).parent
31+
APP_DIR = Path(__file__).parents[1]
2432

2533
settings = get_settings()
26-
version_info = get_version()
34+
version_info = get_version(APP_DIR)
2735

2836
logging.config.dictConfig(CONFIG)
2937

@@ -47,37 +55,62 @@ def traces_sampler(sampling_context: dict[str, Any]) -> float:
4755
)
4856

4957

58+
# https://github.com/tiangolo/fastapi/discussions/9241
59+
@asynccontextmanager
60+
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
61+
jira_service = jbi.jira.get_service()
62+
bugzilla_service = jbi.bugzilla.get_service()
63+
64+
checks.register(bugzilla_service.check_bugzilla_connection, name="bugzilla.up")
65+
checks.register(
66+
bugzilla_service.check_bugzilla_webhooks,
67+
name="bugzilla.all_webhooks_enabled",
68+
)
69+
70+
checks.register(jira_service.check_jira_connection, name="jira.up")
71+
checks.register_partial(
72+
jira_service.check_jira_all_projects_are_visible,
73+
ACTIONS,
74+
name="jira.all_projects_are_visible",
75+
)
76+
checks.register_partial(
77+
jira_service.check_jira_all_projects_have_permissions,
78+
ACTIONS,
79+
name="jira.all_projects_have_permissions",
80+
)
81+
checks.register_partial(
82+
jira_service.check_jira_all_project_custom_components_exist,
83+
ACTIONS,
84+
name="jira.all_project_custom_components_exist",
85+
)
86+
checks.register_partial(
87+
jira_service.check_jira_all_project_issue_types_exist,
88+
ACTIONS,
89+
name="jira.all_project_issue_types_exist",
90+
)
91+
checks.register(jira_service.check_jira_pandoc_install, name="jira.pandoc_install")
92+
93+
yield
94+
95+
5096
app = FastAPI(
5197
title="Jira Bugzilla Integration (JBI)",
5298
description="Platform providing synchronization of Bugzilla bugs to Jira issues.",
5399
version=version_info["version"],
54100
debug=settings.app_debug,
101+
lifespan=lifespan,
55102
)
56103

57-
app.include_router(router)
58-
app.mount("/static", StaticFiles(directory=SRC_DIR / "static"), name="static")
104+
app.state.APP_DIR = APP_DIR
105+
app.state.DOCKERFLOW_HEARTBEAT_FAILED_STATUS_CODE = 503
106+
app.state.DOCKERFLOW_SUMMARY_LOG_QUERYSTRING = True
59107

108+
app.include_router(router)
109+
app.include_router(dockerflow_router)
110+
app.add_middleware(RequestIdMiddleware)
111+
app.add_middleware(MozlogRequestSummaryLogger)
60112

61-
@app.middleware("http")
62-
async def request_summary(
63-
request: Request, call_next: Callable[[Request], Awaitable[Response]]
64-
) -> Response:
65-
"""Middleware to log request info"""
66-
summary_logger = logging.getLogger("request.summary")
67-
request_time = time.time()
68-
try:
69-
response = await call_next(request)
70-
log_fields = format_request_summary_fields(
71-
request, request_time, status_code=response.status_code
72-
)
73-
summary_logger.info("", extra=log_fields)
74-
return response
75-
except Exception as exc:
76-
log_fields = format_request_summary_fields(
77-
request, request_time, status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
78-
)
79-
summary_logger.info(exc, extra=log_fields)
80-
raise
113+
app.mount("/static", StaticFiles(directory=SRC_DIR / "static"), name="static")
81114

82115

83116
@app.exception_handler(RequestValidationError)
@@ -100,11 +133,3 @@ async def validation_exception_handler(
100133
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
101134
content={"detail": jsonable_encoder(exc.errors())},
102135
)
103-
104-
105-
app.add_middleware(
106-
CorrelationIdMiddleware,
107-
header_name="X-Request-Id",
108-
generator=lambda: token_hex(16),
109-
validator=None,
110-
)

jbi/bugzilla/service.py

Lines changed: 50 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,56 @@ def list_webhooks(self):
5151

5252
return self.client.list_webhooks()
5353

54+
def check_bugzilla_connection(self):
55+
if not self.client.logged_in():
56+
return [checks.Error("Login fails or service down", id="bugzilla.login")]
57+
return []
58+
59+
def check_bugzilla_webhooks(self):
60+
# Do not bother executing the rest of checks if connection fails.
61+
if messages := self.check_bugzilla_connection():
62+
return messages
63+
64+
# Check that all JBI webhooks are enabled in Bugzilla,
65+
# and report disabled ones.
66+
try:
67+
jbi_webhooks = self.list_webhooks()
68+
except (BugzillaClientError, requests.HTTPError) as e:
69+
return [
70+
checks.Error(
71+
f"Could not list webhooks ({e})", id="bugzilla.webhooks.fetch"
72+
)
73+
]
74+
75+
results = []
76+
77+
if len(jbi_webhooks) == 0:
78+
results.append(
79+
checks.Warning("No webhooks enabled", id="bugzilla.webhooks.empty")
80+
)
81+
82+
for webhook in jbi_webhooks:
83+
# Report errors in each webhook
84+
statsd.gauge(f"jbi.bugzilla.webhooks.{webhook.slug}.errors", webhook.errors)
85+
# Warn developers when there are errors
86+
if webhook.errors > 0:
87+
results.append(
88+
checks.Warning(
89+
f"Webhook {webhook.name} has {webhook.errors} error(s)",
90+
id="bugzilla.webhooks.errors",
91+
)
92+
)
93+
94+
if not webhook.enabled:
95+
results.append(
96+
checks.Error(
97+
f"Webhook {webhook.name} is disabled ({webhook.errors} errors)",
98+
id="bugzilla.webhooks.disabled",
99+
)
100+
)
101+
102+
return results
103+
54104

55105
@lru_cache(maxsize=1)
56106
def get_service():
@@ -59,58 +109,3 @@ def get_service():
59109
settings.bugzilla_base_url, api_key=str(settings.bugzilla_api_key)
60110
)
61111
return BugzillaService(client=client)
62-
63-
64-
@checks.register(name="bugzilla.up")
65-
def check_bugzilla_connection(service=None):
66-
service = service or get_service()
67-
if not service.client.logged_in():
68-
return [checks.Error("Login fails or service down", id="bugzilla.login")]
69-
return []
70-
71-
72-
@checks.register(name="bugzilla.all_webhooks_enabled")
73-
def check_bugzilla_webhooks(service=None):
74-
service = service or get_service()
75-
76-
# Do not bother executing the rest of checks if connection fails.
77-
if messages := check_bugzilla_connection(service):
78-
return messages
79-
80-
# Check that all JBI webhooks are enabled in Bugzilla,
81-
# and report disabled ones.
82-
try:
83-
jbi_webhooks = service.list_webhooks()
84-
except (BugzillaClientError, requests.HTTPError) as e:
85-
return [
86-
checks.Error(f"Could not list webhooks ({e})", id="bugzilla.webhooks.fetch")
87-
]
88-
89-
results = []
90-
91-
if len(jbi_webhooks) == 0:
92-
results.append(
93-
checks.Warning("No webhooks enabled", id="bugzilla.webhooks.empty")
94-
)
95-
96-
for webhook in jbi_webhooks:
97-
# Report errors in each webhook
98-
statsd.gauge(f"jbi.bugzilla.webhooks.{webhook.slug}.errors", webhook.errors)
99-
# Warn developers when there are errors
100-
if webhook.errors > 0:
101-
results.append(
102-
checks.Warning(
103-
f"Webhook {webhook.name} has {webhook.errors} error(s)",
104-
id="bugzilla.webhooks.errors",
105-
)
106-
)
107-
108-
if not webhook.enabled:
109-
results.append(
110-
checks.Error(
111-
f"Webhook {webhook.name} is disabled ({webhook.errors} errors)",
112-
id="bugzilla.webhooks.disabled",
113-
)
114-
)
115-
116-
return results

jbi/environment.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,9 @@
22
Module dedicated to interacting with the environment (variables, version.json)
33
"""
44

5-
import json
6-
75
# https://github.com/python/mypy/issues/12841
86
from enum import StrEnum, auto # type: ignore
97
from functools import lru_cache
10-
from pathlib import Path
118
from typing import Optional
129

1310
from pydantic import AnyUrl
@@ -58,13 +55,3 @@ class Settings(BaseSettings):
5855
def get_settings() -> Settings:
5956
"""Return the Settings object; use cache"""
6057
return Settings()
61-
62-
63-
@lru_cache(maxsize=1)
64-
def get_version():
65-
"""Return contents of version.json. This has generic data in repo, but gets the build details in CI."""
66-
info = {}
67-
version_path = Path(__file__).parents[1] / "version.json"
68-
if version_path.exists():
69-
info = json.loads(version_path.read_text(encoding="utf8"))
70-
return info

0 commit comments

Comments
 (0)