I have a Flask API with gunicorn. Gunicorn logs all the requests to my API, i.e.
172.17.0.1 - - [19/Sep/2018:13:50:58 +0000] "GET /api/v1/myview HTTP/1.1" 200 16 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36"
However, I want to filter the logs to exclude a certain endpoint which is called from some other service all few seconds.
I wrote a filter to exclude this endpoint from being logged:
class NoReadyFilter(logging.Filter):
def filter(self, record):
return record.getMessage().find('/api/v1/ready') == -1
and if I add this filter to the werkzeug
logger and use the Flask development server, the filter works. Requests to /api/v1/ready
won't appear in the log files. However, I can't seem to add the filter to the gunicorn
logger. With the following code, requests to /api/v1/ready
still appear:
if __name__ != '__main__':
gunicorn_logger = logging.getLogger('gunicorn.glogging.Logger')
gunicorn_logger.setLevel(logging.INFO)
gunicorn_logger.addFilter(NoReadyFilter())
How can you add a filter to the gunicorn logger? I tried adding it to the gunicorn.error
-logger as suggested here, but it didn't help.
To watch the logs in the console you need to use the option --log-file=- . In version 19.2, Gunicorn logs to the console by default again.
This log file is located at /var/log/cloudify/rest/gunicorn-access.
Gunicorn error logs You can change how verbose these messages are using the "loglevel" setting, which can be set to log more info using the "debug" level, or only errors, using the "error" level, etc.
There are a couple of reasons behind this: Gunicorn has its own loggers, and it’s controlling log level through that mechanism. A fix for this would be to add app.logger.setLevel (logging.DEBUG). But what’s the problem with this approach? Well, first off, that’s hard-coded into the application itself.
Yes, we could refactor that out into an environment variable, but then we have two different log levels: one for the Flask application, but a totally separate one for Gunicorn, which is set through the –log-level parameter (values like “debug”, “info”, “warning”, “error”, and “critical”).
Particularly when it comes to logs, the problem we have with Gunicorn is that it has it’s own log handlers. We need to make sure logs coming from Flask and Gunicorn are wired together in order for us to have a nice logging experience.
The key thing here (line #3) is to set the handlers of our Flask application logger to the Gunicorn logger (using the same output handlers and giving us a consistent logging experience). The last line of that snippet is significant. When you pass –log-level to Gunicorn, that is going to (unsurprisingly) be the log level for its appropriate handler.
I finally found a way of doing it by creating a subclass
class CustomGunicornLogger(glogging.Logger):
def setup(self, cfg):
super().setup(cfg)
# Add filters to Gunicorn logger
logger = logging.getLogger("gunicorn.access")
logger.addFilter(NoReadyFilter())
that inherits from guncorn.glogging.Logger
. You can then provide this class as a parameter for gunicorn
, e.g.
gunicorn --logger-class "myproject.CustomGunicornLogger" app
While a custom logging class would work, it is probably an overkill for a simple access log filter. Instead, I would use Gunicorn's on_starting() server hook to add a filter to the access logger.
The hook can be added in the settings file (default gunicorn.conf.py
), so all gunicorn configuration stays in one place.
import logging
import re
wsgi_app = 'myapp.wsgi'
bind = '0.0.0.0:9000'
workers = 5
accesslog = '-'
class RequestPathFilter(logging.Filter):
def __init__(self, *args, path_re, **kwargs):
super().__init__(*args, **kwargs)
self.path_filter = re.compile(path_re)
def filter(self, record):
req_path = record.args['U']
if not self.path_filter.match(req_path):
return True # log this entry
# ... additional conditions can be added here ...
return False # do not log this entry
def on_starting(server):
server.log.access_log.addFilter(RequestPathFilter(path_re=r'^/api/v1/ready$'))
Some notes on this sample implementation:
RequestPathFilter
can also be nested on_starting()
to hide it from external modules.record.args
. This contains the raw values used to construct the logging message.record.getMessage()
instead of the raw values is bad because:
Wget/1.20.1/api/v1/ready (linux-gnu)
.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With