Skip to content

Commit 5f90902

Browse files
committed
Filter sentry requests
1 parent 5c0f43c commit 5f90902

5 files changed

Lines changed: 137 additions & 50 deletions

File tree

astra_app/config/logging_filters.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ def _is_root_or_login_path(path: str) -> bool:
2424
)
2525

2626

27+
def _is_quiet_request_path(path: str) -> bool:
28+
return path == "/_ci" or path.startswith("/_ci/")
29+
30+
2731
def _extract_exception_from_args(args: object) -> BaseException | None:
2832
if isinstance(args, BaseException):
2933
return args
@@ -77,6 +81,20 @@ def filter(self, record: logging.LogRecord) -> bool:
7781
return not _is_root_or_login_path(request_path)
7882

7983

84+
class QuietRequestPathFilter(logging.Filter):
85+
def filter(self, record: logging.LogRecord) -> bool:
86+
request_path: str | None = None
87+
if hasattr(record, "request_path"):
88+
request_path = str(record.request_path)
89+
else:
90+
request_path = _extract_request_path(record.getMessage())
91+
92+
if request_path is None:
93+
return True
94+
95+
return not _is_quiet_request_path(request_path)
96+
97+
8098
class RequestContextFilter(logging.Filter):
8199
def filter(self, record: logging.LogRecord) -> bool:
82100
context = get_request_log_context()

astra_app/config/settings.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,9 @@ def _sentry_before_send(event: dict, hint: dict) -> dict | None:
987987
'hetrix_access': {
988988
'()': 'config.logging_filters.HetrixAccessFilter',
989989
},
990+
'quiet_request_paths': {
991+
'()': 'config.logging_filters.QuietRequestPathFilter',
992+
},
990993
'request_context': {
991994
'()': 'config.logging_filters.RequestContextFilter',
992995
},
@@ -1016,14 +1019,14 @@ def _sentry_before_send(event: dict, hint: dict) -> dict | None:
10161019
'core': {
10171020
'handlers': ['console'],
10181021
'level': 'DEBUG' if DEBUG else 'INFO',
1019-
'filters': ['request_context'],
1022+
'filters': ['request_context', 'quiet_request_paths'],
10201023
'propagate': False,
10211024
},
10221025
# Structured app-level access logs enriched with request/user context.
10231026
'astra.access': {
10241027
'handlers': ['access_console'],
10251028
'level': 'INFO',
1026-
'filters': ['health_endpoint', 'hetrix_access', 'request_context'],
1029+
'filters': ['health_endpoint', 'hetrix_access', 'request_context', 'quiet_request_paths'],
10271030
'propagate': False,
10281031
},
10291032
# FreeIPA client libs can be noisy; keep them at INFO by default.
@@ -1037,14 +1040,14 @@ def _sentry_before_send(event: dict, hint: dict) -> dict | None:
10371040
'django.request': {
10381041
'handlers': ['console'],
10391042
'level': 'DEBUG',
1040-
'filters': ['request_context'],
1043+
'filters': ['request_context', 'quiet_request_paths'],
10411044
'propagate': False,
10421045
},
10431046
# Access logs from `runserver`.
10441047
'django.server': {
10451048
'handlers': ['console'],
10461049
'level': 'WARNING',
1047-
'filters': ['health_endpoint', 'hetrix_access', 'request_context'],
1050+
'filters': ['health_endpoint', 'hetrix_access', 'request_context', 'quiet_request_paths'],
10481051
'propagate': False,
10491052
},
10501053
# Django security events

astra_app/core/middleware.py

Lines changed: 50 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@
3131
_ACCESS_LOG_TEMPLATE = '%({x-forwarded-for}i)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
3232

3333

34+
def _should_skip_access_log(request_path: str) -> bool:
35+
return request_path == "/_ci" or request_path.startswith("/_ci/")
36+
37+
3438
def _format_access_log_timestamp() -> str:
3539
return timezone.now().strftime("[%d/%b/%Y:%H:%M:%S %z]")
3640

@@ -369,51 +373,52 @@ def __call__(self, request):
369373
duration_seconds = time.monotonic() - started_at
370374
duration_ms = int(round(duration_seconds * 1000))
371375
request.META["astra.duration_seconds"] = duration_seconds
372-
status_code = response.status_code if response is not None else 500
373-
if status_code >= 500:
374-
outcome = "server_error"
375-
elif status_code >= 400:
376-
outcome = "client_error"
377-
else:
378-
outcome = "success"
379-
380-
extra: dict[str, int | str] = {
381-
"event": "astra.http.access",
382-
"component": "http",
383-
"outcome": outcome,
384-
"http_status": status_code,
385-
"request_method": request.method,
386-
"request_path": request.path,
387-
"duration_ms": duration_ms,
388-
}
389-
390-
request_query = str(request.META.get("QUERY_STRING") or "").strip()
391-
if request_query:
392-
extra["request_query"] = request_query
393-
394-
request_id = str(request.META.get("HTTP_X_REQUEST_ID") or "").strip()
395-
if request_id:
396-
extra["request_id"] = request_id
397-
398-
client_ip = _request_client_ip(request)
399-
if client_ip:
400-
extra["client_ip"] = client_ip
401-
402-
user_id = try_get_username_from_user(request.user) if request.user.is_authenticated else None
403-
if user_id:
404-
extra["user_id"] = user_id
405-
406-
if error is not None:
407-
extra |= exception_log_fields(error)
408-
409-
access_atoms = _build_access_log_atoms(
410-
request,
411-
status_code=status_code,
412-
response=response,
413-
client_ip=client_ip,
414-
user_id=user_id,
415-
)
416-
access_logger.info(_ACCESS_LOG_TEMPLATE, access_atoms, extra=extra)
376+
if not _should_skip_access_log(request.path):
377+
status_code = response.status_code if response is not None else 500
378+
if status_code >= 500:
379+
outcome = "server_error"
380+
elif status_code >= 400:
381+
outcome = "client_error"
382+
else:
383+
outcome = "success"
384+
385+
extra: dict[str, int | str] = {
386+
"event": "astra.http.access",
387+
"component": "http",
388+
"outcome": outcome,
389+
"http_status": status_code,
390+
"request_method": request.method,
391+
"request_path": request.path,
392+
"duration_ms": duration_ms,
393+
}
394+
395+
request_query = str(request.META.get("QUERY_STRING") or "").strip()
396+
if request_query:
397+
extra["request_query"] = request_query
398+
399+
request_id = str(request.META.get("HTTP_X_REQUEST_ID") or "").strip()
400+
if request_id:
401+
extra["request_id"] = request_id
402+
403+
client_ip = _request_client_ip(request)
404+
if client_ip:
405+
extra["client_ip"] = client_ip
406+
407+
user_id = try_get_username_from_user(request.user) if request.user.is_authenticated else None
408+
if user_id:
409+
extra["user_id"] = user_id
410+
411+
if error is not None:
412+
extra |= exception_log_fields(error)
413+
414+
access_atoms = _build_access_log_atoms(
415+
request,
416+
status_code=status_code,
417+
response=response,
418+
client_ip=client_ip,
419+
user_id=user_id,
420+
)
421+
access_logger.info(_ACCESS_LOG_TEMPLATE, access_atoms, extra=extra)
417422

418423

419424
class LoginRequiredMiddleware:

astra_app/core/tests/test_logging_filters.py

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,14 @@ def test_astra_access_logger_uses_access_and_context_filters(self) -> None:
1313
configured_filters = settings.LOGGING["loggers"]["astra.access"]["filters"]
1414
self.assertEqual(
1515
configured_filters,
16-
["health_endpoint", "hetrix_access", "request_context"],
16+
["health_endpoint", "hetrix_access", "request_context", "quiet_request_paths"],
1717
)
1818

19+
def test_core_and_django_request_loggers_use_quiet_request_path_filter(self) -> None:
20+
self.assertIn("quiet_request_paths", settings.LOGGING["loggers"]["core"]["filters"])
21+
self.assertIn("quiet_request_paths", settings.LOGGING["loggers"]["django.request"]["filters"])
22+
self.assertIn("quiet_request_paths", settings.LOGGING["loggers"]["django.server"]["filters"])
23+
1924
def test_django_server_logger_suppresses_info_access_lines(self) -> None:
2025
self.assertEqual(settings.LOGGING["loggers"]["django.server"]["level"], "WARNING")
2126

@@ -138,6 +143,50 @@ def test_hetrix_access_filter_keeps_other_paths_and_agents(self) -> None:
138143
)
139144
self.assertTrue(filt.filter(non_hetrix_root))
140145

146+
def test_quiet_request_path_filter_drops_ci_tunnel_logs_from_message(self) -> None:
147+
from config.logging_filters import QuietRequestPathFilter
148+
149+
filt = QuietRequestPathFilter()
150+
151+
record_tunnel = logging.LogRecord(
152+
name="django.server",
153+
level=logging.INFO,
154+
pathname=__file__,
155+
lineno=1,
156+
msg='- - - [27/Jan/2026:10:49:08 +0000] "POST /_ci/envelope/ HTTP/1.1" 200 0 "-" "sentry"',
157+
args=(),
158+
exc_info=None,
159+
)
160+
self.assertFalse(filt.filter(record_tunnel))
161+
162+
record_other = logging.LogRecord(
163+
name="django.server",
164+
level=logging.INFO,
165+
pathname=__file__,
166+
lineno=1,
167+
msg='- - - [27/Jan/2026:10:49:08 +0000] "POST /users/ HTTP/1.1" 200 0 "-" "browser"',
168+
args=(),
169+
exc_info=None,
170+
)
171+
self.assertTrue(filt.filter(record_other))
172+
173+
def test_quiet_request_path_filter_drops_ci_tunnel_logs_from_request_context(self) -> None:
174+
from config.logging_filters import QuietRequestPathFilter
175+
176+
filt = QuietRequestPathFilter()
177+
record = logging.LogRecord(
178+
name="core.views_sentry",
179+
level=logging.WARNING,
180+
pathname=__file__,
181+
lineno=1,
182+
msg="Failed to forward Sentry envelope",
183+
args=(),
184+
exc_info=None,
185+
)
186+
record.request_path = "/_ci/envelope/"
187+
188+
self.assertFalse(filt.filter(record))
189+
141190
def test_request_context_filter_populates_searchable_log_fields(self) -> None:
142191
from config.logging_filters import RequestContextFilter
143192

astra_app/core/tests/test_middleware_freeipa_restore.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -382,3 +382,15 @@ def _boom(_request):
382382
self.assertEqual(extra["outcome"], "server_error")
383383
self.assertEqual(extra["error_type"], "RuntimeError")
384384
self.assertEqual(extra["error_message"], "middleware boom")
385+
386+
def test_structured_access_log_middleware_skips_sentry_tunnel_requests(self):
387+
factory = RequestFactory()
388+
request = factory.post("/_ci/envelope/", data=b"{}", content_type="application/x-sentry-envelope")
389+
request.user = AnonymousUser()
390+
391+
with patch("core.middleware.access_logger.info", autospec=True) as mocked_info:
392+
middleware = StructuredAccessLogMiddleware(lambda _req: HttpResponse(status=200))
393+
response = middleware(request)
394+
395+
self.assertEqual(response.status_code, 200)
396+
mocked_info.assert_not_called()

0 commit comments

Comments
 (0)