# How to Set Up File and Console Logging in FastAPI

> Learn to set up file and console logging in FastAPI using Python's logging module and structlog. Configure your FastAPI app's logging easily with this guide.

- Repository: [Benav Labs/fastapi-boilerplate](https://github.com/benavlabs/fastapi-boilerplate)
- Tags: how-to-guide
- Published: 2026-02-26

---

**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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/core/config.py) and initialized in [`src/app/core/logger.py`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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 retain
- `FILE_LOG_LEVEL` and `CONSOLE_LOG_LEVEL` — independent severity thresholds
- `FILE_LOG_FORMAT_JSON` and `CONSOLE_LOG_FORMAT_JSON` — toggle between JSON and human-readable output
- `CONSOLE_LOG_INCLUDE_PATH` and `CONSOLE_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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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:

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

1. **File Handler** (lines 97‑106): A `RotatingFileHandler` writes to `logs/app.log`, rotating when `FILE_LOG_MAX_BYTES` is exceeded and preserving `FILE_LOG_BACKUP_COUNT` archives. It uses the formatter configured via `FILE_LOG_FORMAT_JSON`.

2. **Console Handler** (lines 110‑116): A `StreamHandler` writes to stdout/stderr, using the same formatter builder but respecting `CONSOLE_LOG_FORMAT_JSON` and 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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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:

```dotenv

# 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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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:

```python
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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/core/logger.py) after the existing handler configuration (following line 126):

```python
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.1` through `app.log.N` (archived logs up to `FILE_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 `FileLoggerSettings` and `ConsoleLoggerSettings` in [`src/app/core/config.py`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/src/app/core/config.py)
- **Dual output** through `RotatingFileHandler` (persistent storage) and `StreamHandler` (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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`](https://github.com/benavlabs/fastapi-boilerplate/blob/main/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`.