uvicorn icon indicating copy to clipboard operation
uvicorn copied to clipboard

Pass request info as log record extra fields

Open dolfinus opened this issue 2 years ago • 1 comments
trafficstars

Summary

I've running an application with uvicorn, and also using python-json-logger to output all production logs in JSON (to pass them to ELK as is, without complicated parsing config).

Here is an example logging config:

version: 1
disable_existing_loggers: false

formatters:
  json:
    (): pythonjsonlogger.jsonlogger.JsonFormatter
    fmt: '%(name)s %(lineno)d %(levelname)s %(message)s'
    timestamp: true

handlers:
  main:
    class: logging.StreamHandler
    formatter: json
    stream: ext://sys.stdout

loggers:
  '':
    handlers: [main]
    propagate: false
  uvicorn:
    handlers: [main]
    propagate: false
  uvicorn.access:
    handlers: [main]
    propagate: false

So instead of logs like these:

INFO:     Started server process [34753]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
INFO:     127.0.0.1:60084 - "GET /docs HTTP/1.1" 200 OK
INFO:     127.0.0.1:60084 - "GET /openapi.json HTTP/1.1" 200 OK

I got these:

{"name": "uvicorn.error", "lineno": 76, "levelname": "INFO", "message": "Started server process [37546]", "color_message": "Started server process [\u001b[36m%d\u001b[0m]", "timestamp": "2023-11-16T09:38:39.691669+00:00"}
{"name": "uvicorn.error", "lineno": 46, "levelname": "INFO", "message": "Waiting for application startup.", "timestamp": "2023-11-16T09:38:39.691809+00:00"}
{"name": "uvicorn.error", "lineno": 60, "levelname": "INFO", "message": "Application startup complete.", "timestamp": "2023-11-16T09:38:39.692186+00:00"}
{"name": "uvicorn.error", "lineno": 218, "levelname": "INFO", "message": "Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)", "color_message": "Uvicorn running on \u001b[1m%s://%s:%d\u001b[0m (Press CTRL+C to quit)", "timestamp": "2023-11-16T09:38:39.692461+00:00"}
{"name": "uvicorn.access", "lineno": 483, "levelname": "INFO", "message": "127.0.0.1:60270 - \"GET /docs HTTP/1.1\" 200", "client_addr": "127.0.0.1:60270", "method": "GET", "full_path": "/docs", "http_version": "1.1", "status": 200, "timestamp": "2023-11-16T09:38:47.859381+00:00"}
{"name": "uvicorn.access", "lineno": 483, "levelname": "INFO", "message": "127.0.0.1:60270 - \"GET /openapi.json HTTP/1.1\" 200", "client_addr": "127.0.0.1:60270", "method": "GET", "full_path": "/openapi.json", "http_version": "1.1", "status": 200, "timestamp": "2023-11-16T09:38:48.421080+00:00"}

Message contains lines like 127.0.0.1:60270 - \"GET /openapi.json HTTP/1.1\" 200, but I prefer to have separated fields client_address, method, full_path, status and so on. uvicorn.access loger already has access to all of this info, but it is logged only in one text field message, which I don't want to parse.

Instead, I've changed the way access_log.info is called - arguments are passed both to message format and to extra, so they can be accessed by other log formatters from LogRecord object. The result is:

version: 1
disable_existing_loggers: false

formatters:
  json:
    (): pythonjsonlogger.jsonlogger.JsonFormatter
    fmt: '%(name)s %(lineno)d %(levelname)s %(message)s'
    timestamp: true
  access:
    (): pythonjsonlogger.jsonlogger.JsonFormatter
    fmt: '%(name)s %(lineno)d %(levelname)s %(client_addr)s %(method)s %(full_path)s %(http_version)s %(status)s'
    timestamp: true

handlers:
  main:
    class: logging.StreamHandler
    formatter: json
    stream: ext://sys.stdout
  access:
    class: logging.StreamHandler
    formatter: access
    stream: ext://sys.stdout

loggers:
  '':
    handlers: [main]
    propagate: false
  uvicorn:
    handlers: [main]
    propagate: false
  uvicorn.access:
    handlers: [access]
    propagate: false

{"name": "uvicorn.error", "lineno": 76, "levelname": "INFO", "message": "Started server process [37991]", "color_message": "Started server process [\u001b[36m%d\u001b[0m]", "timestamp": "2023-11-16T09:41:07.350819+00:00"}
{"name": "uvicorn.error", "lineno": 46, "levelname": "INFO", "message": "Waiting for application startup.", "timestamp": "2023-11-16T09:41:07.351085+00:00"}
{"name": "uvicorn.error", "lineno": 60, "levelname": "INFO", "message": "Application startup complete.", "timestamp": "2023-11-16T09:41:07.351890+00:00"}
{"name": "uvicorn.error", "lineno": 218, "levelname": "INFO", "message": "Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)", "color_message": "Uvicorn running on \u001b[1m%s://%s:%d\u001b[0m (Press CTRL+C to quit)", "timestamp": "2023-11-16T09:41:07.352437+00:00"}
{"name": "uvicorn.access", "lineno": 483, "levelname": "INFO", "client_addr": "127.0.0.1:48356", "method": "GET", "full_path": "/docs", "http_version": "1.1", "status": 200, "timestamp": "2023-11-16T09:41:09.065982+00:00"}
{"name": "uvicorn.access", "lineno": 483, "levelname": "INFO", "client_addr": "127.0.0.1:48356", "method": "GET", "full_path": "/openapi.json", "http_version": "1.1", "status": 200, "timestamp": "2023-11-16T09:41:09.738589+00:00"}

Checklist

  • [X] I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
  • [X] I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • [ ] I've updated the documentation accordingly.

dolfinus avatar Nov 16 '23 09:11 dolfinus

Can someone review, please?

dolfinus avatar Dec 16 '23 22:12 dolfinus

Moved to #2321

dolfinus avatar May 01 '24 11:05 dolfinus