Repository: getsentry/sentry-python

Issue:
Split up breadcrumb and event logging handler, expose them
While migrating sentry.io to the new SDK this came up: There should be two separate handlers, one for creating events and one for creating breadcrumbs. Expose and document them as well.


Discussion:

Resolving patch:
diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py
--- a/sentry_sdk/integrations/logging.py
+++ b/sentry_sdk/integrations/logging.py
@@ -29,7 +29,24 @@ class LoggingIntegration(Integration):
     identifier = "logging"
 
     def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL):
-        self._handler = SentryHandler(level=level, event_level=event_level)
+        self._handler = None
+        self._breadcrumb_handler = None
+
+        if level is not None:
+            self._breadcrumb_handler = BreadcrumbHandler(level=level)
+
+        if event_level is not None:
+            self._handler = EventHandler(level=event_level)
+
+    def _handle_record(self, record):
+        if self._handler is not None and record.levelno >= self._handler.level:
+            self._handler.handle(record)
+
+        if (
+            self._breadcrumb_handler is not None
+            and record.levelno >= self._breadcrumb_handler.level
+        ):
+            self._breadcrumb_handler.handle(record)
 
     @staticmethod
     def setup_once():
@@ -43,39 +60,38 @@ def sentry_patched_callhandlers(self, record):
             if record.name not in _IGNORED_LOGGERS:
                 integration = Hub.current.get_integration(LoggingIntegration)
                 if integration is not None:
-                    integration._handler.handle(record)
+                    integration._handle_record(record)
             return old_callhandlers(self, record)
 
         logging.Logger.callHandlers = sentry_patched_callhandlers
 
 
-class SentryHandler(logging.Handler, object):
-    def __init__(self, level=DEFAULT_LEVEL, event_level=DEFAULT_EVENT_LEVEL, **kwargs):
-        logging.Handler.__init__(self, level, **kwargs)
-        if event_level is None:
-            self._event_level = None
-        else:
-            self._event_level = logging._checkLevel(event_level)
+def _can_record(record):
+    return record.name not in _IGNORED_LOGGERS
+
+
+def _breadcrumb_from_record(record):
+    return {
+        "ty": "log",
+        "level": _logging_to_event_level(record.levelname),
+        "category": record.name,
+        "message": record.message,
+        "timestamp": datetime.datetime.fromtimestamp(record.created),
+    }
+
 
+def _logging_to_event_level(levelname):
+    return {"critical": "fatal"}.get(levelname.lower(), levelname.lower())
+
+
+class EventHandler(logging.Handler, object):
     def emit(self, record):
         with capture_internal_exceptions():
             self.format(record)
             return self._emit(record)
 
-    def can_record(self, record):
-        return record.name not in _IGNORED_LOGGERS
-
-    def _breadcrumb_from_record(self, record):
-        return {
-            "ty": "log",
-            "level": self._logging_to_event_level(record.levelname),
-            "category": record.name,
-            "message": record.message,
-            "timestamp": datetime.datetime.fromtimestamp(record.created),
-        }
-
     def _emit(self, record):
-        if not self.can_record(record):
+        if not _can_record(record):
             return
 
         hub = Hub.current
@@ -83,35 +99,41 @@ def _emit(self, record):
         if integration is None:
             return
 
-        if self._should_create_event(record):
-            with capture_internal_exceptions():
-                hint = None
-                # exc_info might be None or (None, None, None)
-                if record.exc_info is not None and record.exc_info[0] is not None:
-                    event, hint = event_from_exception(
-                        record.exc_info,
-                        client_options=hub.client.options,
-                        mechanism={"type": "logging", "handled": True},
-                    )
-                else:
-                    event = {}
-
-                event["level"] = self._logging_to_event_level(record.levelname)
-                event["logger"] = record.name
-                event["logentry"] = {
-                    "message": to_string(record.msg),
-                    "params": record.args,
-                }
-
-                hub.capture_event(event, hint=hint)
+        # exc_info might be None or (None, None, None)
+        if record.exc_info is not None and record.exc_info[0] is not None:
+            event, hint = event_from_exception(
+                record.exc_info,
+                client_options=hub.client.options,
+                mechanism={"type": "logging", "handled": True},
+            )
+        else:
+            event = {}
+            hint = None
+
+        event["level"] = _logging_to_event_level(record.levelname)
+        event["logger"] = record.name
+        event["logentry"] = {"message": to_string(record.msg), "params": record.args}
 
+        hub.capture_event(event, hint=hint)
+
+
+# Legacy name
+SentryHandler = EventHandler
+
+
+class BreadcrumbHandler(logging.Handler, object):
+    def emit(self, record):
         with capture_internal_exceptions():
-            hub.add_breadcrumb(
-                self._breadcrumb_from_record(record), hint={"log_record": record}
-            )
+            self.format(record)
+            return self._emit(record)
 
-    def _logging_to_event_level(self, levelname):
-        return {"critical": "fatal"}.get(levelname.lower(), levelname.lower())
+    def _emit(self, record):
+        if not _can_record(record):
+            return
+
+        hub = Hub.current
+        integration = hub.get_integration(LoggingIntegration)
+        if integration is None:
+            return
 
-    def _should_create_event(self, record):
-        return self._event_level is not None and record.levelno >= self._event_level
+        hub.add_breadcrumb(_breadcrumb_from_record(record), hint={"log_record": record})


Test patch: