Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to override Gunicorn's logging config to use a custom formatter

I would like gunicorn.error to use the following key-value based log format instead of the default defined in gunicorn/glogging.py:

'format': 'timestamp=%(asctime)s pid=%(process)d loglevel=%(levelname)s msg=%(message)s'`

In my gunicorn config file:

import logging.config

workers = 2
bind = "127.0.0.1:8000"
loglevel = 'INFO'

LOGGING = {
    'version': 1,
    'disable_existing_loggers': True,
    'formatters': {
        'key_value': {
            'format': 'timestamp=%(asctime)s pid=%(process)d loglevel=%(levelname)s msg=%(message)s'
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'key_value',
            'stream': 'ext://sys.stdout'
        }
    },
    'loggers': {
        'gunicorn.error': {
            'handlers': ['console'],
            'level': 'INFO',
            'propagate': False,
        },
        'flask.app': {
            'handlers': ['console'],
            'level': 'INFO',
            'propagate': False,
        }
    },
}

logging.config.dictConfig(LOGGING)

Gunicorn logs twice, in my custom format and in the default format:

timestamp=2016-12-11 15:20:49,559 pid=22110 loglevel=INFO msg=Starting gunicorn 19.6.0
[2016-12-11 15:20:49 +0000] [22110] [INFO] Starting gunicorn 19.6.0
timestamp=2016-12-11 15:20:49,559 pid=22110 loglevel=INFO msg=Listening at: http://127.0.0.1:8000 (22110)
[2016-12-11 15:20:49 +0000] [22110] [INFO] Listening at: http://127.0.0.1:8000 (22110)
timestamp=2016-12-11 15:20:49,559 pid=22110 loglevel=INFO msg=Using worker: sync
[2016-12-11 15:20:49 +0000] [22110] [INFO] Using worker: sync
timestamp=2016-12-11 15:20:49,560 pid=22115 loglevel=INFO msg=Booting worker with pid: 22115
[2016-12-11 15:20:49 +0000] [22115] [INFO] Booting worker with pid: 22115
timestamp=2016-12-11 15:20:49,595 pid=22115 loglevel=INFO msg=Starting Flask application
timestamp=2016-12-11 15:20:49,659 pid=22120 loglevel=INFO msg=Booting worker with pid: 22120
[2016-12-11 15:20:49 +0000] [22120] [INFO] Booting worker with pid: 22120
timestamp=2016-12-11 15:20:49,693 pid=22120 loglevel=INFO msg=Starting Flask application

I used the logging_tree library to take a look at the configured loggers and I'm seeing 2 gunicorn loggers emitting to the console:

<--""
   Level WARNING
   |
   o<--"flask"
   |   Level NOTSET so inherits level WARNING
   |   |
   |   o   "flask.app"
   |       Level INFO
   |       Propagate OFF
   |       Handler Stream <open file '<stdout>', mode 'w' at 0x7f86676b1150>
   |         Level INFO
   |         Formatter fmt='timestamp=%(asctime)s pid=%(process)d loglevel=%(levelname)s msg=%(message)s' datefmt=None
   |
   o<--"gunicorn"
       Level NOTSET so inherits level WARNING
       |
       o   "gunicorn.access"
       |   Level INFO
       |   Propagate OFF
       |
       o   "gunicorn.error"
       |   Level INFO
       |   Propagate OFF
       |   Handler Stream <open file '<stdout>', mode 'w' at 0x7f86676b1150>
       |     Level INFO
       |     Formatter fmt='timestamp=%(asctime)s pid=%(process)d loglevel=%(levelname)s msg=%(message)s' datefmt=None
       |   Handler Stream <open file '<stderr>', mode 'w' at 0x7f86676b11e0>
       |     Formatter fmt='%(asctime)s [%(process)d] [%(levelname)s] %(message)s' datefmt='[%Y-%m-%d %H:%M:%S %z]'
       |
       o<--"gunicorn.http"
           Level NOTSET so inherits level WARNING
           |
           o<--"gunicorn.http.wsgi"
               Level NOTSET so inherits level WARNING

Gunicorn's docs say it's possible to specify the logger class to use, but I don't know how to do this.

like image 339
Python Novice Avatar asked Dec 11 '16 15:12

Python Novice


Video Answer


3 Answers

I have had very good luck by specifying my own custom logging class. You can do this by making a class that inherits from Gunicorn's gunicorn.glogging.Logger class, and then override the setup(self, cfg) method of this class.

For example:

import logging
from gunicorn import glogging


class CustomLogger(glogging.Logger):
    """Custom logger for Gunicorn log messages."""

    def setup(self, cfg):
        """Configure Gunicorn application logging configuration."""
        super().setup(cfg)

        # Override Gunicorn's `error_log` configuration.
        self._set_handler(
            self.error_log, cfg.errorlog, logging.Formatter(
                fmt=('timestamp=%(asctime)s pid=%(process)d '
                     'loglevel=%(levelname)s msg=%(message)s')))

Now, depending upon how you launch Gunicorn, you can either provide the full path to this class; IE: program.app.CustomLogger via the --logger-class option, or you can directly pass it in via your own customer Gunicorn application class like so:

from gunicorn.app import base
from program.app import app, CustomLogger


class WebServer(base.BaseApplication):
    """Gunicorn WSGI Web Server."""

    def __init__(self, app, options):
        """Initialize server object."""
        self.options = options or {}
        self.application = app
        super().__init__()

    def load():
        """Return WSGI application."""
        return self.application

    def load_config():
        """Load configuration into Gunicorn."""
        self.cfg.set('logger_class', CustomLogger)


if __name__ == '__main__':
    WebServer(app, {}).run()

This should accomplish your desired goal without any yucky INI files being required and it is a completely supported configuration style.

like image 174
Jitsusama Avatar answered Oct 21 '22 19:10

Jitsusama


The problem with gunicorn's logging is that it normally runs in the same Python process before your code, configures logging on its own and logs some messages before any of your WSGI-related code runs and you have a chance to configure logging your way. Fortunately, as already mentioned its "logger" (gunicorn.glogging.Logger by default) is configurable.

$ gunicorn --help | grep logger
  --logger-class STRING
                        The logger you want to use to log events in Gunicorn.

Note that incremental configuration (i.e. dictConfig called once by gunicorn then by you) is not recommended and likely not what you want:

[...] there is not a compelling case for arbitrarily altering the object graph of loggers, handlers, filters, formatters at run-time, once a configuration is set up; the verbosity of loggers and handlers can be controlled just by setting levels (and, in the case of loggers, propagation flags). Changing the object graph arbitrarily in a safe way is problematic in a multi-threaded environment; while not impossible, the benefits are not worth the complexity it adds to the implementation.

Hence, my suggests it to treat gunicorn as a regular library with regards to logging and completely disable its logging configuration (in favour of you application's).

This is a trivial application which has HTTP and console entry points that should have the same logging configuration for both:

logging_cfg = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'KeyValueFormatter': {
            'format': (
                'timestamp=%(asctime)s pid=%(process)d '
                'loglevel=%(levelname)s msg=%(message)s'
            )
        },
    },
    'handlers': {
        'console': {
            'level': 'INFO',
            'class': 'logging.StreamHandler',
            'formatter': 'KeyValueFormatter',
        }
    },
    'loggers': {
        'gunicorn.access': {
            'propagate': True,
        },
        'gunicorn.error': {
            'propagate': True,
        },
    },
    'root': {
        'level': 'INFO',
        'handlers': ['console'],
    }
}

And here its implementation, app.py.

import logging.config

from gunicorn import glogging


logger = logging.getLogger(__name__)

def get_result():
    logger.info('Calculating result')
    return b'Hello world!'

def wsgi_app(environ, start_response):
    result = get_result()
    start_response('200 OK', [('Content-Type', 'text/html')])
    return [result]

def configure_logging():
    logging.config.dictConfig(logging_cfg)

class UniformLogger(glogging.Logger):

    def setup(self, cfg):
        configure_logging()


if __name__ == '__main__':
    configure_logging()
    print(get_result())

If you run python app.py you should get:

timestamp=2021-07-25 12:09:04,488 pid=4599 loglevel=INFO msg=Calculating result
b'Hello world!'

And if you run gunicorn --logger-class=app.UniformLogger app:wsgi_app followed by curl localhost:8000:

timestamp=2021-07-25 12:16:56,892 pid=4874 loglevel=INFO msg=Starting gunicorn 20.1.0
timestamp=2021-07-25 12:16:56,892 pid=4874 loglevel=INFO msg=Listening at: http://127.0.0.1:8000 (4874)
timestamp=2021-07-25 12:16:56,893 pid=4874 loglevel=INFO msg=Using worker: sync
timestamp=2021-07-25 12:16:56,894 pid=4876 loglevel=INFO msg=Booting worker with pid: 4876
timestamp=2021-07-25 12:17:06,926 pid=4876 loglevel=INFO msg=Calculating result
like image 44
saaj Avatar answered Oct 21 '22 21:10

saaj


Gunicorn uses a custom logger called glogger glogger code which internally has a Logger class, you could change the format your messages with attributes that are already available to the logger.

The gunicorn_config file with custom format may look like this (the attributes are self explanatory)

from gunicorn import glogging    
glogging.Logger.error_fmt = '{"AppName": "%(name)s", "logLevel": "%(levelname)s", "Timestamp": "%(created)f", "Class_Name":"%(module)s", "Method_name": "%(funcName)s", "process_id":%(process)d, "message": "%(message)s"}'
glogging.Logger.datefmt = ""

glogging.Logger.access_fmt = '{"AppName": "%(name)s", "logLevel": "%(levelname)s", "Timestamp": "%(created)f","Class_Name":"%(module)s", "Method_name": "%(funcName)s", "process_id":%(process)d, "message": "%(message)s"}'
glogging.Logger.syslog_fmt = '{"AppName": "%(name)s", "logLevel": "%(levelname)s", "Timestamp": "%(created)f","Class_Name":"%(module)s", "Method_name": "%(funcName)s", "process_id":%(process)d, "message": "%(message)s"}'

I hope this helps, in my opinion it is one of the clean way of overriding the logger format.

but if you want add custom attributes in the logs you may have to create a new instance of the logger class as this logger class doesn't support filter. and assigning a logger instance wont help either.

like image 27
Hemanth Kumar Avatar answered Oct 21 '22 19:10

Hemanth Kumar