Skip to content

Commit c51e3eb

Browse files
authored
Tweak logging (log 500 requests), other logging-adjacent tweaks (#161)
* Set enum for acceptable environment values * Turn app reload off by default * Add environment variable to controll FastAPI debug mode * Add asgi.py module at project root to run app * Handle 500s in request summary exception handler - use new function to format fields * Refactor logging config - use mozlog format for root handler - disable uvicorn access logs - pass config dict to Uvicorn Config
1 parent fafc484 commit c51e3eb

File tree

10 files changed

+125
-93
lines changed

10 files changed

+125
-93
lines changed

.env.example

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Environment variables for the local development environment
2-
32
ENV=local
3+
APP_RELOAD=True
4+
APP_DEBUG=True
45

56
# Jira API Secrets
67
JIRA_USERNAME="fake_jira_username"

.secrets.baseline

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,14 +106,14 @@
106106
"filename": ".env.example",
107107
"hashed_secret": "4b9a4ce92b6a01a4cd6ee1672d31c043f2ae79ab",
108108
"is_verified": false,
109-
"line_number": 7
109+
"line_number": 8
110110
},
111111
{
112112
"type": "Secret Keyword",
113113
"filename": ".env.example",
114114
"hashed_secret": "77ea6398f252999314d609a708842a49fc43e055",
115115
"is_verified": false,
116-
"line_number": 10
116+
"line_number": 11
117117
}
118118
],
119119
"README.md": [
@@ -126,5 +126,5 @@
126126
}
127127
]
128128
},
129-
"generated_at": "2022-07-21T13:06:53Z"
129+
"generated_at": "2022-07-28T11:58:31Z"
130130
}

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ lint: $(INSTALL_STAMP)
5656

5757
.PHONY: start
5858
start: $(INSTALL_STAMP) $(DOTENV_FILE)
59-
poetry run python -m src.app.api
59+
poetry run python -m asgi
6060

6161
$(DOTENV_FILE):
6262
cp .env.example $(DOTENV_FILE)

asgi.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import uvicorn
2+
3+
from src.app.environment import get_settings
4+
from src.app.log import CONFIG as LOGGING_CONFIG
5+
6+
settings = get_settings()
7+
8+
9+
if __name__ == "__main__":
10+
server = uvicorn.Server(
11+
uvicorn.Config(
12+
"src.app.api:app",
13+
host=settings.host,
14+
port=settings.port,
15+
reload=settings.app_reload,
16+
log_config=LOGGING_CONFIG,
17+
)
18+
)
19+
server.run()

docker-compose.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ version: "3.8"
22
services:
33
web:
44
build: .
5-
command: python -m src.app.api
5+
command: python -m asgi
66
env_file:
77
- .env
88
# Let the init system handle signals for us.

src/app/api.py

Lines changed: 37 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from typing import Dict, List, Optional
99

1010
import sentry_sdk
11-
import uvicorn # type: ignore
1211
from fastapi import Body, Depends, FastAPI, Request
1312
from fastapi.encoders import jsonable_encoder
1413
from fastapi.responses import HTMLResponse
@@ -18,7 +17,6 @@
1817

1918
from src.app import configuration
2019
from src.app.environment import get_settings
21-
from src.app.log import configure_logging
2220
from src.app.monitor import api_router as monitor_router
2321
from src.jbi.bugzilla import BugzillaWebhookRequest
2422
from src.jbi.models import Actions
@@ -31,12 +29,11 @@
3129

3230
settings = get_settings()
3331

34-
configure_logging()
35-
3632
app = FastAPI(
3733
title="Jira Bugzilla Integration (JBI)",
3834
description="JBI v2 Platform",
3935
version="3.0.3",
36+
debug=settings.app_debug,
4037
)
4138

4239
app.include_router(monitor_router)
@@ -48,6 +45,42 @@
4845
app.add_middleware(SentryAsgiMiddleware)
4946

5047

48+
def format_log_fields(request: Request, request_time: float, status_code: int) -> Dict:
49+
"""Prepare Fields for Mozlog request summary"""
50+
51+
current_time = time.time()
52+
fields = {
53+
"agent": request.headers.get("User-Agent"),
54+
"path": request.url.path,
55+
"method": request.method,
56+
"lang": request.headers.get("Accept-Language"),
57+
"querystring": dict(request.query_params),
58+
"errno": 0,
59+
"t": int((current_time - request_time) * 1000.0),
60+
"time": datetime.fromtimestamp(current_time).isoformat(),
61+
"status_code": status_code,
62+
}
63+
return fields
64+
65+
66+
@app.middleware("http")
67+
async def request_summary(request: Request, call_next):
68+
"""Middleware to log request info"""
69+
summary_logger = logging.getLogger("request.summary")
70+
request_time = time.time()
71+
try:
72+
response = await call_next(request)
73+
log_fields = format_log_fields(
74+
request, request_time, status_code=response.status_code
75+
)
76+
summary_logger.info("", extra=log_fields)
77+
return response
78+
except Exception as exc:
79+
log_fields = format_log_fields(request, request_time, status_code=500)
80+
summary_logger.info(exc, extra=log_fields)
81+
raise
82+
83+
5184
@app.get("/", include_in_schema=False)
5285
def root():
5386
"""Expose key configuration"""
@@ -63,33 +96,6 @@ def root():
6396
}
6497

6598

66-
@app.middleware("http")
67-
async def request_summary(request: Request, call_next):
68-
"""Middleware to log request info"""
69-
summary_logger = logging.getLogger("request.summary")
70-
previous_time = time.time()
71-
72-
infos = {
73-
"agent": request.headers.get("User-Agent"),
74-
"path": request.url.path,
75-
"method": request.method,
76-
"lang": request.headers.get("Accept-Language"),
77-
"querystring": dict(request.query_params),
78-
"errno": 0,
79-
}
80-
81-
response = await call_next(request)
82-
83-
current = time.time()
84-
duration = int((current - previous_time) * 1000.0)
85-
isotimestamp = datetime.fromtimestamp(current).isoformat()
86-
infos = {"time": isotimestamp, "code": response.status_code, "t": duration, **infos}
87-
88-
summary_logger.info("", extra=infos)
89-
90-
return response
91-
92-
9399
@app.post("/bugzilla_webhook")
94100
def bugzilla_webhook(
95101
request: BugzillaWebhookRequest = Body(..., embed=False),
@@ -135,13 +141,3 @@ def powered_by_jbi(
135141
"enable_query": enabled,
136142
}
137143
return templates.TemplateResponse("powered_by_template.html", context)
138-
139-
140-
if __name__ == "__main__":
141-
uvicorn.run(
142-
"src.app.api:app",
143-
host=settings.host,
144-
port=settings.port,
145-
reload=settings.app_reload,
146-
log_level=settings.log_level,
147-
)

src/app/environment.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,22 @@
22
Module dedicated to interacting with the environment (variables, version.json)
33
"""
44
import json
5+
from enum import Enum
56
from functools import lru_cache
67
from pathlib import Path
78
from typing import Optional
89

910
from pydantic import AnyUrl, BaseSettings
1011

1112

13+
class Environment(str, Enum):
14+
"""Production environment choices"""
15+
16+
LOCAL = "local"
17+
NONPROD = "nonprod"
18+
PRODUCTION = "production"
19+
20+
1221
class SentryDsn(AnyUrl):
1322
"""Url type to validate Sentry DSN"""
1423

@@ -18,9 +27,10 @@ class Settings(BaseSettings):
1827

1928
host: str = "0.0.0.0"
2029
port: int = 8000
21-
app_reload: bool = True
22-
env: str = "nonprod"
30+
app_reload: bool = False
31+
app_debug: bool = False
2332
max_retries: int = 3
33+
env: Environment = Environment.NONPROD
2434

2535
# Jira
2636
jira_base_url: str = "https://mozit-test.atlassian.net/"

src/app/log.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,30 +9,31 @@
99

1010
settings = get_settings()
1111

12-
13-
def configure_logging():
14-
"""Configure logging; mozlog format for Dockerflow"""
15-
logging_config = {
16-
"version": 1,
17-
"formatters": {
18-
"mozlog_json": {
19-
"()": "dockerflow.logging.JsonLogFormatter",
20-
"logger_name": "jbi",
21-
},
12+
CONFIG = {
13+
"version": 1,
14+
"disable_existing_loggers": False,
15+
"formatters": {
16+
"mozlog_json": {
17+
"()": "dockerflow.logging.JsonLogFormatter",
18+
"logger_name": "jbi",
2219
},
23-
"handlers": {
24-
"console": {
25-
"level": settings.log_level.upper(),
26-
"class": "logging.StreamHandler",
27-
"formatter": "mozlog_json",
28-
"stream": sys.stdout,
29-
}
20+
},
21+
"handlers": {
22+
"console": {
23+
"level": settings.log_level.upper(),
24+
"class": "logging.StreamHandler",
25+
"formatter": "mozlog_json",
26+
"stream": sys.stdout,
3027
},
31-
"loggers": {
32-
"request.summary": {"handlers": ["console"], "level": "INFO"},
33-
"backoff": {"handlers": ["console"], "level": "INFO"},
34-
"src.jbi": {"handlers": ["console"], "level": "DEBUG"},
28+
"null": {
29+
"class": "logging.NullHandler",
3530
},
36-
}
37-
38-
logging.config.dictConfig(logging_config)
31+
},
32+
"loggers": {
33+
"": {"handlers": ["console"]},
34+
"request.summary": {"level": logging.INFO},
35+
"src": {"level": logging.DEBUG},
36+
"uvicorn": {"level": logging.INFO},
37+
"uvicorn.access": {"handlers": ["null"], "propagate": False},
38+
},
39+
}

tests/unit/app/test_api.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""
22
Module for testing src/app/api.py
33
"""
4+
import logging
45
from datetime import datetime
56

67
# pylint: disable=cannot-enumerate-pytest-fixtures
@@ -36,16 +37,17 @@ def test_read_root(anon_client):
3637

3738

3839
def test_request_summary_is_logged(caplog):
39-
with TestClient(app) as anon_client:
40-
# https://fastapi.tiangolo.com/advanced/testing-events/
41-
anon_client.get("/__lbheartbeat__")
40+
with caplog.at_level(logging.INFO):
41+
with TestClient(app) as anon_client:
42+
# https://fastapi.tiangolo.com/advanced/testing-events/
43+
anon_client.get("/__lbheartbeat__")
4244

43-
summary = caplog.records[-1]
45+
summary = caplog.records[-1]
4446

45-
assert summary.name == "request.summary"
46-
assert summary.method == "GET"
47-
assert summary.path == "/__lbheartbeat__"
48-
assert summary.querystring == {}
47+
assert summary.name == "request.summary"
48+
assert summary.method == "GET"
49+
assert summary.path == "/__lbheartbeat__"
50+
assert summary.querystring == {}
4951

5052

5153
def test_whiteboard_tags(anon_client):

tests/unit/jbi/test_runner.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""
22
Module for testing src/jbi/runner.py
33
"""
4+
import logging
5+
46
# pylint: disable=cannot-enumerate-pytest-fixtures
57
from unittest import mock
68

@@ -89,11 +91,12 @@ def test_execution_logging_for_successful_requests(
8991
actions_example: Actions,
9092
settings: Settings,
9193
):
92-
execute_action(
93-
request=webhook_create_example,
94-
actions=actions_example,
95-
settings=settings,
96-
)
94+
with caplog.at_level(logging.DEBUG):
95+
execute_action(
96+
request=webhook_create_example,
97+
actions=actions_example,
98+
settings=settings,
99+
)
97100

98101
captured_log_msgs = [
99102
r.msg % r.args for r in caplog.records if r.name == "src.jbi.runner"
@@ -114,13 +117,13 @@ def test_execution_logging_for_ignored_requests(
114117
):
115118
assert webhook_create_example.bug
116119
webhook_create_example.bug.whiteboard = "foo"
117-
118-
with pytest.raises(IgnoreInvalidRequestError):
119-
execute_action(
120-
request=webhook_create_example,
121-
actions=actions_example,
122-
settings=settings,
123-
)
120+
with caplog.at_level(logging.DEBUG):
121+
with pytest.raises(IgnoreInvalidRequestError):
122+
execute_action(
123+
request=webhook_create_example,
124+
actions=actions_example,
125+
settings=settings,
126+
)
124127

125128
captured_log_msgs = [
126129
r.msg % r.args for r in caplog.records if r.name == "src.jbi.runner"

0 commit comments

Comments
 (0)