Skip to content

Commit f16dab1

Browse files
ajslaterclaude
andauthored
Downgrade routine anon-403s on /api/v3/auth/profile/ to DEBUG (#759)
A fresh browser visit to Codex probes /api/v3/auth/profile/ before any session cookie exists, which Django's BaseHandler.get_response logs as a WARNING ("Forbidden: /api/v3/auth/profile/") for every first-time visitor. That drowns the main log in routine noise. Add a logging.Filter on django.request that downgrades records matching that exact "Forbidden: <path>" pattern to DEBUG, parameterized by a small frozenset of known-noisy paths so abuse on other endpoints still surfaces at WARNING. Also move django.request into the loggers dict where dictConfig will actually see it — the old top-level placement was silently ignored. Records still flow to the root loguru handler via propagation. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 10d5662 commit f16dab1

2 files changed

Lines changed: 108 additions & 17 deletions

File tree

codex/settings/logging.py

Lines changed: 40 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,35 @@
11
"""Logging Settings."""
22

3-
from logging import Handler
4-
from typing import override
3+
import logging
4+
from logging import Filter, Handler
5+
from typing import Final, override
56

67
from loguru import logger
78

9+
# Paths whose routine anon-403s are noise, not abuse. The first-load
10+
# probe to ``/api/v3/auth/profile/`` is the obvious offender — a fresh
11+
# browser hits it before any session cookie exists. Django's
12+
# BaseHandler.get_response logs every 4xx at WARNING; we downgrade the
13+
# ones we expect so the main log stays useful.
14+
_NOISY_FORBIDDEN_PATHS: Final[frozenset[str]] = frozenset({"/api/v3/auth/profile/"})
15+
16+
17+
class DowngradeNoisyForbiddenFilter(Filter):
18+
"""Downgrade routine anon 403s on known-noisy paths to DEBUG."""
19+
20+
@override
21+
def filter(self, record) -> bool:
22+
if record.levelno != logging.WARNING:
23+
return True
24+
msg = record.getMessage()
25+
if not msg.startswith("Forbidden: "):
26+
return True
27+
path = msg.split(": ", 1)[1]
28+
if path in _NOISY_FORBIDDEN_PATHS:
29+
record.levelno = logging.DEBUG
30+
record.levelname = "DEBUG"
31+
return True
32+
833

934
class LoguruHandler(Handler):
1035
"""Redirect logging to loguru."""
@@ -71,9 +96,22 @@ def get_logging_settings(loglevel: str | int, *, debug: bool) -> dict[str, int |
7196
}
7297
)
7398
level = "DEBUG" if loglevel == "TRACE" else loglevel
99+
# ``django.request`` belongs inside ``loggers`` so dictConfig sees it;
100+
# putting it at the top level (as this file used to) is silently
101+
# ignored. Records still reach the root ``loguru`` handler via
102+
# propagation, but the filter only runs when the logger is declared
103+
# here.
104+
loggers["django.request"] = {
105+
"filters": ["downgrade_noisy_forbidden"],
106+
}
74107
return {
75108
"version": 1,
76109
"disable_existing_loggers": True,
110+
"filters": {
111+
"downgrade_noisy_forbidden": {
112+
"()": "codex.settings.logging.DowngradeNoisyForbiddenFilter",
113+
},
114+
},
77115
"handlers": {
78116
"loguru": {
79117
"class": "codex.settings.logging.LoguruHandler",
@@ -84,20 +122,5 @@ def get_logging_settings(loglevel: str | int, *, debug: bool) -> dict[str, int |
84122
"level": level,
85123
"propagate": True,
86124
},
87-
"django": {
88-
"handlers": ["loguru"],
89-
"level": level,
90-
"propagate": True,
91-
},
92-
"django.request": {
93-
"handlers": ["loguru"],
94-
"level": level,
95-
"propagate": True,
96-
},
97-
"django.server": {
98-
"handlers": ["loguru"],
99-
"level": level,
100-
"propagate": True,
101-
},
102125
"loggers": loggers,
103126
}

tests/test_logging_filter.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""Tests for the noisy-Forbidden log filter."""
2+
3+
import logging
4+
from typing import Final, override
5+
6+
from django.test import TestCase
7+
8+
from codex.settings.logging import DowngradeNoisyForbiddenFilter
9+
10+
_FILTER_NAME: Final = "test"
11+
12+
13+
def _build_record(level: int, msg: str, *args: object) -> logging.LogRecord:
14+
return logging.LogRecord(
15+
name="django.request",
16+
level=level,
17+
pathname=__file__,
18+
lineno=0,
19+
msg=msg,
20+
args=args,
21+
exc_info=None,
22+
)
23+
24+
25+
class DowngradeNoisyForbiddenFilterTests(TestCase):
26+
"""Cover the level-downgrade logic on the request logger filter."""
27+
28+
@override
29+
def setUp(self) -> None:
30+
"""Build one filter shared across the cases."""
31+
self.flt = DowngradeNoisyForbiddenFilter(_FILTER_NAME) # pyright: ignore[reportUninitializedInstanceVariable]
32+
33+
def test_downgrades_noisy_path(self) -> None:
34+
"""First-load profile probe becomes DEBUG."""
35+
record = _build_record(
36+
logging.WARNING, "%s: %s", "Forbidden", "/api/v3/auth/profile/"
37+
)
38+
kept = self.flt.filter(record)
39+
assert kept is True
40+
assert record.levelno == logging.DEBUG
41+
assert record.levelname == "DEBUG"
42+
43+
def test_keeps_unrelated_forbidden_at_warning(self) -> None:
44+
"""A Forbidden on a non-noisy path stays at WARNING."""
45+
record = _build_record(
46+
logging.WARNING, "%s: %s", "Forbidden", "/api/v3/admin/users/"
47+
)
48+
kept = self.flt.filter(record)
49+
assert kept is True
50+
assert record.levelno == logging.WARNING
51+
52+
def test_keeps_non_forbidden_at_warning(self) -> None:
53+
"""A Bad Request line through django.request stays at WARNING."""
54+
record = _build_record(
55+
logging.WARNING, "%s: %s", "Bad Request", "/api/v3/auth/profile/"
56+
)
57+
kept = self.flt.filter(record)
58+
assert kept is True
59+
assert record.levelno == logging.WARNING
60+
61+
def test_ignores_non_warning_levels(self) -> None:
62+
"""ERROR-level records pass through untouched."""
63+
record = _build_record(
64+
logging.ERROR, "%s: %s", "Forbidden", "/api/v3/auth/profile/"
65+
)
66+
kept = self.flt.filter(record)
67+
assert kept is True
68+
assert record.levelno == logging.ERROR

0 commit comments

Comments
 (0)