I'm playing around with FastAPI and Structlog and wanted to test and convert log format from plain text/string to JSON format for better readability and processing by the log aggregator platforms. Facing a case where certain log output are available in JSON but rest in plain string.
Current Output
INFO: 127.0.0.1:62154 - "GET /api/preface HTTP/1.1" 200 OK
INFO: 127.0.0.1:62154 - "GET /loader.json HTTP/1.1" 200 OK
INFO: 127.0.0.1:62155 - "GET /hello_world HTTP/1.1" 200 OK
{"key":"test_key","message":"Push to NFS Success","event":"Testing Fast API..","logger":"test_my_api","filename":"main.py","func_name":"Hello_World","process":23760,"module":"docker","thread":23140,"pathname":"D:\\my_work\\fast_api\\main.py","process_name":"SpawnProcess-1","level":"info","time-iso":"2023-06-30T15:25:03.113400Z"}
Expected Output:
{
"level": "INFO",
"IP": "127.0 .0 .1: 62154",
"method": "GET",
"endpoint": "/loader.json",
"protocol": "HTTP / 1.1",
"status_code": 200,
"status": "OK"
}
{
"level": "INFO",
"IP": "127.0 .0 .1: 62155",
"method": "GET",
"endpoint": "/api/preface",
"protocol": "HTTP / 1.1",
"status_code": 200,
"status": "OK"
}
{
"level": "INFO",
"IP": "127.0 .0 .1: 62155",
"method": "GET",
"endpoint": "/hello_world",
"protocol": "HTTP / 1.1",
"status_code": 200,
"status": "OK"
}
{"key":"test_key","message":"Push to NFS Success","event":"Testing Fast API..","logger":"test_my_api","filename":"main.py","func_name":"Hello_World","process":23760,"module":"docker","thread":23140,"pathname":"D:\\my_work\\fast_api\\main.py","process_name":"SpawnProcess-1","level":"info","time-iso":"2023-06-30T15:25:03.113400Z"}
What am I missing here ? thanks !
struct.py
import orjson
import structlog
import logging
## Added only the necessary context.
class StructLogTest:
def __init__(self, logging_level=logging.DEBUG, logger_name="test"):
self.logging_level = logging_level
self.logger_name = logger_name
StructLogTest.logger_name_var = self.logger_name
self.configure_structlog(self.logging_level, self.logger_name)
def logger_name(_, __, event_dict):
event_dict["test_log"] = StructLogTest.logger_name_var
return event_dict
@staticmethod
def configure_structlog(logging_level, logger_name):
structlog.configure(
processors=[
StructLogTest.logger_name,
structlog.threadlocal.merge_threadlocal,
structlog.processors.CallsiteParameterAdder(),
structlog.processors.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.StackInfoRenderer(),
structlog.processors.format_exc_info,
structlog.processors.TimeStamper(fmt="iso", utc=True, key="time-iso"),
structlog.processors.JSONRenderer(serializer=orjson.dumps),
],
wrapper_class=structlog.make_filtering_bound_logger(logging_level),
context_class=dict,
logger_factory=structlog.BytesLoggerFactory(),
)
return structlog
def define_Logger(self, *args, **kwargs):
return structlog.get_logger(*args, **kwargs)
def info(self, message, *args, **kwargs):
return structlog.get_logger().info(message, *args, **kwargs)
and other methods so on..
main.py
from struct import StructLogTest
from fastapi import APIRouter
import requests
from requests.auth import HTTPBasicAuth
from requests import Response
log = StructLogTest(logger_name="test_my_api")
log = log.get_Logger()
@router.get("/hello_world")
def Hello_World():
logg = log.bind(key=test_key)
logg.info(
"Testing Fast API..",
message=some_other_meaningful_function.dump(),
)
return {" Hello World !! "}
Structlog is an entirely separate logging framework from stdlib
logging. It will not configure the stdlib logging framework for you. Uvicorn uses stdlib logging, and you can't change that short of forking and editing the source code to use some other logging framework.You do have some options here, though. The simplest is just to configure stdlib logging with a json formatter, for example create
uvicorn-logconfig.ini:That uses a stdlib json formatter from python-json-logger. Now, run your app with something like:
The logs from uvicorn and from your own application code will render as JSON, the former through stdlib formatters and the latter through structlog processors.
Here's what I saw when starting up your app, making a request to http://127.0.0.1:8000/hello_world to get the "Testing Fast API..." event from structlog, and then shutting down uvicorn with Ctrl+C:
This is recommended as the simplest approach in structlog's documentation (see the subsection "Don't Integrate" in the Standard Library section):
Since stdlib logging is highly configurable, and structlog is also highly flexible, there are other approaches possible to integrate structlog and stdlib more tightly. See Rendering Using structlog-based Formatters Within
loggingif interested.