Skip to content

Commit 6826c41

Browse files
TheByronHimesCito
andauthored
Log request duration (GSI-1220) (#54)
* Reduce CID logs to DEBUG level * Bump version from 3.1.7 -> 3.1.8 * Measure and log request duration while CID is set * Stringify url in details and add space before ms * Add a test checking for the req duration log * Use integer for duration Co-authored-by: Christoph Zwerschke <[email protected]> * Specify "_ms" in log details Co-authored-by: Christoph Zwerschke <[email protected]> * Improve req duration log test --------- Co-authored-by: Christoph Zwerschke <[email protected]>
1 parent 98fdf4e commit 6826c41

File tree

4 files changed

+57
-6
lines changed

4 files changed

+57
-6
lines changed

.pyproject_generation/pyproject_custom.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "ghga_service_commons"
3-
version = "3.1.7"
3+
version = "3.1.8"
44
description = "A library that contains common functionality used in services of GHGA"
55
readme = "README.md"
66
authors = [

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ classifiers = [
2323
"Intended Audience :: Developers",
2424
]
2525
name = "ghga_service_commons"
26-
version = "3.1.7"
26+
version = "3.1.8"
2727
description = "A library that contains common functionality used in services of GHGA"
2828
dependencies = [
2929
"pydantic >=2, <3",

src/ghga_service_commons/api/api.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020
"""
2121

2222
import asyncio
23+
import http
2324
import logging
25+
import time
2426
from collections.abc import Sequence
2527
from functools import partial
2628
from typing import Optional, Union
@@ -159,7 +161,7 @@ def set_header_correlation_id(request: Request, correlation_id: str):
159161
request.scope.update(headers=headers.raw)
160162
# delete _headers to force update
161163
delattr(request, "_headers")
162-
log.info("Assigned %s as header correlation ID value.", correlation_id)
164+
log.debug("Assigned %s as header correlation ID value.", correlation_id)
163165

164166

165167
def get_validated_correlation_id(
@@ -177,7 +179,7 @@ def get_validated_correlation_id(
177179
"""
178180
if not correlation_id and generate_correlation_id:
179181
correlation_id = new_correlation_id()
180-
log.info("Generated new correlation id: %s", correlation_id)
182+
log.debug("Generated new correlation id: %s", correlation_id)
181183
else:
182184
validate_correlation_id(correlation_id)
183185
return correlation_id
@@ -217,6 +219,29 @@ async def correlation_id_middleware(
217219
return response
218220

219221

222+
async def request_logging_middleware(request: Request, call_next):
223+
"""Measure and log the amount of time it takes to process the HTTP request."""
224+
url = request.url
225+
start_time = time.time()
226+
response = await call_next(request)
227+
duration = int(round((time.time() - start_time) * 1000))
228+
try:
229+
status_phrase = http.HTTPStatus(response.status_code).phrase
230+
except ValueError:
231+
status_phrase = ""
232+
msg = f'{request.method} {url} "{response.status_code} {status_phrase}" - {duration} ms'
233+
log.info(
234+
msg,
235+
extra={
236+
"method": request.method,
237+
"url": str(url),
238+
"status_code": response.status_code,
239+
"duration_ms": duration,
240+
},
241+
)
242+
return response
243+
244+
220245
def configure_app(app: FastAPI, config: ApiConfigBase):
221246
"""Configure a FastAPI app based on a config object."""
222247
app.root_path = config.api_root_path.rstrip("/")
@@ -235,6 +260,7 @@ def configure_app(app: FastAPI, config: ApiConfigBase):
235260
kwargs["allow_credentials"] = config.cors_allow_credentials
236261

237262
app.add_middleware(CORSMiddleware, **kwargs)
263+
app.add_middleware(BaseHTTPMiddleware, dispatch=request_logging_middleware)
238264
app.add_middleware(
239265
BaseHTTPMiddleware,
240266
dispatch=partial(

tests/integration/test_api.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717

1818
import asyncio
1919
import multiprocessing
20+
import re
2021
import time
2122

2223
import httpx
2324
import pytest
2425
from fastapi import FastAPI
2526

2627
from ghga_service_commons.api import ApiConfigBase, run_server
28+
from ghga_service_commons.api.api import configure_app
2729
from ghga_service_commons.api.testing import AsyncTestClient
2830
from ghga_service_commons.httpyexpect.server import HttpException
2931
from ghga_service_commons.httpyexpect.server.handlers.fastapi_ import (
@@ -32,8 +34,9 @@
3234
from tests.integration.fixtures.hello_world_test_app import GREETING, app
3335
from tests.integration.fixtures.utils import find_free_port
3436

37+
pytestmark = pytest.mark.asyncio()
38+
3539

36-
@pytest.mark.asyncio
3740
async def test_run_server():
3841
"""Test the run_server wrapper function."""
3942
config = ApiConfigBase()
@@ -58,7 +61,6 @@ async def test_run_server():
5861
assert response.json() == GREETING
5962

6063

61-
@pytest.mark.asyncio
6264
async def test_configure_exception_handler():
6365
"""Test the exception handler configuration of a FastAPI app."""
6466
# example params for an http exception
@@ -92,3 +94,26 @@ async def test_route():
9294
assert body["exception_id"] == exception_id
9395
assert body["description"] == description
9496
assert body["data"] == data
97+
98+
99+
async def test_request_duration_log(caplog):
100+
"""Check the middleware function that logs request duration in ms."""
101+
caplog.set_level("INFO")
102+
103+
# small fastapi app with basic endpoint
104+
test_app = FastAPI()
105+
test_app.get("/")(lambda: 200)
106+
107+
config = ApiConfigBase()
108+
configure_app(test_app, config)
109+
110+
client = AsyncTestClient(test_app)
111+
await client.get("/")
112+
113+
# Get list of the log messages
114+
for record in caplog.record_tuples:
115+
if record[0] == "ghga_service_commons.api.api":
116+
assert re.match(r'GET http://.* "200 OK" - \d+ ms$', record[2])
117+
break
118+
else:
119+
assert False, "Request duration log was not captured"

0 commit comments

Comments
 (0)