Repository: plotly/dash

Issue:
Dash object adds handler to logger creating extra handler
Hi, 

Just ran into this issue that stems from the two conventions of 
```
logger = logging.getLogger(__name__)
```
```
app = dash.Dash(__name__)
```
and the line 
https://github.com/plotly/dash/blob/3a1a1c6dbc6cd96040148f927af808c1a60e22f8/dash/dash.py#L217

This results in an extra handler being added to the `__name__` logger which is presumably unwanted by the user. 

I think the convention is to add at most add a `logging.NullHandler()` handler to logging library/packages.  From [Configuring Logging for a Library section](https://docs.python.org/2/howto/logging.html#configuring-logging-for-a-library) (though it's Python2 fyi), it states: 

> Note: It is strongly advised that you do not add any handlers other than NullHandler to your library’s loggers. This is because the configuration of handlers is the prerogative of the application developer who uses your library. The application developer knows their target audience and what handlers are most appropriate for their application: if you add handlers ‘under the hood’, you might well interfere with their ability to carry out unit tests and deliver logs which suit their requirements.

When I have a moment I would be happy to try and open a small PR  rectifying it if maintainers decide they would want to avoid adding logging handlers in Dash. 

Nice one :) 


Discussion:
I just ran into this bug, which caused my package level loggers to output everything in duplicate. I spent quite a while debugging it, since thought it was caused by Flask.

I strongly agree with the idea of not adding logging handlers. What happened in my case was that my package already had a logger by `__name__` and then Dash forcibly added a `StreamHandler` to it.



A possible workaround is the following:
```
log = logging.getLogger(__name__)
dashapp1 = dash.Dash(.... )
# remove last handler. The one added by dash
log.handlers.pop()
```
@RiccardoNizzolo yep that's how I work around it currently also,
Working around it is easy once you figure out what's going on. But at least for me, that took quite a while. Please don't force logging handlers on us :)
I'm annoyed that the team would do that, add a handler in this manner. Similar to @jjnurminen I spent hours and hours trying to nail down the cause. I didn't want to just remove handlers as a workaround until I figured out why I was getting so many messages ("Did I configure logging wrong?").

Don't get me wrong, love Dash for what it makes possible, which is a lot. But let's save other developers from this trouble.
Bump this ticket as I see this ticket now after I created a ticket to JupyterDash https://github.com/plotly/jupyter-dash/issues/61. While I think this issue is less obvious with Dash, it's obvious and annoying with JupyterDash as with Jupyterlab one could repeatedly run notebook cell that creates JuptyerDash object. 
I just hit this and like others had to spend a while to determine that dash was adding a sysout handler as its not expected that a library makes changes to your logging configuration. It's frustrating as it goes to std out without any formatting.

Should use "logging.NullHandler()" to avoid warnings with a config to turn on if required.

An example of how this is done better is the AWS library where NullHandler is used by default with the ability to add a handler is there if required.
https://github.com/boto/boto3/blob/develop/boto3/__init__.py
3 years later this is still not fixed... a library/package should not be setting up own handlers - this is responsibility of end-user application. At the very least that should be an opt-in for those who can't be bothered setting up their own handlers.
Apologies for not providing a maintainer response to this issue before. We're really hardly using this logger. There's one [debug log](https://github.com/plotly/dash/blob/5baac83c6e2e0222c52c193b6eb74833e184aaf1/dash/dash.py#L907) that we can probably just delete. The only one we care about is the one that displays the url we're serving on:

https://github.com/plotly/dash/blob/5baac83c6e2e0222c52c193b6eb74833e184aaf1/dash/dash.py#L1949

That message should definitely be displayed by default, but is there any reason not to just `print` it? If we did that, I guess we wouldn't want to remove `self.logger` entirely, or everyone using these workarounds would see their code fail, but we could drop it to an unused `NullHandler` and remove it if and when we make a new major release.
I would happily have my application break and have to remove my stupid hacks to `mydashapp.logger` if y'all would get rid of these handlers.  As others have said, libraries shouldn't be setting up handlers.
I have tried all above mentioned workarounds. It doesn't work. As soon as I add my custom logger and FileHandler to it, very log starts to be duplicated.

Resolving patch:
diff --git a/dash/dash.py b/dash/dash.py
--- a/dash/dash.py
+++ b/dash/dash.py
@@ -329,6 +329,9 @@ class Dash:
     :param background_callback_manager: Background callback manager instance
         to support the ``@callback(..., background=True)`` decorator.
         One of ``DiskcacheManager`` or ``CeleryManager`` currently supported.
+
+    :param add_log_handler: Automatically add a StreamHandler to the app logger
+        if not added previously.
     """
 
     def __init__(  # pylint: disable=too-many-statements
@@ -361,6 +364,7 @@ def __init__(  # pylint: disable=too-many-statements
         update_title="Updating...",
         long_callback_manager=None,
         background_callback_manager=None,
+        add_log_handler=True,
         **obsolete,
     ):
         _validate.check_obsolete(obsolete)
@@ -481,8 +485,10 @@ def __init__(  # pylint: disable=too-many-statements
         self._long_callback_count = 0
         self._background_manager = background_callback_manager or long_callback_manager
 
-        self.logger = logging.getLogger(name)
-        self.logger.addHandler(logging.StreamHandler(stream=sys.stdout))
+        self.logger = logging.getLogger(__name__)
+
+        if not self.logger.handlers and add_log_handler:
+            self.logger.addHandler(logging.StreamHandler(stream=sys.stdout))
 
         if isinstance(plugins, patch_collections_abc("Iterable")):
             for plugin in plugins:


Test patch: