Skip to content

Commit b355ee1

Browse files
committed
fix imports
1 parent 0391a59 commit b355ee1

File tree

4 files changed

+77
-75
lines changed

4 files changed

+77
-75
lines changed

src/common/core/main.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from common.core.cli import healthcheck
1414
from common.core.logging import setup_logging
15+
from common.gunicorn.processors import make_gunicorn_access_processor
1516

1617
env = Env()
1718

@@ -37,8 +38,6 @@ def ensure_cli_env() -> typing.Generator[None, None, None]:
3738
ctx = contextlib.ExitStack()
3839

3940
# Set up logging early, before Django settings are loaded.
40-
from common.gunicorn.logging import make_gunicorn_access_processor
41-
4241
setup_logging(
4342
log_level=env.str("LOG_LEVEL", "INFO"),
4443
log_format=env.str("LOG_FORMAT", "generic"),

src/common/gunicorn/logging.py

Lines changed: 1 addition & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22
import os
3-
from datetime import datetime, timedelta
3+
from datetime import timedelta
44
from typing import Any
55

66
from gunicorn.config import Config # type: ignore[import-untyped]
@@ -9,82 +9,11 @@
99
from gunicorn.instrument.statsd import ( # type: ignore[import-untyped]
1010
Statsd as StatsdGunicornLogger,
1111
)
12-
from structlog.typing import EventDict, Processor, WrappedLogger
1312

1413
from common.gunicorn import metrics
15-
from common.gunicorn.constants import (
16-
WSGI_EXTRA_SUFFIX_TO_CATEGORY,
17-
wsgi_extra_key_regex,
18-
)
1914
from common.gunicorn.utils import get_extra
2015

2116

22-
def make_gunicorn_access_processor(
23-
access_log_extra_items: list[str] | None = None,
24-
) -> Processor:
25-
"""Create a processor that extracts structured fields from Gunicorn access logs.
26-
27-
Gunicorn populates ``record.args`` with a dict of request/response data
28-
(keyed by format variables like ``h``, ``m``, ``s``, ``U``, etc.). This
29-
processor detects those records and promotes the data into the event dict
30-
so it flows through the normal rendering pipeline.
31-
32-
Pass the returned processor to :func:`~common.core.logging.setup_logging`
33-
via ``extra_foreign_processors``.
34-
"""
35-
36-
def processor(
37-
logger: WrappedLogger,
38-
method_name: str,
39-
event_dict: EventDict,
40-
) -> EventDict:
41-
record = event_dict.get("_record")
42-
if record is None or record.name != "gunicorn.access":
43-
return event_dict
44-
# ProcessorFormatter clears record.args before running
45-
# foreign_pre_chain; the originals are stashed on the record
46-
# by _SentryFriendlyProcessorFormatter.format().
47-
args = getattr(record, "_original_args", record.args)
48-
if not isinstance(args, dict):
49-
return event_dict
50-
51-
url = args.get("U", "")
52-
if q := args.get("q"):
53-
url += f"?{q}"
54-
55-
if t := args.get("t"):
56-
event_dict["time"] = datetime.strptime(
57-
t, "[%d/%b/%Y:%H:%M:%S %z]"
58-
).isoformat()
59-
event_dict["path"] = url
60-
event_dict["remote_ip"] = args.get("h", "")
61-
event_dict["method"] = args.get("m", "")
62-
event_dict["status"] = str(args.get("s", ""))
63-
event_dict["user_agent"] = args.get("a", "")
64-
event_dict["duration_in_ms"] = args.get("M", 0)
65-
event_dict["response_size_in_bytes"] = args.get("B") or 0
66-
67-
if access_log_extra_items:
68-
for extra_key in access_log_extra_items:
69-
extra_key_lower = extra_key.lower()
70-
if (
71-
(extra_value := args.get(extra_key_lower))
72-
and (re_match := wsgi_extra_key_regex.match(extra_key_lower))
73-
and (
74-
category := WSGI_EXTRA_SUFFIX_TO_CATEGORY.get(
75-
re_match.group("suffix")
76-
)
77-
)
78-
):
79-
event_dict.setdefault(category, {})[re_match.group("key")] = (
80-
extra_value
81-
)
82-
83-
return event_dict
84-
85-
return processor
86-
87-
8817
class PrometheusGunicornLogger(StatsdGunicornLogger): # type: ignore[misc]
8918
"""Gunicorn logger that records Prometheus metrics on each access log entry."""
9019

src/common/gunicorn/processors.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from datetime import datetime
2+
3+
from structlog.typing import EventDict, Processor, WrappedLogger
4+
5+
from common.gunicorn.constants import (
6+
WSGI_EXTRA_SUFFIX_TO_CATEGORY,
7+
wsgi_extra_key_regex,
8+
)
9+
10+
11+
def make_gunicorn_access_processor(
12+
access_log_extra_items: list[str] | None = None,
13+
) -> Processor:
14+
"""Create a processor that extracts structured fields from Gunicorn access logs.
15+
16+
Gunicorn populates ``record.args`` with a dict of request/response data
17+
(keyed by format variables like ``h``, ``m``, ``s``, ``U``, etc.). This
18+
processor detects those records and promotes the data into the event dict
19+
so it flows through the normal rendering pipeline.
20+
21+
Pass the returned processor to :func:`~common.core.logging.setup_logging`
22+
via ``extra_foreign_processors``.
23+
"""
24+
25+
def processor(
26+
logger: WrappedLogger,
27+
method_name: str,
28+
event_dict: EventDict,
29+
) -> EventDict:
30+
record = event_dict.get("_record")
31+
if record is None or record.name != "gunicorn.access":
32+
return event_dict
33+
# ProcessorFormatter clears record.args before running
34+
# foreign_pre_chain; the originals are stashed on the record
35+
# by _SentryFriendlyProcessorFormatter.format().
36+
args = getattr(record, "_original_args", record.args)
37+
if not isinstance(args, dict):
38+
return event_dict
39+
40+
url = args.get("U", "")
41+
if q := args.get("q"):
42+
url += f"?{q}"
43+
44+
if t := args.get("t"):
45+
event_dict["time"] = datetime.strptime(
46+
t, "[%d/%b/%Y:%H:%M:%S %z]"
47+
).isoformat()
48+
event_dict["path"] = url
49+
event_dict["remote_ip"] = args.get("h", "")
50+
event_dict["method"] = args.get("m", "")
51+
event_dict["status"] = str(args.get("s", ""))
52+
event_dict["user_agent"] = args.get("a", "")
53+
event_dict["duration_in_ms"] = args.get("M", 0)
54+
event_dict["response_size_in_bytes"] = args.get("B") or 0
55+
56+
if access_log_extra_items:
57+
for extra_key in access_log_extra_items:
58+
extra_key_lower = extra_key.lower()
59+
if (
60+
(extra_value := args.get(extra_key_lower))
61+
and (re_match := wsgi_extra_key_regex.match(extra_key_lower))
62+
and (
63+
category := WSGI_EXTRA_SUFFIX_TO_CATEGORY.get(
64+
re_match.group("suffix")
65+
)
66+
)
67+
):
68+
event_dict.setdefault(category, {})[re_match.group("key")] = (
69+
extra_value
70+
)
71+
72+
return event_dict
73+
74+
return processor

tests/unit/common/gunicorn/test_logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
from common.gunicorn.logging import (
1414
GunicornJsonCapableLogger,
1515
PrometheusGunicornLogger,
16-
make_gunicorn_access_processor,
1716
)
17+
from common.gunicorn.processors import make_gunicorn_access_processor
1818
from common.gunicorn.utils import DEFAULT_ACCESS_LOG_FORMAT
1919
from common.test_tools import AssertMetricFixture
2020

0 commit comments

Comments
 (0)