Skip to content

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.

1. Leverage Uvicorn Loggers (Simple Approach)

Use Uvicorn's existing logger hierarchy for custom messages. This ensures your logs inherit Uvicorn's configuration:

python
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 or log_level parameter in uvicorn.run():
    bash
    uvicorn 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:

python
# 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:

python
from settings import LOGGING_CONFIG

uvicorn.run(app, log_config=LOGGING_CONFIG)

Handler Types:

  • StreamHandler: Output to terminal (default)
  • RotatingFileHandler: Auto-rotating file logs
  • TimedRotatingFileHandler: Time-based rotation

JSON Logging Formatter

For structured logging, add this JSON formatter:

python
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:

json
{"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:

python
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:

python
@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:

python
logging.getLogger("uvicorn").propagate = False

Best Practices

  1. Environment Consistency
    Use the same logging configuration in dev/prod by loading configs from environment variables:

    python
    log_config = os.getenv("LOG_CONFIG", None)
    uvicorn.run(app, log_config=log_config)
  2. 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
      }
    }
  3. Lifespan Initialization
    Initialize loggers during app startup for router modules:

    python
    from 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:

  1. Simple use: Attach to uvicorn.error logger with log_level setting
  2. Advanced control: Use dictConfig with custom formatters/handlers
  3. 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.