Repository: django/channels

Issue:
runserver should log to "django.channels.server" instead of stderr
Django v1.10 switched to using "django.server" as the logger for runserver. My console is being spammed with stderr messages that I can't configure in development when using channels, and it's very difficult to see what I want to see, which is not the 200 requests. If you would please, follow suit with Django and set up a logger using "django.channels.server" and change the runserver stderr logging to use that logger instead. Even if it doesn't follow the 200 = INFO, 400+ = ERROR paradigm, it would be nice to be able to shut that off so I can see what I'm looking for in other dev logs.


Discussion:
The `runserver` command doesn't use the logging framework for the access log right now, so we also need to switch it over to that, and work out what category the WebSocket access messages fall into.
That would be super awesome. I'd be happy to create a PR for the changes if you can let me know a decision on what the category you want is.
I'd say put everything but disconnect into INFO, and disconnects as WARNING. That seems like the most reasonable mapping - you can see all the code in channels' `runserver.py` if you want to judge for yourself though, I'm open to ideas.
Same problem here!

Making disconnect as WARNING would be a little tricky with tools like Sentry. It would require a little work with filtering unneeded warning log entry. It's not actually django-like too. Django uses WARNING to log 4xx errors. Errors. WebSocket disconnect does not look like an error.

But actually WebSocket could log as default `runserver`. With ability to filter logs by it's channel type would do the job.

For example:

    # part of django LOGGING dictionary
    'filters': {
        'http_only': {
            '()': 'channels.logging.ChannelTypeFilter',
            'channel_type': 'http',
        },
        'ws_only': {
            '()': 'channels.logging.ChannelTypeFilter',
            'channel_type': 'ws',
        },
        'custom_only': {
            '()': 'channels.logging.ChannelTypeFilter',
            'channel_type': ['ws', 'custom'],
        }
    },
Hey, I started working on this. 
Already replaced stderr with logging [here](https://github.com/labcodes/channels/commit/4022cf06c4264c348bc733aa0dbce2e8d7f37c79)

I think the next step here would be changing `channels/logs.py` to extend Django's default logging config. Am I going in the right way?

Thanks
Yes, that should be enough to get it displaying.
@andrewgodwin what I'm trying to do right now is to update the `DEFAULT_LOGGING` with django channels logging configuration.

something like this:

```diff
def setup_logger(name, verbosity=1):
     """
     Basic logger for runserver etc.
     """
 
-    formatter = logging.Formatter(
-        fmt='%(asctime)s - %(levelname)s - %(module)s - %(message)s')
-
-    handler.setFormatter(formatter)
-
-    # Set up main logger
-    logger = logging.getLogger(name)
-    logger.setLevel(logging.INFO)
-    logger.addHandler(handler)
-    if verbosity > 1:
-        logger.setLevel(logging.DEBUG)
+    DEFAULT_LOGGING['formatters'].update({
+        'django.channels.server': {
+            '()': 'django.utils.log.ServerFormatter',
+            'format': '%(asctime)s - %(levelname)s - %(module)s - %(message)s',
+        }
+    })
+
+    DEFAULT_LOGGING['handlers'].update({
+        'django.channels.server': {
+            'level': 'INFO',
+            'class': 'logging.StreamHandler',
+            'formatter': 'django.channels.server',
+        }
+    })
+
+    DEFAULT_LOGGING['loggers'].update({
+        'django.channels.server': {
+            'handlers': ['django.channels.server'],
+            'level': 'INFO',
+            'propagate': False,
+        },
+    })
 
```

But I still couldn't think in a way to not repeat code from [django.utils.log](https://github.com/django/django/blob/master/django/utils/log.py#L16-L73), any ideas here?
You probably will have to duplicate code, I don't know if overriding DEFAULT_LOGGING is going to work?
No, it didn't work. I copied it instead, but I'm still having problems because it isn't recognizing `django.utils.log` as a package.

I'm going to take a deeper look into this, posting just to give a feedback. 

Sorry about the delay on this.
@renatooliveira, any updates? Thanks so far :+1: 
hey @lwm, I've been struggling on how to make the DEFAULT_LOGGING dict compatible with all django versions, since formatters were just added on 1.10 I think. Was supposed to ask that here but procrastination and conference travels postponed that.

I'm really sorry about the delay, I'm planning to finish this issue this week

I waiting this fix very much ))
Thank you for your commits
any update?
If there was an update it would be here. This won't get fixed unless someone works on it as it's low priority.

Resolving patch:
diff --git a/channels/log.py b/channels/log.py
deleted file mode 100644
--- a/channels/log.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import logging
-
-handler = logging.StreamHandler()
-
-
-def setup_logger(name, verbosity=1):
-    """
-    Basic logger for runserver etc.
-    """
-
-    formatter = logging.Formatter(
-        fmt="%(asctime)s - %(levelname)s - %(module)s - %(message)s"
-    )
-
-    handler.setFormatter(formatter)
-
-    # Set up main logger
-    logger = logging.getLogger(name)
-    logger.setLevel(logging.INFO)
-    logger.addHandler(handler)
-    if verbosity > 1:
-        logger.setLevel(logging.DEBUG)
-
-    # Set up daphne protocol loggers
-    for module in ["daphne.ws_protocol", "daphne.http_protocol", "daphne.server"]:
-        daphne_logger = logging.getLogger(module)
-        daphne_logger.addHandler(handler)
-        daphne_logger.setLevel(logging.DEBUG if verbosity > 1 else logging.INFO)
-
-    logger.propagate = False
-    return logger
diff --git a/channels/management/commands/runserver.py b/channels/management/commands/runserver.py
--- a/channels/management/commands/runserver.py
+++ b/channels/management/commands/runserver.py
@@ -1,4 +1,5 @@
 import datetime
+import logging
 import sys
 
 from django.apps import apps
@@ -7,13 +8,14 @@
 from django.core.management.commands.runserver import Command as RunserverCommand
 
 from channels import __version__
-from channels.log import setup_logger
 from channels.routing import get_default_application
 from daphne.endpoints import build_endpoint_description_strings
 from daphne.server import Server
 
 from ...staticfiles import StaticFilesWrapper
 
+logger = logging.getLogger("django.channels.server")
+
 
 class Command(RunserverCommand):
     protocol = "http"
@@ -46,8 +48,6 @@ def add_arguments(self, parser):
         )
 
     def handle(self, *args, **options):
-        self.verbosity = options.get("verbosity", 1)
-        self.logger = setup_logger("django.channels", self.verbosity)
         self.http_timeout = options.get("http_timeout", None)
         self.websocket_handshake_timeout = options.get("websocket_handshake_timeout", 5)
         # Check Channels is installed right
@@ -92,7 +92,7 @@ def inner_run(self, *args, **options):
 
         # Launch server in 'main' thread. Signals are disabled as it's still
         # actually a subthread under the autoreloader.
-        self.logger.debug("Daphne running, listening on %s:%s", self.addr, self.port)
+        logger.debug("Daphne running, listening on %s:%s", self.addr, self.port)
 
         # build the endpoint description string from host/port options
         endpoints = build_endpoint_description_strings(host=self.addr, port=self.port)
@@ -106,7 +106,7 @@ def inner_run(self, *args, **options):
                 root_path=getattr(settings, "FORCE_SCRIPT_NAME", "") or "",
                 websocket_handshake_timeout=self.websocket_handshake_timeout,
             ).run()
-            self.logger.debug("Daphne exited")
+            logger.debug("Daphne exited")
         except KeyboardInterrupt:
             shutdown_message = options.get("shutdown_message", "")
             if shutdown_message:
@@ -131,39 +131,34 @@ def log_action(self, protocol, action, details):
         """
         Logs various different kinds of requests to the console.
         """
-        # All start with timestamp
-        msg = "[%s] " % datetime.datetime.now().strftime("%Y/%m/%d %H:%M:%S")
         # HTTP requests
         if protocol == "http" and action == "complete":
-            msg += (
-                "HTTP %(method)s %(path)s %(status)s [%(time_taken).2f, %(client)s]\n"
-                % details
-            )
+            msg = "HTTP %(method)s %(path)s %(status)s [%(time_taken).2f, %(client)s]"
+
             # Utilize terminal colors, if available
             if 200 <= details["status"] < 300:
                 # Put 2XX first, since it should be the common case
-                msg = self.style.HTTP_SUCCESS(msg)
+                logger.info(self.style.HTTP_SUCCESS(msg), details)
             elif 100 <= details["status"] < 200:
-                msg = self.style.HTTP_INFO(msg)
+                logger.info(self.style.HTTP_INFO(msg), details)
             elif details["status"] == 304:
-                msg = self.style.HTTP_NOT_MODIFIED(msg)
+                logger.info(self.style.HTTP_NOT_MODIFIED(msg), details)
             elif 300 <= details["status"] < 400:
-                msg = self.style.HTTP_REDIRECT(msg)
+                logger.info(self.style.HTTP_REDIRECT(msg), details)
             elif details["status"] == 404:
-                msg = self.style.HTTP_NOT_FOUND(msg)
+                logger.warn(self.style.HTTP_NOT_FOUND(msg), details)
             elif 400 <= details["status"] < 500:
-                msg = self.style.HTTP_BAD_REQUEST(msg)
+                logger.warn(self.style.HTTP_BAD_REQUEST(msg), details)
             else:
                 # Any 5XX, or any other response
-                msg = self.style.HTTP_SERVER_ERROR(msg)
+                logger.error(self.style.HTTP_SERVER_ERROR(msg), details)
+
         # Websocket requests
         elif protocol == "websocket" and action == "connected":
-            msg += "WebSocket CONNECT %(path)s [%(client)s]\n" % details
+            logger.info("WebSocket CONNECT %(path)s [%(client)s]", details)
         elif protocol == "websocket" and action == "disconnected":
-            msg += "WebSocket DISCONNECT %(path)s [%(client)s]\n" % details
+            logger.info("WebSocket DISCONNECT %(path)s [%(client)s]", details)
         elif protocol == "websocket" and action == "connecting":
-            msg += "WebSocket HANDSHAKING %(path)s [%(client)s]\n" % details
+            logger.info("WebSocket HANDSHAKING %(path)s [%(client)s]", details)
         elif protocol == "websocket" and action == "rejected":
-            msg += "WebSocket REJECT %(path)s [%(client)s]\n" % details
-
-        sys.stderr.write(msg)
+            logger.info("WebSocket REJECT %(path)s [%(client)s]", details)
diff --git a/channels/management/commands/runworker.py b/channels/management/commands/runworker.py
--- a/channels/management/commands/runworker.py
+++ b/channels/management/commands/runworker.py
@@ -1,11 +1,14 @@
+import logging
+
 from django.core.management import BaseCommand, CommandError
 
 from channels import DEFAULT_CHANNEL_LAYER
 from channels.layers import get_channel_layer
-from channels.log import setup_logger
 from channels.routing import get_default_application
 from channels.worker import Worker
 
+logger = logging.getLogger("django.channels.worker")
+
 
 class Command(BaseCommand):
 
@@ -34,8 +37,7 @@ def handle(self, *args, **options):
         if self.channel_layer is None:
             raise CommandError("You do not have any CHANNEL_LAYERS configured.")
         # Run the worker
-        self.logger = setup_logger("django.channels", self.verbosity)
-        self.logger.info("Running worker for channels %s", options["channels"])
+        logger.info("Running worker for channels %s", options["channels"])
         worker = self.worker_class(
             application=get_default_application(),
             channels=options["channels"],


Test patch: