|
1 | 1 | import logging |
2 | 2 | import os |
3 | | -from datetime import datetime, timedelta |
| 3 | +from datetime import timedelta |
4 | 4 | from typing import Any |
5 | 5 |
|
6 | 6 | from gunicorn.config import Config # type: ignore[import-untyped] |
|
9 | 9 | from gunicorn.instrument.statsd import ( # type: ignore[import-untyped] |
10 | 10 | Statsd as StatsdGunicornLogger, |
11 | 11 | ) |
12 | | -from structlog.typing import EventDict, Processor, WrappedLogger |
13 | 12 |
|
14 | 13 | from common.gunicorn import metrics |
15 | | -from common.gunicorn.constants import ( |
16 | | - WSGI_EXTRA_SUFFIX_TO_CATEGORY, |
17 | | - wsgi_extra_key_regex, |
18 | | -) |
19 | 14 | from common.gunicorn.utils import get_extra |
20 | 15 |
|
21 | 16 |
|
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 | | - |
88 | 17 | class PrometheusGunicornLogger(StatsdGunicornLogger): # type: ignore[misc] |
89 | 18 | """Gunicorn logger that records Prometheus metrics on each access log entry.""" |
90 | 19 |
|
|
0 commit comments