How to Set Up File and Console Logging in FastAPI
The benavlabs/fastapi-boilerplate ships with a centralized logging system that routes output to both rotating files and the console using Python's standard logging module combined with structlog, all configurable through Pydantic settings in src/app/core/config.py and initialized in src/app/core/logger.py.
This guide explains how to configure and extend the production-ready logging implementation found in the benavlabs/fastapi-boilerplate repository. The architecture separates configuration from implementation, allowing you to control log formatting, rotation policies, and output destinations via environment variables without modifying application code.
Architecture Overview
The logging system consists of three coordinated layers: configuration models that define behavior, a bootstrap module that wires handlers to the root logger, and middleware that injects request context. Together, these components ensure every log line captures relevant metadata while writing to both persistent files and stdout/stderr.
Configuration Layer
All logging behavior is controlled through src/app/core/config.py, which defines two Pydantic sub-models within the main Settings class (lines 24‑48). The FileLoggerSettings class manages rotation and persistence, while ConsoleLoggerSettings controls terminal output formatting.
Key configuration fields include:
FILE_LOG_MAX_BYTES— triggers rotation when the log file exceeds this size (default 10 MiB)FILE_LOG_BACKUP_COUNT— number of archived log files to retainFILE_LOG_LEVELandCONSOLE_LOG_LEVEL— independent severity thresholdsFILE_LOG_FORMAT_JSONandCONSOLE_LOG_FORMAT_JSON— toggle between JSON and human-readable outputCONSOLE_LOG_INCLUDE_PATHandCONSOLE_LOG_INCLUDE_METHOD— control which request fields appear in console logs
These settings load automatically from environment variables or a .env file, requiring no code changes to adjust behavior.
Logger Bootstrap and Structlog Integration
The src/app/core/logger.py module initializes the global logging infrastructure in four stages. First, it configures structlog (lines 73‑77) to add shared processors for timestamping and context variable binding:
structlog.configure(
processors=SHARED_PROCESSORS + [structlog.stdlib.ProcessorFormatter.wrap_for_formatter],
logger_factory=structlog.stdlib.LoggerFactory(),
cache_logger_on_first_use=True,
)
Second, the build_formatter function (lines 80‑90) creates a ProcessorFormatter that conditionally renders output as JSON via JSONRenderer or as colored console text via ConsoleRenderer. This formatter respects the inclusion flags defined in your settings, filtering out request metadata when disabled.
Third, the module attaches two handlers to Python's root logger:
-
File Handler (lines 97‑106): A
RotatingFileHandlerwrites tologs/app.log, rotating whenFILE_LOG_MAX_BYTESis exceeded and preservingFILE_LOG_BACKUP_COUNTarchives. It uses the formatter configured viaFILE_LOG_FORMAT_JSON. -
Console Handler (lines 110‑116): A
StreamHandlerwrites to stdout/stderr, using the same formatter builder but respectingCONSOLE_LOG_FORMAT_JSONand console-specific inclusion settings.
Fourth, the root logger is cleared of pre-existing handlers (lines 120‑126) to prevent duplicate entries, then the new handlers are attached.
Uvicorn Integration
To eliminate log duplication, the bootstrap process reconfigures Uvicorn's internal loggers (lines 128‑133). The uvicorn, uvicorn.error, and uvicorn.access loggers are stripped of their default handlers and set to propagate to the root logger, ensuring all HTTP request logs flow through your configured file and console handlers with consistent formatting.
Request Context Injection
The LoggerMiddleware in src/app/middleware/logger_middleware.py (lines 26‑38) binds a unique request_id to structlog's context variables for every incoming request. If the client provides an X-Request-ID header, the middleware uses that value; otherwise, it generates a UUID. This ID automatically appears in every log line emitted during the request lifecycle, enabling traceability across async worker threads.
Configuring Log Output
You control the logging system via environment variables that map to the Pydantic settings. Create a .env file in your project root to override defaults without touching code:
# File logging
FILE_LOG_LEVEL=DEBUG
FILE_LOG_MAX_BYTES=5242880
FILE_LOG_BACKUP_COUNT=10
FILE_LOG_FORMAT_JSON=false
# Console logging
CONSOLE_LOG_LEVEL=INFO
CONSOLE_LOG_FORMAT_JSON=true
CONSOLE_LOG_INCLUDE_PATH=true
CONSOLE_LOG_INCLUDE_METHOD=true
These values are injected at runtime when src/app/core/setup.py imports the logger module during application startup.
Usage Examples
Basic Logging in Application Code
Any module can retrieve a logger that inherits the centralized configuration. Both standard library loggers and structlog loggers work interchangeably:
import logging
import structlog
# Standard library approach
logger = logging.getLogger(__name__)
# Structlog approach (includes context vars automatically)
slog = structlog.get_logger(__name__)
async def process_data():
logger.info("Processing started")
slog.info("Processing details", item_count=42, status="active")
Both calls emit to the rotating file and console, formatted according to your settings. When CONSOLE_LOG_FORMAT_JSON is true, the output is machine-parseable; when false, it renders as human-readable text with timestamps and log levels color-coded.
Adding Custom Handlers
To send specific log levels to additional destinations (e.g., syslog or cloud logging), extend src/app/core/logger.py after the existing handler configuration (following line 126):
import logging.handlers
# Example: syslog integration
syslog_handler = logging.handlers.SysLogHandler(address="/dev/log")
syslog_handler.setLevel(logging.WARNING)
syslog_handler.setFormatter(
build_formatter(
json_output=settings.CONSOLE_LOG_FORMAT_JSON,
pre_chain=SHARED_PROCESSORS,
)
)
root_logger = logging.getLogger()
root_logger.addHandler(syslog_handler)
Place this code block immediately after the existing root logger configuration to maintain consistent handler ordering.
Verifying Log Rotation
To confirm file rotation works, temporarily lower FILE_LOG_MAX_BYTES to 1024 bytes and generate log traffic. You will observe the creation of:
src/app/logs/app.log(current active log)src/app/logs/app.log.1throughapp.log.N(archived logs up toFILE_LOG_BACKUP_COUNT)
The RotatingFileHandler manages this automatically when the current file exceeds the byte threshold.
Summary
The benavlabs/fastapi-boilerplate provides a robust, production-ready solution to set up file and console logging with minimal configuration:
- Centralized control via
FileLoggerSettingsandConsoleLoggerSettingsinsrc/app/core/config.py - Dual output through
RotatingFileHandler(persistent storage) andStreamHandler(console visibility) - Structured logging via structlog with automatic request ID injection through
LoggerMiddleware - Uvicorn integration that prevents duplicate logs by propagating access logs through the root logger
- Runtime configurability using environment variables, requiring no code changes to adjust levels or formats
Frequently Asked Questions
How do I disable file logging and keep only console output?
Set FILE_LOG_LEVEL to a value higher than your application's highest log emission (e.g., CRITICAL), or modify src/app/core/logger.py to conditionally skip the RotatingFileHandler attachment based on a new environment flag. The existing architecture keeps handlers independent, so removing the file handler (lines 97‑106) will not affect console output.
Why do my logs show duplicate entries or missing request IDs?
Duplicate entries typically occur when Uvicorn's default handlers are not properly detached. Verify that lines 128‑133 in src/app/core/logger.py are executing, which remove handlers from uvicorn, uvicorn.error, and uvicorn.access loggers and enable propagation. Missing request IDs usually indicate that LoggerMiddleware is not mounted in your FastAPI application—ensure it is added to the middleware stack in your application factory or main.py.
Can I change log levels at runtime without restarting the application?
Python's standard logging module supports runtime reconfiguration, but the boilerplate loads settings at startup when src/app/core/setup.py imports the logger module. To change levels dynamically, you would need to implement a reload mechanism that calls logging.getLogger().setLevel() or restarts the application context. Alternatively, use a .env file with a process manager that supports graceful restarts to pick up new levels without dropping connections.
What is the performance impact of using structlog versus standard logging?
The boilerplate mitigates structlog's overhead through cache_logger_on_first_use=True (line 76) and by binding context variables at the middleware layer rather than per-log-line. The ProcessorFormatter merges context efficiently, making the performance difference negligible for typical web workloads. For high-throughput scenarios, enable FILE_LOG_FORMAT_JSON to avoid the string formatting overhead of ConsoleRenderer.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →