Repository: benoitc/gunicorn

Issue:
options to redirect stdout/stderr in gunicorn logging
following a discussion on twitter, I would like to rediscuss having an option to redtirect stdout/stderr to our logs:

<img width="627" alt="screen shot 2016-05-18 at 13 29 17" src="https://cloud.githubusercontent.com/assets/14498/15357022/936bf032-1cfc-11e6-96af-92207a96ca2b.png">

This behaviour has been removed in 41523188bc05fcbba840ba2e18ff67cd9df638e9  3 years ago. But rediscussed last year and now.

I think we could have a `--capture-stdio` option that would allows it. Thoughts?



Discussion:
I love this idea. I was going to propose it myself after struggling with trying to get Django to have its stderr show up in gunicorn's error.log. I was just about to ask about it on the mailing list, but it seems that lists.gunicorn.org is dead.

So yeah, there should be a way to get stdout and stderr from the wsgi app sent to the same places that gunicorn uses for its error logging etc. In this way, it will be like the old days of CGI scripts sending their output to apache log files. :)

It _does_ seem to be causing a lot of confusion. I've answered now a post on SO and I think several questions on our issue tracker about missing log messages.

Making it opt-in seems the best idea.


Resolving patch:
diff --git a/gunicorn/config.py b/gunicorn/config.py
--- a/gunicorn/config.py
+++ b/gunicorn/config.py
@@ -917,6 +917,7 @@ class WorkerTmpDir(Setting):
         If not set, the default temporary directory will be used.
         """
 
+
 class User(Setting):
     name = "user"
     section = "Server Mechanics"
@@ -1121,6 +1122,20 @@ class Loglevel(Setting):
         """
 
 
+class CaptureOutput(Setting):
+    name = "capture_output"
+    section = "Logging"
+    cli = ["--capture-output"]
+    validator = validate_bool
+    action = 'store_true'
+    default = False
+    desc = """\
+        Redirect stdout/stderr to Error log.
+
+        .. versionadded:: 19.6
+        """
+
+
 class LoggerClass(Setting):
     name = "logger_class"
     section = "Logging"
diff --git a/gunicorn/glogging.py b/gunicorn/glogging.py
--- a/gunicorn/glogging.py
+++ b/gunicorn/glogging.py
@@ -12,6 +12,7 @@
 import os
 import socket
 import sys
+import threading
 import traceback
 
 from gunicorn import util
@@ -186,6 +187,8 @@ def __init__(self, cfg):
         self.access_log.propagate = False
         self.error_handlers = []
         self.access_handlers = []
+        self.logfile = None
+        self.lock = threading.Lock()
         self.cfg = cfg
         self.setup(cfg)
 
@@ -195,8 +198,16 @@ def setup(self, cfg):
         self.access_log.setLevel(logging.INFO)
 
         # set gunicorn.error handler
+        if self.cfg.capture_output and cfg.errorlog != "-":
+            for stream in sys.stdout, sys.stderr:
+                stream.flush()
+
+            self.logfile = open(cfg.errorlog, 'a+')
+            os.dup2(self.logfile.fileno(), sys.stdout.fileno())
+            os.dup2(self.logfile.fileno(), sys.stderr.fileno())
+
         self._set_handler(self.error_log, cfg.errorlog,
-                logging.Formatter(self.error_fmt, self.datefmt))
+                          logging.Formatter(self.error_fmt, self.datefmt))
 
         # set gunicorn.access handler
         if cfg.accesslog is not None:
@@ -318,6 +329,18 @@ def now(self):
         return time.strftime('[%d/%b/%Y:%H:%M:%S %z]')
 
     def reopen_files(self):
+        if self.cfg.capture_output and self.cfg.errorlog != "-":
+            for stream in sys.stdout, sys.stderr:
+                stream.flush()
+
+            with self.lock:
+                if self.logfile is not None:
+                    self.logfile.close()
+                self.logfile = open(self.cfg.errorlog, 'a+')
+                os.dup2(self.logfile.fileno(), sys.stdout.fileno())
+                os.dup2(self.logfile.fileno(), sys.stderr.fileno())
+
+
         for log in loggers():
             for handler in log.handlers:
                 if isinstance(handler, logging.FileHandler):


Test patch: