如何覆盖 Gunicorn 的日志配置以使用自定义格式化程序

Pyt*_*ice 12 python logging flask gunicorn

我希望 gunicorn.error 使用以下基于键值的日志格式,而不是在gunicorn/glogging.py 中定义的默认值:

'format': 'timestamp=%(asctime)s pid=%(process)d loglevel=%(levelname)s msg=%(message)s'`
Run Code Online (Sandbox Code Playgroud)

在我的 gunicorn 配置文件中:

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)
Run Code Online (Sandbox Code Playgroud)

Gunicorn 以我的自定义格式和默认格式记录两次:

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
Run Code Online (Sandbox Code Playgroud)

我使用logging_tree库来查看配置的记录器,我看到 2 个 gunicorn 记录器发送到控制台:

<--""
   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
Run Code Online (Sandbox Code Playgroud)

Gunicorn 的文档说可以指定要使用的记录器类,但我不知道如何做到这一点。

saa*_*aaj 9

gunicorn日志记录的问题在于,它通常在您的代码之前在同一个 Python 进程中运行,自行配置日志记录并在任何 WSGI 相关代码运行之前记录一些消息,并且您有机会按照自己的方式配置日志记录。幸运的是,正如已经提到的,它的“记录器”(gunicorn.glogging.Logger默认情况下)是可配置的。

$ gunicorn --help | grep logger
  --logger-class STRING
                        The logger you want to use to log events in Gunicorn.
Run Code Online (Sandbox Code Playgroud)

请注意,不建议使用增量配置(即由您dictConfig调用一次),并且可能不是您想要的:gunicorn

[...] 一旦设置了配置,就没有令人信服的情况可以在运行时任意更改记录器、处理程序、过滤器、格式化程序的对象图;记录器和处理程序的详细程度可以通过设置级别来控制(对于记录器,还可以设置传播标志)。在多线程环境中以安全的方式任意改变对象图是有问题的;虽然并非不可能,但其带来的好处不值得它增加实施的复杂性。

因此,我建议将其视为gunicorn日志记录方面的常规库并完全禁用其日志记录配置(有利于您的应用程序)。

这是一个简单的应用程序,具有 HTTP 和控制台入口点,两者应该具有相同的日志记录配置:

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'],
    }
}
Run Code Online (Sandbox Code Playgroud)

这里是它的实现,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())
Run Code Online (Sandbox Code Playgroud)

如果你运行python app.py你应该得到:

timestamp=2021-07-25 12:09:04,488 pid=4599 loglevel=INFO msg=Calculating result
b'Hello world!'
Run Code Online (Sandbox Code Playgroud)

如果你gunicorn --logger-class=app.UniformLogger app:wsgi_app接着运行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
Run Code Online (Sandbox Code Playgroud)


Jit*_*ama 6

通过指定我自己的自定义日志记录类,我很幸运。你可以通过创建一个继承自 Gunicorn 的gunicorn.glogging.Logger类的类来实现,然后覆盖setup(self, cfg)这个类的方法。

例如:

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')))
Run Code Online (Sandbox Code Playgroud)

现在,根据您启动 Gunicorn 的方式,您可以提供此类的完整路径;IE:program.app.CustomLogger通过--logger-class选项,或者您可以直接通过您自己的客户 Gunicorn 应用程序类传递它,如下所示:

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()
Run Code Online (Sandbox Code Playgroud)

这应该可以实现您想要的目标,而无需任何令人讨厌的 INI 文件,并且它是一种完全受支持的配置样式。


小智 5

Gunicorn 使用名为 glogger 的自定义记录器glogger 代码,其内部有一个 Logger 类,您可以使用记录器已有的属性更改消息的格式。

自定义格式的gunicorn_config文件可能看起来像这样(属性是不言自明的)

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"}'
Run Code Online (Sandbox Code Playgroud)

我希望这有帮助,在我看来,这是覆盖记录器格式的干净方法之一。

但是如果您想在日志中添加自定义属性,您可能必须创建记录器类的新实例,因为此记录器类不支持过滤器。并且分配记录器实例也无济于事。


Lau*_*RTE 0

我认为你只需要给配置文件(.ini.conf)提供你自己的配置。确保覆盖默认值。

examples/logging.conf目录中有一个examples