FastAPI Logging Configuration with Uvicorn
Problem Statement
When running a FastAPI application with Uvicorn, developers often encounter a common issue: custom log messages don't appear in the output. This includes:
- Startup messages (e.g.,
LOG.info("API is starting up")
) - Endpoint-specific logs (e.g.,
LOG.info("GET /")
) - Custom debug information
Uvicorn's built-in logging captures request/response information but silences your application logs by default. The challenge is configuring logging to work seamlessly in both local development (with --reload
) and production environments without environment-specific hacks.
Recommended Solutions
1. Leverage Uvicorn Loggers (Simple Approach)
Use Uvicorn's existing logger hierarchy for custom messages. This ensures your logs inherit Uvicorn's configuration:
from fastapi import FastAPI
import logging
import uvicorn
app = FastAPI()
# Use uvicorn's error logger
logger = logging.getLogger("uvicorn.error")
@app.get("/")
async def read_root():
logger.info("GET / endpoint accessed") # This will appear!
return {"Hello": "World"}
if __name__ == "__main__":
uvicorn.run(app, log_level="info")
Key Notes:
uvicorn.error
is the main logger Uvicorn uses for application messages- Set log level via
--log-level
CLI flag orlog_level
parameter inuvicorn.run()
:bashuvicorn main:app --log-level debug --reload
- Supported levels:
trace
,debug
,info
,warning
,error
,critical
- Disable access logs separately with
--no-access-log
2. Advanced Customization with Logging Config
For granular control over formatting and handlers, use Python's dictConfig
:
# settings.py
LOGGING_CONFIG = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"default": {
"format": "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
},
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "default",
"stream": "ext://sys.stdout"
},
"file": {
"class": "logging.handlers.RotatingFileHandler",
"formatter": "default",
"filename": "app.log",
"maxBytes": 5*1024*1024, # 5 MB
"backupCount": 3
}
},
"loggers": {
"uvicorn": {"handlers": ["console", "file"], "level": "INFO"},
"uvicorn.error": {"handlers": ["console", "file"], "level": "INFO"},
"uvicorn.access": {"handlers": ["console"], "level": "INFO"}
}
}
Apply in your application:
from settings import LOGGING_CONFIG
uvicorn.run(app, log_config=LOGGING_CONFIG)
Handler Types:
StreamHandler
: Output to terminal (default)RotatingFileHandler
: Auto-rotating file logsTimedRotatingFileHandler
: Time-based rotation
JSON Logging Formatter
For structured logging, add this JSON formatter:
import logging
import json
class JSONFormatter(logging.Formatter):
def format(self, record):
return json.dumps({
"time": record.asctime,
"level": record.levelname,
"message": record.getMessage(),
"logger": record.name
})
formatter = JSONFormatter()
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logging.getLogger("uvicorn.error").addHandler(handler)
Sample output:
{"time": "2023-08-30 09:15:22", "level": "INFO", "message": "Server started", "logger": "uvicorn.error"}
3. Standalone Custom Logger
For complete separation from Uvicorn’s loggers:
import logging
import sys
# Configure root logger
logger = logging.getLogger("myapp")
logger.setLevel(logging.DEBUG)
# Console handler
console_handler = logging.StreamHandler(sys.stdout)
console_format = logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
console_handler.setFormatter(console_format)
# File handler
file_handler = logging.FileHandler("app.log")
file_format = logging.Formatter("%(asctime)s [%(process)d] [%(levelname)s] %(message)s")
file_handler.setFormatter(file_format)
logger.addHandler(console_handler)
logger.addHandler(file_handler)
Access in endpoints:
@app.get("/data")
async def get_data():
logger.info("Fetching data from database")
return {...}
Tradeoffs:
- ✅ Full control over formatting/handling
- ❌ Loses integration with Uvicorn's lifecycle events
- ❌ Requires manual level management
Logger Propagation
By default, child loggers propagate messages to parent loggers. Disable propagation when using custom configurations:
logging.getLogger("uvicorn").propagate = False
Best Practices
Environment Consistency
Use the same logging configuration in dev/prod by loading configs from environment variables:pythonlog_config = os.getenv("LOG_CONFIG", None) uvicorn.run(app, log_config=log_config)
Access Log Separation
Keep access logs (HTTP requests) separate from application logs:python# settings.py "loggers": { "uvicorn.access": { "handlers": ["access_file"], "level": "INFO", "propagate": False } }
Lifespan Initialization
Initialize loggers during app startup for router modules:pythonfrom contextlib import asynccontextmanager @asynccontextmanager async def lifespan(app: FastAPI): app.state.logger = logging.getLogger("uvicorn.error") yield app = FastAPI(lifespan=lifespan) # In routers @router.get("/items") async def read_items(request: Request): request.app.state.logger.info("Fetching items")
Conclusion
Configure FastAPI & Uvicorn logging using these methods:
- Simple use: Attach to
uvicorn.error
logger withlog_level
setting - Advanced control: Use
dictConfig
with custom formatters/handlers - Full isolation: Create separate logger with explicit handlers
Ensure consistent logging behavior across environments by centralizing your configuration and avoiding hard-coded logger implementations. The uvicorn.error
logger approach provides the smoothest integration for most use cases, while custom configurations offer flexibility for complex deployments.