# One FastAPI logging config for dev + prod (Uvicorn/Gunicorn)

Stop maintaining separate logging configs for each environment. fapilog auto-detects whether you're in development or production and adapts formatting, log levels, and color output automatically.

## The Problem

A typical project starts with something like this:

```python
# config/logging_dev.py
LOG_LEVEL = "DEBUG"
LOG_FORMAT = "pretty"
LOG_COLORS = True

# config/logging_prod.py
LOG_LEVEL = "INFO"
LOG_FORMAT = "json"
LOG_COLORS = False
```

These configurations drift apart over time:
- Someone adds a field to prod logging but forgets dev
- Color codes in production break JSON parsers
- Debug logs flood production when someone forgets to change `LOG_LEVEL`

## The Solution: One Config That Adapts

```python
from fastapi import FastAPI
from fapilog.fastapi import FastAPIBuilder

app = FastAPI(
    lifespan=FastAPIBuilder()
        .with_preset("fastapi")
        .build()
)
```

This single configuration works everywhere:
- **Local development** (`uvicorn main:app --reload`): Pretty format, DEBUG level, colors enabled
- **Production** (Gunicorn workers): JSON format, INFO level, no colors

No environment-specific config files. No conditional imports. Just one line.

## What Changes Per Environment

fapilog detects your environment and adjusts defaults accordingly:

| Setting | Development (TTY) | Production (no TTY) |
|---------|------------------|---------------------|
| Format | Pretty (human-readable) | JSON (machine-parseable) |
| Level | DEBUG | INFO |
| Colors | Yes | No |

### How Detection Works

fapilog uses multiple signals to determine the environment:

1. **TTY detection**: Is stdout connected to a terminal?
   - Yes → Development defaults (pretty format, DEBUG)
   - No → Production defaults (JSON format, INFO)

2. **CI detection**: Common CI environment variables (`CI`, `GITHUB_ACTIONS`, `GITLAB_CI`, etc.)
   - Present → Forces INFO level, no colors

3. **Container detection**: Docker, Kubernetes, Lambda
   - Detected → Production-appropriate defaults

The priority order ensures containers and CI always get production behavior, while local terminals get developer-friendly output.

## Running in Different Environments

### Local Development (Uvicorn)

```bash
uvicorn main:app --reload
```

Output is pretty-printed with colors:

```
2026-01-21 10:30:00 INFO  Processing order order_id=12345 request_id=abc-123
```

### Production (Gunicorn)

```bash
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker
```

Output is JSON (no colors):

```json
{"timestamp": "2026-01-21T10:30:00.123Z", "level": "INFO", "message": "Processing order", "order_id": "12345", "request_id": "abc-123"}
```

### Docker

```dockerfile
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
```

Automatic JSON format—Docker containers don't have TTYs.

## Overriding Defaults

Sometimes you need to force specific behavior regardless of environment.

### Environment Variable Overrides

```bash
# Force DEBUG in production (temporary debugging)
export FAPILOG_CORE__LOG_LEVEL=DEBUG

# Force JSON in development (testing log aggregation locally)
export FAPILOG_CORE__SINKS='["stdout_json"]'
```

### Programmatic Overrides

```python
from fapilog.fastapi import FastAPIBuilder

# Always use JSON, even in development
app = FastAPI(
    lifespan=FastAPIBuilder()
        .with_preset("fastapi")
        .add_stdout_json()
        .build()
)

# Always DEBUG, even in production (not recommended)
app = FastAPI(
    lifespan=FastAPIBuilder()
        .with_preset("fastapi")
        .with_level("DEBUG")
        .build()
)
```

### Testing JSON Output Locally

To verify your JSON output before deploying:

```bash
# Pipe through jq to validate JSON
uvicorn main:app 2>&1 | jq .
```

Or force JSON format in code:

```python
from fapilog import get_async_logger

async def main():
    logger = await get_async_logger(format="json")
    await logger.info("Testing JSON output")
```

## Complete Example

```python
from fastapi import FastAPI, Depends
from fapilog.fastapi import FastAPIBuilder, get_request_logger

# Works everywhere - adapts to environment automatically
app = FastAPI(
    lifespan=FastAPIBuilder()
        .with_preset("fastapi")
        .build()
)

@app.get("/api/orders/{order_id}")
async def get_order(order_id: str, logger=Depends(get_request_logger)):
    await logger.info("Processing order", order_id=order_id)
    return {"order_id": order_id, "status": "shipped"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

Run locally:
```bash
python main.py  # Pretty output, DEBUG level
```

Run in production:
```bash
gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker  # JSON output, INFO level
```

## Going Deeper

- [FastAPI JSON Logging](fastapi-json-logging.md) - Detailed JSON output configuration
- [Environment Variables](../user-guide/environment-variables.md) - All configuration options
- [Configuration](../user-guide/configuration.md) - Settings hierarchy and precedence
- [Why Fapilog?](../why-fapilog.md) - How fapilog compares to other logging libraries
