Skip to content

Commit 238cadf

Browse files
sl0thentr0pyclaude
andauthored
feat(logging): Separate ignore lists for events/breadcrumbs and sentry logs (#5698)
### Description Split `_IGNORED_LOGGERS` into two independent sets so that framework loggers silenced for events/breadcrumbs (e.g. `django.server`) can still be captured as Sentry Logs. - `_IGNORED_LOGGERS` controls `EventHandler` and `BreadcrumbHandler` - `_IGNORED_LOGGERS_SENTRY_LOGS` controls `SentryLogsHandler` - Add `ignore_logger_for_sentry_logs()` and `unignore_logger*()` helpers - Move `_can_record` into each handler class with its own ignore set - Split `_handle_record` into separate event and sentry-logs paths #### Issues * resolves: #5689 * resolves: PY-2146 Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6d33d36 commit 238cadf

File tree

3 files changed

+134
-16
lines changed

3 files changed

+134
-16
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ sentry-python-serverless*.zip
2121
.eggs
2222
venv
2323
.venv
24+
tox.venv
2425
.vscode/tags
2526
.pytest_cache
2627
.hypothesis

sentry_sdk/integrations/logging.py

Lines changed: 82 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,18 @@
5454
#
5555
# Note: Ignoring by logger name here is better than mucking with thread-locals.
5656
# We do not necessarily know whether thread-locals work 100% correctly in the user's environment.
57+
#
58+
# Events/breadcrumbs and Sentry Logs have separate ignore lists so that
59+
# framework loggers silenced for events (e.g. django.server) can still be
60+
# captured as Sentry Logs.
5761
_IGNORED_LOGGERS = set(
5862
["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"]
5963
)
6064

65+
_IGNORED_LOGGERS_SENTRY_LOGS = set(
66+
["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"]
67+
)
68+
6169

6270
def ignore_logger(
6371
name: str,
@@ -67,11 +75,47 @@ def ignore_logger(
6775
use this to prevent their actions being recorded as breadcrumbs. Exposed
6876
to users as a way to quiet spammy loggers.
6977
78+
This does **not** affect Sentry Logs — use
79+
:py:func:`ignore_logger_for_sentry_logs` for that.
80+
7081
:param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``).
7182
"""
7283
_IGNORED_LOGGERS.add(name)
7384

7485

86+
def ignore_logger_for_sentry_logs(
87+
name: str,
88+
) -> None:
89+
"""This disables recording as Sentry Logs calls to a logger of a
90+
specific name.
91+
92+
:param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``).
93+
"""
94+
_IGNORED_LOGGERS_SENTRY_LOGS.add(name)
95+
96+
97+
def unignore_logger(
98+
name: str,
99+
) -> None:
100+
"""Reverts a previous :py:func:`ignore_logger` call, re-enabling
101+
recording of breadcrumbs and events for the named logger.
102+
103+
:param name: The name of the logger to unignore.
104+
"""
105+
_IGNORED_LOGGERS.discard(name)
106+
107+
108+
def unignore_logger_for_sentry_logs(
109+
name: str,
110+
) -> None:
111+
"""Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call,
112+
re-enabling recording of Sentry Logs for the named logger.
113+
114+
:param name: The name of the logger to unignore.
115+
"""
116+
_IGNORED_LOGGERS_SENTRY_LOGS.discard(name)
117+
118+
75119
class LoggingIntegration(Integration):
76120
identifier = "logging"
77121

@@ -104,6 +148,7 @@ def _handle_record(self, record: "LogRecord") -> None:
104148
):
105149
self._breadcrumb_handler.handle(record)
106150

151+
def _handle_sentry_logs_record(self, record: "LogRecord") -> None:
107152
if (
108153
self._sentry_logs_handler is not None
109154
and record.levelno >= self._sentry_logs_handler.level
@@ -118,6 +163,7 @@ def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any":
118163
# keeping a local reference because the
119164
# global might be discarded on shutdown
120165
ignored_loggers = _IGNORED_LOGGERS
166+
ignored_loggers_sentry_logs = _IGNORED_LOGGERS_SENTRY_LOGS
121167

122168
try:
123169
return old_callhandlers(self, record)
@@ -126,15 +172,25 @@ def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any":
126172
# the integration. Otherwise we have a high chance of getting
127173
# into a recursion error when the integration is resolved
128174
# (this also is slower).
129-
if (
130-
ignored_loggers is not None
131-
and record.name.strip() not in ignored_loggers
132-
):
175+
name = record.name.strip()
176+
177+
handle_events = (
178+
ignored_loggers is not None and name not in ignored_loggers
179+
)
180+
handle_sentry_logs = (
181+
ignored_loggers_sentry_logs is not None
182+
and name not in ignored_loggers_sentry_logs
183+
)
184+
185+
if handle_events or handle_sentry_logs:
133186
integration = sentry_sdk.get_client().get_integration(
134187
LoggingIntegration
135188
)
136189
if integration is not None:
137-
integration._handle_record(record)
190+
if handle_events:
191+
integration._handle_record(record)
192+
if handle_sentry_logs:
193+
integration._handle_sentry_logs_record(record)
138194

139195
logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore
140196

@@ -170,13 +226,6 @@ class _BaseHandler(logging.Handler):
170226
)
171227
)
172228

173-
def _can_record(self, record: "LogRecord") -> bool:
174-
"""Prevents ignored loggers from recording"""
175-
for logger in _IGNORED_LOGGERS:
176-
if fnmatch(record.name.strip(), logger):
177-
return False
178-
return True
179-
180229
def _logging_to_event_level(self, record: "LogRecord") -> str:
181230
return LOGGING_TO_EVENT_LEVEL.get(
182231
record.levelno, record.levelname.lower() if record.levelname else ""
@@ -198,6 +247,13 @@ class EventHandler(_BaseHandler):
198247
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
199248
"""
200249

250+
def _can_record(self, record: "LogRecord") -> bool:
251+
"""Prevents ignored loggers from recording"""
252+
for logger in _IGNORED_LOGGERS:
253+
if fnmatch(record.name.strip(), logger):
254+
return False
255+
return True
256+
201257
def emit(self, record: "LogRecord") -> "Any":
202258
with capture_internal_exceptions():
203259
self.format(record)
@@ -290,6 +346,13 @@ class BreadcrumbHandler(_BaseHandler):
290346
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
291347
"""
292348

349+
def _can_record(self, record: "LogRecord") -> bool:
350+
"""Prevents ignored loggers from recording"""
351+
for logger in _IGNORED_LOGGERS:
352+
if fnmatch(record.name.strip(), logger):
353+
return False
354+
return True
355+
293356
def emit(self, record: "LogRecord") -> "Any":
294357
with capture_internal_exceptions():
295358
self.format(record)
@@ -321,6 +384,13 @@ class SentryLogsHandler(_BaseHandler):
321384
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
322385
"""
323386

387+
def _can_record(self, record: "LogRecord") -> bool:
388+
"""Prevents ignored loggers from recording"""
389+
for logger in _IGNORED_LOGGERS_SENTRY_LOGS:
390+
if fnmatch(record.name.strip(), logger):
391+
return False
392+
return True
393+
324394
def emit(self, record: "LogRecord") -> "Any":
325395
with capture_internal_exceptions():
326396
self.format(record)

tests/integrations/logging/test_logging.py

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55

66
from sentry_sdk import get_client
77
from sentry_sdk.consts import VERSION
8-
from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
8+
from sentry_sdk.integrations.logging import (
9+
LoggingIntegration,
10+
ignore_logger,
11+
ignore_logger_for_sentry_logs,
12+
unignore_logger,
13+
unignore_logger_for_sentry_logs,
14+
)
915
from tests.test_logs import envelopes_to_logs
1016

1117
other_logger = logging.getLogger("testfoo")
@@ -222,34 +228,37 @@ def test_logging_captured_warnings(sentry_init, capture_events, recwarn):
222228
assert str(recwarn[0].message) == "third"
223229

224230

225-
def test_ignore_logger(sentry_init, capture_events):
231+
def test_ignore_logger(sentry_init, capture_events, request):
226232
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
227233
events = capture_events()
228234

229235
ignore_logger("testfoo")
236+
request.addfinalizer(lambda: unignore_logger("testfoo"))
230237

231238
other_logger.error("hi")
232239

233240
assert not events
234241

235242

236-
def test_ignore_logger_whitespace_padding(sentry_init, capture_events):
243+
def test_ignore_logger_whitespace_padding(sentry_init, capture_events, request):
237244
"""Here we test insensitivity to whitespace padding of ignored loggers"""
238245
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
239246
events = capture_events()
240247

241248
ignore_logger("testfoo")
249+
request.addfinalizer(lambda: unignore_logger("testfoo"))
242250

243251
padded_logger = logging.getLogger(" testfoo ")
244252
padded_logger.error("hi")
245253
assert not events
246254

247255

248-
def test_ignore_logger_wildcard(sentry_init, capture_events):
256+
def test_ignore_logger_wildcard(sentry_init, capture_events, request):
249257
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
250258
events = capture_events()
251259

252260
ignore_logger("testfoo.*")
261+
request.addfinalizer(lambda: unignore_logger("testfoo.*"))
253262

254263
nested_logger = logging.getLogger("testfoo.submodule")
255264

@@ -262,6 +271,44 @@ def test_ignore_logger_wildcard(sentry_init, capture_events):
262271
assert event["logentry"]["formatted"] == "hi"
263272

264273

274+
def test_ignore_logger_does_not_affect_sentry_logs(
275+
sentry_init, capture_envelopes, request
276+
):
277+
"""ignore_logger should suppress events/breadcrumbs but not Sentry Logs."""
278+
sentry_init(enable_logs=True)
279+
envelopes = capture_envelopes()
280+
281+
ignore_logger("testfoo")
282+
request.addfinalizer(lambda: unignore_logger("testfoo"))
283+
284+
other_logger.error("hi")
285+
get_client().flush()
286+
287+
logs = envelopes_to_logs(envelopes)
288+
assert len(logs) == 1
289+
assert logs[0]["body"] == "hi"
290+
291+
292+
def test_ignore_logger_for_sentry_logs(sentry_init, capture_envelopes, request):
293+
"""ignore_logger_for_sentry_logs should suppress Sentry Logs but not events."""
294+
sentry_init(enable_logs=True)
295+
envelopes = capture_envelopes()
296+
297+
ignore_logger_for_sentry_logs("testfoo")
298+
request.addfinalizer(lambda: unignore_logger_for_sentry_logs("testfoo"))
299+
300+
other_logger.error("hi")
301+
get_client().flush()
302+
303+
# Event should still be captured
304+
event_envelopes = [e for e in envelopes if e.items[0].type == "event"]
305+
assert len(event_envelopes) == 1
306+
307+
# But no Sentry Logs
308+
logs = envelopes_to_logs(envelopes)
309+
assert len(logs) == 0
310+
311+
265312
def test_logging_dictionary_interpolation(sentry_init, capture_events):
266313
"""Here we test an entire dictionary being interpolated into the log message."""
267314
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)

0 commit comments

Comments
 (0)