diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fda530..cec8c42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ Changelog ========= +## TBD (Unreleased) + +### Enhancements + +* Add support for `level` parameter in `BugsnagHandler` constructor and `Client.log_handler()` method. + Allows setting the minimum logging level during handler initialization. + Fully backward compatible - existing code continues to work unchanged. + [#249](https://github.com/bugsnag/bugsnag-python/pull/249) + ## v4.9.0 (2026-04-21) ### Enhancements diff --git a/bugsnag/client.py b/bugsnag/client.py index 9717ce9..00dba65 100644 --- a/bugsnag/client.py +++ b/bugsnag/client.py @@ -1,4 +1,5 @@ import builtins +import logging import sys import threading import warnings @@ -234,9 +235,12 @@ def should_deliver(self, event: Event) -> bool: def log_handler( self, - extra_fields: Optional[List[str]] = None + extra_fields: Optional[Dict[str, List[str]]] = None, + level: int = logging.NOTSET ) -> BugsnagHandler: - return BugsnagHandler(client=self, extra_fields=extra_fields) + return BugsnagHandler( + client=self, extra_fields=extra_fields, level=level + ) @property def feature_flags(self) -> List[FeatureFlag]: diff --git a/bugsnag/handlers.py b/bugsnag/handlers.py index ded43a1..f741dfa 100644 --- a/bugsnag/handlers.py +++ b/bugsnag/handlers.py @@ -9,11 +9,11 @@ class BugsnagHandler(logging.Handler, object): - def __init__(self, client=None, extra_fields=None): + def __init__(self, client=None, extra_fields=None, level=logging.NOTSET): """ Creates a new handler which sends records to Bugsnag """ - super(BugsnagHandler, self).__init__() + super(BugsnagHandler, self).__init__(level=level) self.client = client self.custom_metadata_fields = extra_fields self.callbacks = [self.extract_default_metadata, diff --git a/tests/test_handlers.py b/tests/test_handlers.py index ed38564..1460cd5 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -689,3 +689,83 @@ def test_log_filter_leaves_breadcrumbs_when_handler_has_no_level(self): assert breadcrumb.message == 'Everything is not fine' assert breadcrumb.type == BreadcrumbType.LOG assert breadcrumb.metadata == {'logLevel': 'ERROR'} + + def test_handler_level_via_constructor(self): + """Test setting level via constructor parameter""" + handler = BugsnagHandler(level=logging.ERROR) + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + + # Should NOT report (below handler level) + logger.warning('This is just a warning') + self.assertSentReportCount(0) + + # Should report (at handler level) + logger.error('This is an error') + logger.removeHandler(handler) + self.assertSentReportCount(1) + + json_body = self.server.events_received[0]['json_body'] + event = json_body['events'][0] + exception = event['exceptions'][0] + self.assertEqual('LogERROR', exception['errorClass']) + + def test_handler_level_default_notset(self): + """Test default level is NOTSET""" + handler = BugsnagHandler() + self.assertEqual(handler.level, logging.NOTSET) + + def test_handler_level_with_client_and_extra_fields(self): + """Test level parameter works with other parameters""" + client = Client( + api_key='abcdef', + endpoint=self.server.events_url, + session_endpoint=self.server.sessions_url, + asynchronous=False + ) + handler = BugsnagHandler( + client=client, + extra_fields={'fruit': ['grapes']}, + level=logging.WARNING + ) + self.assertEqual(handler.level, logging.WARNING) + self.assertEqual(handler.client, client) + self.assertEqual(handler.custom_metadata_fields, {'fruit': ['grapes']}) + + def test_client_log_handler_with_level(self): + """Test client.log_handler() with level parameter""" + client = Client( + api_key='abcdef', + endpoint=self.server.events_url, + session_endpoint=self.server.sessions_url, + asynchronous=False + ) + + with scoped_logger() as logger: + handler = client.log_handler(level=logging.ERROR) + logger.setLevel(logging.DEBUG) + logger.addHandler(handler) + + logger.warning('Warning message') + self.assertSentReportCount(0) + + logger.error('Error message') + self.assertSentReportCount(1) + + json_body = self.server.events_received[0]['json_body'] + event = json_body['events'][0] + exception = event['exceptions'][0] + self.assertEqual('LogERROR', exception['errorClass']) + + def test_backward_compatibility_no_level(self): + """Ensure backward compatibility when level not specified""" + # All these should work without breaking + h1 = BugsnagHandler() + h2 = BugsnagHandler(client=None) + h3 = BugsnagHandler(extra_fields={'test': ['field']}) + h4 = BugsnagHandler(client=None, extra_fields={'test': ['field']}) + + # All should have default NOTSET level + for handler in [h1, h2, h3, h4]: + self.assertEqual(handler.level, logging.NOTSET)