Repository: slackapi/bolt-python

Issue:
The request-scoped WebClient doesn't use the provided logger
Bolt is creating a `WebClient` instance for each request its handling here:

https://github.com/slackapi/bolt-python/blob/3ad0788451b0dd2ba88884712ba96d26557e40f8/slack_bolt/app/app.py#L1376

It does not pass the `logger` param.

This means that all requests that are logged by that client won't use the logger provided to the App.


Discussion:
Hi @dirleyrls-kake thanks for bringing this up 💯 

The line above the  initialization of `client_per_request` we assign the Bolt app logger to the `context` of the request, from my perspective it makes sense to have a logger for Bolt and another for the WebClient since they are separate libraries 🤔 ([slack-bolt](https://pypi.org/project/slack-bolt/), [slack-sdk](https://pypi.org/project/slack-sdk/)), what would be your expected behavior and why?
@WilliamBergamin , thanks for the reply.

You are right they are separate libraries, but this particular `WebClient` is an internal resource created & owned by Slack Bolt. If I configure a custom logger while setting up Slack Bolt, I expect this custom logger to be propagated to every resource that Slack Bolt creates internally.

In my particular use-case, I want to customize all Slack-related loggers to include a prefix (`[slack]`). This expectation extends to the logger used by the `WebClient` too, since its an internal resource.

I searched the docs for a way to achieve this but found none. I noticed in some places Bolt tries to propagate the logger, eg.: https://github.com/slackapi/bolt-python/blob/98f0b46c08cca6536e4944b8e42b8e1075ab0d26/slack_bolt/oauth/oauth_flow.py#L76 and https://github.com/slackapi/bolt-python/blob/98f0b46c08cca6536e4944b8e42b8e1075ab0d26/slack_bolt/app/app.py#L239.

Currently I had to overload some private method to customize this logger:

```python
class LoggerRespectingBoltApp(App):
    def _init_context(self, req: BoltRequest):
        super()._init_context(req)
        req.context["client"]._logger = logger
```

If you don't want to propagate the logger by default could you at least provide a public API and some documentation about how to do it?
I looked more into this and and I think it is a bug 🤔 

Looking back it seems like the intention of #246 was to make this possible and I believe it was working for a while, until #714 which creates a copy of the client for each request and did not carry over the logger set up in the app client instance

We will need to make changes to the underlying sdk in order to expose the client logger



Patch:
diff --git a/requirements.txt b/requirements.txt
index f0b0cf98..c8d66106 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,1 +1,1 @@
-slack_sdk>=3.33.5,<4
+slack_sdk>=3.35.0,<4
diff --git a/slack_bolt/app/app.py b/slack_bolt/app/app.py
index 3d5532b7..69da0a0d 100644
--- a/slack_bolt/app/app.py
+++ b/slack_bolt/app/app.py
@@ -1381,6 +1381,7 @@ def _init_context(self, req: BoltRequest):
             proxy=self._client.proxy,
             headers=self._client.headers,
             team_id=req.context.team_id,
+            logger=self._client.logger,
             retry_handlers=self._client.retry_handlers.copy() if self._client.retry_handlers is not None else None,
         )
         req.context["client"] = client_per_request
diff --git a/slack_bolt/app/async_app.py b/slack_bolt/app/async_app.py
index 50a36e5d..3fcc3d95 100644
--- a/slack_bolt/app/async_app.py
+++ b/slack_bolt/app/async_app.py
@@ -1413,6 +1413,7 @@ def _init_context(self, req: AsyncBoltRequest):
             trust_env_in_session=self._async_client.trust_env_in_session,
             headers=self._async_client.headers,
             team_id=req.context.team_id,
+            logger=self._async_client.logger,
             retry_handlers=(
                 self._async_client.retry_handlers.copy() if self._async_client.retry_handlers is not None else None
             ),


Test patch:
diff --git a/tests/scenario_tests/test_web_client_customization.py b/tests/scenario_tests/test_web_client_customization.py
index 3caf536e..f78c7747 100644
--- a/tests/scenario_tests/test_web_client_customization.py
+++ b/tests/scenario_tests/test_web_client_customization.py
@@ -73,7 +73,7 @@ def listener(ack, client):
 
     def test_web_client_logger_is_default_app_logger(self):
         app = App(token=self.valid_token, signing_secret=self.signing_secret, token_verification_enabled=False)
-        assert app.client._logger == app.logger  # TODO: use client.logger when available
+        assert app.client.logger == app.logger
 
     def test_web_client_logger_is_app_logger(self):
         app = App(
@@ -82,8 +82,65 @@ def test_web_client_logger_is_app_logger(self):
             logger=self.test_logger,
             token_verification_enabled=False,
         )
-        assert app.client._logger == app.logger  # TODO: use client.logger when available
-        assert app.client._logger == self.test_logger  # TODO: use client.logger when available
+        assert app.client.logger == app.logger
+        assert app.client.logger == self.test_logger
+
+    def test_default_web_client_uses_bolt_framework_logger(self):
+        app = App(token=self.valid_token, signing_secret=self.signing_secret, token_verification_enabled=False)
+        app.client.base_url = self.mock_api_server_base_url
+
+        @app.action("a")
+        def listener(ack, client: WebClient):
+            assert client.logger == app.logger
+            ack()
+
+        request = self.build_valid_request()
+        response = app.dispatch(request)
+        assert response.status == 200
+        assert response.body == ""
+        assert_auth_test_count(self, 1)
+
+    def test_default_web_client_uses_bolt_app_custom_logger(self):
+        app = App(
+            token=self.valid_token,
+            signing_secret=self.signing_secret,
+            token_verification_enabled=False,
+            logger=self.test_logger,
+        )
+        app.client.base_url = self.mock_api_server_base_url
+
+        assert app.client.logger == app.logger
+
+        @app.action("a")
+        def listener(ack, client: WebClient):
+            assert client.logger == app.logger
+            assert client.logger == self.test_logger
+            ack()
+
+        request = self.build_valid_request()
+        response = app.dispatch(request)
+        assert response.status == 200
+        assert response.body == ""
+        assert_auth_test_count(self, 1)
+
+    def test_custom_web_client_logger_is_used_instead_of_bolt_app_logger(self):
+        web_client = WebClient(token=self.valid_token, base_url=self.mock_api_server_base_url, logger=self.test_logger)
+        app = App(
+            client=web_client,
+            signing_secret=self.signing_secret,
+        )
+
+        @app.action("a")
+        def listener(ack, client: WebClient):
+            assert client.logger == self.test_logger
+            assert app.logger != self.test_logger
+            ack()
+
+        request = self.build_valid_request()
+        response = app.dispatch(request)
+        assert response.status == 200
+        assert response.body == ""
+        assert_auth_test_count(self, 1)
 
 
 block_actions_body = {
diff --git a/tests/scenario_tests_async/test_web_client_customization.py b/tests/scenario_tests_async/test_web_client_customization.py
index fd95ed5a..c9b42a61 100644
--- a/tests/scenario_tests_async/test_web_client_customization.py
+++ b/tests/scenario_tests_async/test_web_client_customization.py
@@ -91,12 +91,71 @@ async def listener(ack, client):
 
     def test_web_client_logger_is_default_app_logger(self):
         app = AsyncApp(token=self.valid_token, signing_secret=self.signing_secret)
-        assert app.client._logger == app.logger  # TODO: use client.logger when available
+        assert app.client.logger == app.logger
 
     def test_web_client_logger_is_app_logger(self):
         app = AsyncApp(token=self.valid_token, signing_secret=self.signing_secret, logger=self.test_logger)
-        assert app.client._logger == app.logger  # TODO: use client.logger when available
-        assert app.client._logger == self.test_logger  # TODO: use client.logger when available
+        assert app.client.logger == app.logger
+        assert app.client.logger == self.test_logger
+
+    @pytest.mark.asyncio
+    async def test_default_web_client_uses_bolt_framework_logger(self):
+        app = AsyncApp(token=self.valid_token, signing_secret=self.signing_secret)
+        app.client.base_url = self.mock_api_server_base_url
+
+        @app.action("a")
+        async def listener(ack, client: AsyncWebClient):
+            assert client.logger == app.logger
+            await ack()
+
+        request = self.build_valid_request()
+        response = await app.async_dispatch(request)
+        assert response.status == 200
+        assert response.body == ""
+        await assert_auth_test_count_async(self, 1)
+
+    @pytest.mark.asyncio
+    async def test_default_web_client_uses_bolt_app_custom_logger(self):
+        app = AsyncApp(
+            token=self.valid_token,
+            signing_secret=self.signing_secret,
+            logger=self.test_logger,
+        )
+        app.client.base_url = self.mock_api_server_base_url
+
+        assert app.client.logger == app.logger
+
+        @app.action("a")
+        async def listener(ack, client: AsyncWebClient):
+            assert client.logger == app.logger
+            assert client.logger == self.test_logger
+            await ack()
+
+        request = self.build_valid_request()
+        response = await app.async_dispatch(request)
+        assert response.status == 200
+        assert response.body == ""
+        await assert_auth_test_count_async(self, 1)
+
+    @pytest.mark.asyncio
+    async def test_custom_web_client_logger_is_used_instead_of_bolt_app_logger(self):
+        web_client = AsyncWebClient(token=self.valid_token, base_url=self.mock_api_server_base_url, logger=self.test_logger)
+        app = AsyncApp(
+            client=web_client,
+            signing_secret=self.signing_secret,
+        )
+
+        @app.action("a")
+        async def listener(ack, client: AsyncWebClient):
+            assert client.logger == self.test_logger
+            assert app.logger != self.test_logger
+            await ack()
+
+        request = self.build_valid_request()
+        response = await app.async_dispatch(request)
+        assert response.status == 200
+        assert response.body == ""
+        await assert_auth_test_count_async(self, 1)
 
 
 block_actions_body = {