Repository: bugsnag/bugsnag-python

Issue:
Unable to set grouping_hash using `logger.<level>()` methods
<!-- Before creating, please check if somebody else has already reported your feature request. -->

### Description

There is no way to change the grouping hash value when using logger methods and the BugsnagHandler to send event.

In a general manner, this module API does not make the best use of the standard logging API. That makes it very hard to integrate with existing programs and packages.

**Describe the solution you'd like**

```python
try:
     # Raising code
except:
    logger.exception("This happened", extra={'groupingHash': '<hash value>', ...})
    # Handle the exception.
```

Or to make it even less vendor specific and provide the ability to adopt Bugsnag without modifying a single line of code, while still grouping as one needs, e.g. if the grouping key to use is already available in the log record extras:

```python
import logging
root_logger = logging.getLogger()
# Note that noone should have to write such code by hand (see alternatives section below)
root_logger.addHandler(BugsnagHandler(grouping_hash='<extra-key-name>', ...))

# And later on in the code
try:
     # Raising code
except:
    logger.exception('This happened', extra={'<extra-key-name>': '<hash value>', ...})
    # Handle the exception
```

**Describe alternatives you've considered**

One way to override the `grouping_hash` so far is to use that  keyword parameter of the `bugsnag.notify()` function.
This API is really not great as it imposes to use one that is provider specific in your code when there is no need to.

Another way would be, I believe, to use a [callback](https://github.com/bugsnag/bugsnag-python/blob/master/bugsnag/handlers.py#L80). The problem with this approach is that, as currently implemented, it is not possible to pass callbacks if one wants to configure logging using a standard [configuration file](https://docs.python.org/3/library/logging.config.html). One has to make explicit calls to the `BugsnagHandler.add_callback()` method somewhere in its code. Why would you write code, to do something that should be done through configuration ?

**Additional context**
<!--Add any other context about the feature request here.-->

Comments:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fc7866..517c2f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,11 @@ Changelog
 
 ## TBD
 
+### Enhancements
+
+* Allows changing the grouping hash when using `BugsnagHandler` via the logger methods' `extra` keyword argument
+  [#334](https://github.com/bugsnag/bugsnag-python/pull/334)
+
 ### Bug fixes
 
 * Fixes one of the fields being mistakenly replaced with `[RECURSIVE]` when encoding a list or dictionary with identical siblings but no recursion.
@@ -11,6 +16,7 @@ Changelog
 * Fix the ignore class list not accounting for nested classes
   [#342](https://github.com/bugsnag/bugsnag-python/pull/342)
 
+
 ## v4.4.0 (2023-02-21)
 
 ### Enhancements
diff --git a/bugsnag/handlers.py b/bugsnag/handlers.py
index e48b91a..ded43a1 100644
--- a/bugsnag/handlers.py
+++ b/bugsnag/handlers.py
@@ -18,7 +18,8 @@ class BugsnagHandler(logging.Handler, object):
         self.custom_metadata_fields = extra_fields
         self.callbacks = [self.extract_default_metadata,
                           self.extract_custom_metadata,
-                          self.extract_severity]
+                          self.extract_severity,
+                          self.extract_grouping_hash]
 
     def emit(self, record: LogRecord):
         """
@@ -113,6 +114,13 @@ class BugsnagHandler(logging.Handler, object):
         else:
             options['severity'] = 'info'
 
+    def extract_grouping_hash(self, record: LogRecord, options: Dict):
+        """
+        Add the grouping_hash from a log record to the options
+        """
+        if 'groupingHash' in record.__dict__:
+            options['grouping_hash'] = record.__dict__['groupingHash']
+
     def extract_custom_metadata(self, record: LogRecord, options: Dict):
         """
         Append the contents of selected fields of a record to the metadata


Test patch:
diff --git a/tests/test_handlers.py b/tests/test_handlers.py
index e7e0d30..8aa4ebe 100644
--- a/tests/test_handlers.py
+++ b/tests/test_handlers.py
@@ -457,6 +457,18 @@ class HandlersTest(IntegrationTest):
         self.assertEqual(event['metaData']['custom'],
                          {'exception': 'metadata'})
 
+    @use_client_logger
+    def test_logging_grouping_hash(self, handler, logger):
+        logger.info("This happened", extra={'groupingHash': '<hash value>'})
+
+        self.assertSentReportCount(1)
+        json_body = self.server.received[0]['json_body']
+        event = json_body['events'][0]
+        exception = event['exceptions'][0]
+
+        self.assertEqual(exception['message'], 'This happened')
+        self.assertEqual(event['groupingHash'], '<hash value>')
+
     @use_client_logger
     def test_log_filter_leaves_breadcrumbs_for_logs_below_report_level(
         self,