Repository: jrobichaud/django-structlog

Issue:
AttributeError "object has no attribute 'pk'" if "request.user" is not a Django model
In my Django application, users are not (always) associated with a model. In this case, I get an `AttributeError` in [`django_structlog/middlewares/request.py#L108`](https://github.com/jrobichaud/django-structlog/blob/master/django_structlog/middlewares/request.py#L108) because the `request.user` does not have an attribute `pk`.

A possible solution would be to use the `pk` attribute if it exists and otherwise just take the `str` of the `user`:

```diff
-            user_id = request.user.pk
+            user_id = getattr(request.user, "pk", str(request.user))
```

Discussion:

Patch:
diff --git a/django_structlog/middlewares/request.py b/django_structlog/middlewares/request.py
index 9ec0366..296ee72 100644
--- a/django_structlog/middlewares/request.py
+++ b/django_structlog/middlewares/request.py
@@ -105,7 +105,9 @@ class RequestMiddleware:
     @staticmethod
     def bind_user_id(request):
         if hasattr(request, "user") and request.user is not None:
-            user_id = request.user.pk
-            if isinstance(user_id, uuid.UUID):
-                user_id = str(user_id)
+            user_id = None
+            if hasattr(request.user, "pk"):
+                user_id = request.user.pk
+                if isinstance(user_id, uuid.UUID):
+                    user_id = str(user_id)
             structlog.contextvars.bind_contextvars(user_id=user_id)
diff --git a/docs/events.rst b/docs/events.rst
index 65dc376..e51026c 100644
--- a/docs/events.rst
+++ b/docs/events.rst
@@ -22,19 +22,24 @@ Request Bound Metadata
 
 These metadata are repeated on each log of the current request and will be also be repeated in all children Celery tasks.
 
-+------------------+------------------------------------------------------------------------------------------------------------------------+
-| Key              | Value                                                                                                                  |
-+==================+========================================================================================================================+
-| request_id       | UUID for the request or value of ``X-Request-ID`` HTTP header when provided                                            |
-+------------------+------------------------------------------------------------------------------------------------------------------------+
-| correlation_id   | value of ``X-Correlation-ID`` HTTP header when provided                                                                |
-+------------------+------------------------------------------------------------------------------------------------------------------------+
-| user_id          | user's id or None (requires `django.contrib.auth.middleware.AuthenticationMiddleware`_)                                |
-+                  +                                                                                                                        +
-|                  | `DRF <https://www.django-rest-framework.org/>`_: it will only be in ``request_finished`` and ``request_failed`` events |
-+------------------+------------------------------------------------------------------------------------------------------------------------+
-| ip               | request's ip                                                                                                           |
-+------------------+------------------------------------------------------------------------------------------------------------------------+
++------------------+---------------------------------------------------------------------------------------------------------------------------------+
+| Key              | Value                                                                                                                           |
++==================+=================================================================================================================================+
+| request_id       | UUID for the request or value of ``X-Request-ID`` HTTP header when provided                                                     |
++------------------+---------------------------------------------------------------------------------------------------------------------------------+
+| correlation_id   | value of ``X-Correlation-ID`` HTTP header when provided                                                                         |
++------------------+---------------------------------------------------------------------------------------------------------------------------------+
+| user_id          | user's id or None (requires `django.contrib.auth.middleware.AuthenticationMiddleware`_)                                         |
+|                  |                                                                                                                                 |
+|                  | `DRF <https://www.django-rest-framework.org/>`_: it will only be in ``request_finished`` and ``request_failed`` events          |
+|                  |                                                                                                                                 |
+|                  | If you need to override the bound ``user_id``, it has to be done in all three signals:                                          |
+|                  |  - :attr:`django_structlog.signals.bind_extra_request_metadata`                                                                 |
+|                  |  - :attr:`django_structlog.signals.bind_extra_request_finished_metadata`                                                        |
+|                  |  - :attr:`django_structlog.signals.bind_extra_request_failed_metadata`                                                          |
++------------------+---------------------------------------------------------------------------------------------------------------------------------+
+| ip               | request's ip                                                                                                                    |
++------------------+---------------------------------------------------------------------------------------------------------------------------------+
 
 To bind more metadata or override existing metadata from request see :ref:`django_signals`
 


Test patch:
diff --git a/test_app/tests/middlewares/test_request.py b/test_app/tests/middlewares/test_request.py
index d2e937d..82b98e0 100644
--- a/test_app/tests/middlewares/test_request.py
+++ b/test_app/tests/middlewares/test_request.py
@@ -188,6 +188,32 @@ class TestRequestMiddleware(TestCase):
         self.assertIsInstance(record.msg["user_id"], str)
         self.assertEqual(expected_uuid, record.msg["user_id"])
 
+    def test_process_request_user_without_id(self):
+        mock_response = Mock()
+        mock_response.status_code.return_value = 200
+
+        def get_response(_response):
+            with self.assertLogs(__name__, logging.INFO) as log_results:
+                self.logger.info("hello")
+            self.log_results = log_results
+            return mock_response
+
+        request = self.factory.get("/foo")
+
+        class SimpleUser:
+            pass
+
+        request.user = SimpleUser()
+        middleware = middlewares.RequestMiddleware(get_response)
+        middleware(request)
+
+        self.assertEqual(1, len(self.log_results.records))
+        record = self.log_results.records[0]
+
+        self.assertEqual("INFO", record.levelname)
+        self.assertIn("user_id", record.msg)
+        self.assertIsNone(record.msg["user_id"])
+
     def test_log_user_in_request_finished(self):
         mock_response = Mock()
         mock_response.status_code.return_value = 200