1313
1414from common .core .logging import JsonFormatter
1515from common .gunicorn import metrics
16- from common .gunicorn .constants import WSGI_DJANGO_ROUTE_ENVIRON_KEY
16+ from common .gunicorn .constants import (
17+ WSGI_EXTRA_PREFIX ,
18+ WSGI_EXTRA_SUFFIX_TO_CATEGORY ,
19+ wsgi_extra_key_regex ,
20+ )
21+ from common .gunicorn .utils import get_extra
1722
1823
1924class GunicornAccessLogJsonFormatter (JsonFormatter ):
25+ def _get_extra (self , record_args : dict [str , Any ]) -> dict [str , Any ]:
26+ ret : dict [str , dict [str , Any ]] = {}
27+
28+ extra_items_to_log : list [str ] | None
29+ if extra_items_to_log := getattr (settings , "ACCESS_LOG_EXTRA_ITEMS" , None ):
30+ # We expect the extra items to be in the form of
31+ # Gunicorn's access log format string for
32+ # request headers, response headers and environ variables
33+ # without the % prefix, e.g. "{origin}i" or "{flagsmith.environment_id}e"
34+ # https://docs.gunicorn.org/en/stable/settings.html#access-log-format
35+ for extra_key in extra_items_to_log :
36+ extra_key_lower = extra_key .lower ()
37+ if (
38+ (extra_value := record_args .get (extra_key_lower ))
39+ and (re_match := wsgi_extra_key_regex .match (extra_key_lower ))
40+ and (
41+ extra_category := WSGI_EXTRA_SUFFIX_TO_CATEGORY .get (
42+ re_match .group ("suffix" )
43+ )
44+ )
45+ ):
46+ ret .setdefault (extra_category , {})[re_match .group ("key" )] = (
47+ extra_value
48+ )
49+
50+ return ret
51+
2052 def get_json_record (self , record : logging .LogRecord ) -> dict [str , Any ]:
2153 args = record .args
2254
@@ -32,11 +64,13 @@ def get_json_record(self, record: logging.LogRecord) -> dict[str, Any]:
3264 "time" : datetime .strptime (args ["t" ], "[%d/%b/%Y:%H:%M:%S %z]" ).isoformat (),
3365 "path" : url ,
3466 "remote_ip" : args ["h" ],
35- "route" : args [ "R" ] ,
67+ "route" : args . get ( f"{{ { WSGI_EXTRA_PREFIX } route}}e" ) or "" ,
3668 "method" : args ["m" ],
3769 "status" : str (args ["s" ]),
3870 "user_agent" : args ["a" ],
3971 "duration_in_ms" : args ["M" ],
72+ "response_size_in_bytes" : args ["B" ] or 0 ,
73+ ** self ._get_extra (args ),
4074 }
4175
4276
@@ -56,7 +90,7 @@ def access(
5690 # To avoid cardinality explosion, we use a resolved Django route
5791 # instead of raw path.
5892 # The Django route is set by `RouteLoggerMiddleware`.
59- "route" : environ . get ( WSGI_DJANGO_ROUTE_ENVIRON_KEY ) or "" ,
93+ "route" : get_extra ( environ = environ , key = "route" ) or "" ,
6094 "method" : environ .get ("REQUEST_METHOD" ) or "" ,
6195 "response_status" : resp .status_code ,
6296 }
@@ -65,22 +99,11 @@ def access(
6599 )
66100 metrics .flagsmith_http_server_requests_total .labels (** labels ).inc ()
67101 metrics .flagsmith_http_server_response_size_bytes .labels (** labels ).observe (
68- resp . response_length or 0 ,
102+ getattr ( resp , "sent" , 0 ) ,
69103 )
70104
71105
72106class GunicornJsonCapableLogger (PrometheusGunicornLogger ):
73- def atoms (
74- self ,
75- resp : Response ,
76- req : Request ,
77- environ : dict [str , Any ],
78- request_time : timedelta ,
79- ) -> dict [str , str ]:
80- atoms : dict [str , str ] = super ().atoms (resp , req , environ , request_time )
81- atoms ["R" ] = environ .get (WSGI_DJANGO_ROUTE_ENVIRON_KEY ) or "-"
82- return atoms
83-
84107 def setup (self , cfg : Config ) -> None :
85108 super ().setup (cfg )
86109 if getattr (settings , "LOG_FORMAT" , None ) == "json" :
0 commit comments