Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ sentry-python-serverless*.zip
.eggs
venv
.venv
tox.venv
.vscode/tags
.pytest_cache
.hypothesis
Expand Down
94 changes: 82 additions & 12 deletions sentry_sdk/integrations/logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,18 @@
#
# Note: Ignoring by logger name here is better than mucking with thread-locals.
# We do not necessarily know whether thread-locals work 100% correctly in the user's environment.
#
# Events/breadcrumbs and Sentry Logs have separate ignore lists so that
# framework loggers silenced for events (e.g. django.server) can still be
# captured as Sentry Logs.
_IGNORED_LOGGERS = set(
["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"]
)

_IGNORED_LOGGERS_SENTRY_LOGS = set(
["sentry_sdk.errors", "urllib3.connectionpool", "urllib3.connection"]
)


def ignore_logger(
name: str,
Expand All @@ -67,11 +75,47 @@ def ignore_logger(
use this to prevent their actions being recorded as breadcrumbs. Exposed
to users as a way to quiet spammy loggers.

This does **not** affect Sentry Logs — use
:py:func:`ignore_logger_for_sentry_logs` for that.

:param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``).
"""
_IGNORED_LOGGERS.add(name)


def ignore_logger_for_sentry_logs(
name: str,
) -> None:
"""This disables recording as Sentry Logs calls to a logger of a
specific name.

:param name: The name of the logger to ignore (same string you would pass to ``logging.getLogger``).
"""
_IGNORED_LOGGERS_SENTRY_LOGS.add(name)


def unignore_logger(
name: str,
) -> None:
"""Reverts a previous :py:func:`ignore_logger` call, re-enabling
recording of breadcrumbs and events for the named logger.

:param name: The name of the logger to unignore.
"""
_IGNORED_LOGGERS.discard(name)


def unignore_logger_for_sentry_logs(
name: str,
) -> None:
"""Reverts a previous :py:func:`ignore_logger_for_sentry_logs` call,
re-enabling recording of Sentry Logs for the named logger.

:param name: The name of the logger to unignore.
"""
_IGNORED_LOGGERS_SENTRY_LOGS.discard(name)


class LoggingIntegration(Integration):
identifier = "logging"

Expand Down Expand Up @@ -104,6 +148,7 @@ def _handle_record(self, record: "LogRecord") -> None:
):
self._breadcrumb_handler.handle(record)

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

try:
return old_callhandlers(self, record)
Expand All @@ -126,15 +172,25 @@ def sentry_patched_callhandlers(self: "Any", record: "LogRecord") -> "Any":
# the integration. Otherwise we have a high chance of getting
# into a recursion error when the integration is resolved
# (this also is slower).
if (
ignored_loggers is not None
and record.name.strip() not in ignored_loggers
):
name = record.name.strip()

handle_events = (
ignored_loggers is not None and name not in ignored_loggers
)
handle_sentry_logs = (
ignored_loggers_sentry_logs is not None
and name not in ignored_loggers_sentry_logs
)

if handle_events or handle_sentry_logs:
integration = sentry_sdk.get_client().get_integration(
LoggingIntegration
)
if integration is not None:
integration._handle_record(record)
if handle_events:
integration._handle_record(record)
if handle_sentry_logs:
integration._handle_sentry_logs_record(record)

logging.Logger.callHandlers = sentry_patched_callhandlers # type: ignore

Expand Down Expand Up @@ -170,13 +226,6 @@ class _BaseHandler(logging.Handler):
)
)

def _can_record(self, record: "LogRecord") -> bool:
"""Prevents ignored loggers from recording"""
for logger in _IGNORED_LOGGERS:
if fnmatch(record.name.strip(), logger):
return False
return True

def _logging_to_event_level(self, record: "LogRecord") -> str:
return LOGGING_TO_EVENT_LEVEL.get(
record.levelno, record.levelname.lower() if record.levelname else ""
Expand All @@ -198,6 +247,13 @@ class EventHandler(_BaseHandler):
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
"""

def _can_record(self, record: "LogRecord") -> bool:
"""Prevents ignored loggers from recording"""
for logger in _IGNORED_LOGGERS:
if fnmatch(record.name.strip(), logger):
return False
return True

def emit(self, record: "LogRecord") -> "Any":
with capture_internal_exceptions():
self.format(record)
Expand Down Expand Up @@ -290,6 +346,13 @@ class BreadcrumbHandler(_BaseHandler):
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
"""

def _can_record(self, record: "LogRecord") -> bool:
"""Prevents ignored loggers from recording"""
for logger in _IGNORED_LOGGERS:
if fnmatch(record.name.strip(), logger):
return False
return True

def emit(self, record: "LogRecord") -> "Any":
with capture_internal_exceptions():
self.format(record)
Expand Down Expand Up @@ -321,6 +384,13 @@ class SentryLogsHandler(_BaseHandler):
Note that you do not have to use this class if the logging integration is enabled, which it is by default.
"""

def _can_record(self, record: "LogRecord") -> bool:
"""Prevents ignored loggers from recording"""
for logger in _IGNORED_LOGGERS_SENTRY_LOGS:
if fnmatch(record.name.strip(), logger):
return False
return True

def emit(self, record: "LogRecord") -> "Any":
with capture_internal_exceptions():
self.format(record)
Expand Down
55 changes: 51 additions & 4 deletions tests/integrations/logging/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@

from sentry_sdk import get_client
from sentry_sdk.consts import VERSION
from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger
from sentry_sdk.integrations.logging import (
LoggingIntegration,
ignore_logger,
ignore_logger_for_sentry_logs,
unignore_logger,
unignore_logger_for_sentry_logs,
)
from tests.test_logs import envelopes_to_logs

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


def test_ignore_logger(sentry_init, capture_events):
def test_ignore_logger(sentry_init, capture_events, request):
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()

ignore_logger("testfoo")
request.addfinalizer(lambda: unignore_logger("testfoo"))

other_logger.error("hi")

assert not events


def test_ignore_logger_whitespace_padding(sentry_init, capture_events):
def test_ignore_logger_whitespace_padding(sentry_init, capture_events, request):
"""Here we test insensitivity to whitespace padding of ignored loggers"""
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()

ignore_logger("testfoo")
request.addfinalizer(lambda: unignore_logger("testfoo"))

padded_logger = logging.getLogger(" testfoo ")
padded_logger.error("hi")
assert not events


def test_ignore_logger_wildcard(sentry_init, capture_events):
def test_ignore_logger_wildcard(sentry_init, capture_events, request):
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
events = capture_events()

ignore_logger("testfoo.*")
request.addfinalizer(lambda: unignore_logger("testfoo.*"))

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

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


def test_ignore_logger_does_not_affect_sentry_logs(
sentry_init, capture_envelopes, request
):
"""ignore_logger should suppress events/breadcrumbs but not Sentry Logs."""
sentry_init(enable_logs=True)
envelopes = capture_envelopes()

ignore_logger("testfoo")
request.addfinalizer(lambda: unignore_logger("testfoo"))

other_logger.error("hi")
get_client().flush()

logs = envelopes_to_logs(envelopes)
assert len(logs) == 1
assert logs[0]["body"] == "hi"


def test_ignore_logger_for_sentry_logs(sentry_init, capture_envelopes, request):
"""ignore_logger_for_sentry_logs should suppress Sentry Logs but not events."""
sentry_init(enable_logs=True)
envelopes = capture_envelopes()

ignore_logger_for_sentry_logs("testfoo")
request.addfinalizer(lambda: unignore_logger_for_sentry_logs("testfoo"))

other_logger.error("hi")
get_client().flush()

# Event should still be captured
event_envelopes = [e for e in envelopes if e.items[0].type == "event"]
assert len(event_envelopes) == 1

# But no Sentry Logs
logs = envelopes_to_logs(envelopes)
assert len(logs) == 0


def test_logging_dictionary_interpolation(sentry_init, capture_events):
"""Here we test an entire dictionary being interpolated into the log message."""
sentry_init(integrations=[LoggingIntegration()], default_integrations=False)
Expand Down
Loading