# Redacting secrets and PII in FastAPI logs (Authorization, tokens, fields)

Sensitive data in logs is a security and compliance risk. Authorization headers, API tokens, passwords, and personal data regularly leak into logs. fapilog provides built-in redaction with sensible defaults and extensible patterns.

## Safe by Default

fapilog redacts URL credentials automatically—no configuration required:

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

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

With this setup, URLs containing credentials are automatically scrubbed:

```python
# What you log
await logger.info("Connecting to database", url="postgres://admin:secret123@db.example.com/mydb")

# What appears in logs
{"message": "Connecting to database", "url": "postgres://db.example.com/mydb"}
```

The `url_credentials` redactor is enabled by default and strips `user:pass@` from any URL-like strings.

## What Gets Redacted by Default

fapilog ships with five built-in redactors:

| Redactor | Enabled by Default | What It Does |
|----------|-------------------|--------------|
| `url_credentials` | Yes | Strips `user:pass@` from URLs |
| `field_mask` | No (Yes with preset) | Masks specific field names |
| `regex_mask` | No (Yes with preset) | Masks fields matching regex patterns |
| `field_blocker` | No (Yes with `hardened`) | Blocks high-risk field names (`body`, `payload`, etc.) |
| `string_truncate` | No (explicit opt-in) | Truncates long strings and appends `[truncated]` marker |

The default configuration prioritizes safety without being overly aggressive. URL credentials are the most common accidental leak, so they're handled automatically.

### Full Protection with Presets

The `production`, `adaptive`, and `serverless` presets automatically apply the `CREDENTIALS` redaction preset, which masks:

- Passwords: `password`, `passwd`, `pwd`
- API keys: `api_key`, `apikey`, `api_token`
- Tokens: `token`, `access_token`, `refresh_token`, `auth_token`
- Secrets: `secret`, `api_secret`, `client_secret`, `private_key`
- Auth headers: `authorization`, `auth_header`

## Adding Field-Based Redaction

To redact specific fields by name, use `with_redaction()`:

```python
from fapilog import LoggerBuilder

logger = await (
    LoggerBuilder()
    .with_redaction(
        fields=["password", "ssn", "credit_card", "user.api_key"],
        mask="[REDACTED]",
    )
    .build_async()
)

# What you log
await logger.info("User signup", password="hunter2", email="user@example.com")

# What appears in logs
{"message": "User signup", "password": "[REDACTED]", "email": "user@example.com"}
```

### Auto-Prefix Behavior

By default, simple field names (without dots) are automatically prefixed with `data.` to match the log envelope structure:

```python
# These are equivalent:
.with_redaction(fields=["password"])           # Auto-prefixed to data.password
.with_redaction(fields=["data.password"], auto_prefix=False)
```

To disable auto-prefixing:

```python
.with_redaction(fields=["password"], auto_prefix=False)
```

### Nested Field Paths

Field paths support dot notation for nested objects:

```python
logger = await (
    LoggerBuilder()
    .with_redaction(fields=["user.password", "config.api_key"], auto_prefix=False)
    .build_async()
)

await logger.info(
    "Config loaded",
    user={"name": "alice", "password": "secret"},
    config={"api_key": "sk-123", "timeout": 30},
)
# user.password and config.api_key are masked; other fields preserved
```

## Adding Pattern-Based Redaction

For dynamic field names or broader matching, use regex patterns:

```python
logger = await (
    LoggerBuilder()
    .with_redaction(
        patterns=[
            r"(?i).*password.*",     # Any field containing "password"
            r"(?i).*secret.*",       # Any field containing "secret"
            r"(?i).*token.*",        # Any field containing "token"
            r"(?i)context\.auth.*",  # Auth fields in context
        ]
    )
    .build_async()
)
```

Patterns match against the full dot-path of fields (e.g., `context.auth_token`), not field values.

## Using Compliance Presets

For regulation compliance, use built-in redaction presets:

```python
from fapilog import LoggerBuilder

# GDPR compliance
logger = await (
    LoggerBuilder()
    .with_redaction(preset="GDPR_PII")
    .build_async()
)

# HIPAA compliance
logger = await (
    LoggerBuilder()
    .with_redaction(preset="HIPAA_PHI")
    .build_async()
)

# Multiple regulations
logger = await (
    LoggerBuilder()
    .with_redaction(preset=["GDPR_PII", "PCI_DSS"])
    .build_async()
)
```

Available presets:
- `GDPR_PII` - EU GDPR personal data
- `GDPR_PII_UK` - UK GDPR (includes NHS numbers, NI numbers)
- `CCPA_PII` - California Consumer Privacy Act
- `HIPAA_PHI` - HIPAA Protected Health Information
- `PCI_DSS` - Payment card data
- `CREDENTIALS` - Authentication secrets

### Discovering Presets

```python
from fapilog import LoggerBuilder

# List all available presets
presets = LoggerBuilder.list_redaction_presets()
print(presets)  # ['CCPA_PII', 'CREDENTIALS', 'GDPR_PII', ...]

# Get preset details
info = LoggerBuilder.get_redaction_preset_info("GDPR_PII")
print(info["description"])  # "GDPR Article 4 personal data identifiers"
print(info["fields"][:5])   # ['email', 'phone', 'name', ...]
```

## Combining Presets with Custom Fields

Presets and custom fields are additive:

```python
logger = await (
    LoggerBuilder()
    .with_redaction(preset="GDPR_PII")
    .with_redaction(fields=["internal_user_id", "employee_badge"])
    .build_async()
)
```

## Configuration via Settings

You can also configure redaction through Settings:

```python
from fapilog import Settings

settings = Settings()

# Enable specific redactors
settings.core.redactors = ["field_mask", "regex_mask", "url_credentials"]

# Configure field_mask
settings.redactor_config.field_mask.fields_to_mask = [
    "password",
    "authorization",
    "api_key",
]
settings.redactor_config.field_mask.mask_string = "[REDACTED]"

# Configure regex_mask
settings.redactor_config.regex_mask.patterns = [
    r"(?i).*secret.*",
    r"(?i).*token.*",
]
```

Or via environment variables:

```bash
export FAPILOG_CORE__REDACTORS='["field_mask", "url_credentials"]'
export FAPILOG_REDACTOR_CONFIG__FIELD_MASK__FIELDS_TO_MASK='["password", "ssn"]'
```

## Testing Your Redaction Rules

Verify that sensitive data is actually redacted before deploying:

```python
import pytest
from fapilog import LoggerBuilder
from fapilog.testing import capture_logs


@pytest.mark.asyncio
async def test_password_is_redacted():
    """Verify password fields are masked in log output."""
    async with capture_logs() as logs:
        logger = await (
            LoggerBuilder()
            .with_redaction(fields=["password"])
            .build_async()
        )
        await logger.info("Login attempt", username="alice", password="hunter2")

    # Password value should not appear
    assert "hunter2" not in logs.text
    # Mask should appear instead
    assert "***" in logs.text or "[REDACTED]" in logs.text


@pytest.mark.asyncio
async def test_ssn_pattern_redacted():
    """Verify SSN-like fields are caught by regex pattern."""
    async with capture_logs() as logs:
        logger = await (
            LoggerBuilder()
            .with_redaction(patterns=[r"(?i).*ssn.*"])
            .build_async()
        )
        await logger.info("User data", user_ssn="123-45-6789")

    assert "123-45-6789" not in logs.text


@pytest.mark.asyncio
async def test_url_credentials_stripped():
    """Verify URL credentials are removed by default."""
    async with capture_logs() as logs:
        logger = await LoggerBuilder().build_async()
        await logger.info(
            "Database URL",
            url="postgres://admin:supersecret@db.example.com/app",
        )

    # Credentials should be stripped
    assert "supersecret" not in logs.text
    assert "admin:" not in logs.text
    # Host should remain
    assert "db.example.com" in logs.text
```

### CI/CD Redaction Verification

Add a test that fails if sensitive patterns appear in logs:

```python
FORBIDDEN_PATTERNS = [
    r"\b[A-Za-z0-9]{32,}\b",  # Long tokens
    r"\b\d{3}-\d{2}-\d{4}\b",  # SSN format
    r"password\s*[:=]\s*\S+",  # password=value
]


@pytest.mark.asyncio
async def test_no_sensitive_patterns_in_logs():
    """Fail if any forbidden pattern appears in log output."""
    import re

    async with capture_logs() as logs:
        # Run your application code here
        pass

    for pattern in FORBIDDEN_PATTERNS:
        matches = re.findall(pattern, logs.text, re.IGNORECASE)
        assert not matches, f"Sensitive pattern found: {pattern} -> {matches}"
```

## Auditing What Gets Redacted

To see what redaction is happening, enable diagnostics:

```python
from fapilog import LoggerBuilder

logger = await (
    LoggerBuilder()
    .with_redaction(fields=["password"])
    .with_diagnostics(enabled=True)
    .build_async()
)
```

Diagnostics will log warnings if redaction encounters issues (max depth exceeded, unredactable fields, etc.).

## Performance Guardrails

Redactors have built-in limits to prevent performance issues with deeply nested or large objects:

| Setting | Default | Purpose |
|---------|---------|---------|
| `max_depth` | 16 | Maximum nesting level to traverse |
| `max_keys_scanned` | 1000 | Maximum keys to examine |

Configure these via `with_redaction()`:

```python
logger = await (
    LoggerBuilder()
    .with_redaction(fields=["password"], max_depth=32, max_keys=5000)
    .build_async()
)
```

(declaring-sensitive-data-at-log-time)=
## Declaring Sensitive Data at Log Time

Instead of relying solely on redactor configuration, you can mark data as sensitive when you log it. Pass a `sensitive=` dict and fapilog masks the values **at envelope construction time**, before the event reaches the queue or any sink:

```python
# Developer declares intent — values are masked immediately
await logger.info(
    "User signup",
    sensitive={"email": "alice@example.com", "ssn": "123-45-6789"},
    plan="free",
)

# What appears in logs
{"message": "User signup", "data": {"sensitive": {"email": "***", "ssn": "***"}, "plan": "free"}}
```

`pii=` is an alias for teams that prefer that term:

```python
await logger.info("Payment processed", pii={"card_number": "4111-1111-1111-1111"})
```

Both keywords route to `data.sensitive` in the envelope. Nested dicts and lists are recursively masked:

```python
await logger.info(
    "Checkout",
    sensitive={"card": {"number": "4111-1111-1111-1111", "cvv": "123"}},
)
# data.sensitive.card.number == "***", data.sensitive.card.cvv == "***"
```

### When to Use Each Approach

| Approach | Best For |
|----------|----------|
| `sensitive=` / `pii=` | Data the developer **knows** is sensitive at log time |
| `with_redaction(fields=...)` | Safety net for fields developers might forget to mark |
| `with_redaction(preset=...)` | Compliance-driven blanket coverage |

These are complementary. `sensitive=` gives developer-declared intent; redactors remain the safety net for fields not explicitly marked.

## Blocking High-Risk Fields

Some fields should never appear in logs — request bodies, raw payloads, and response dumps can contain arbitrary user data. The `field_blocker` redactor replaces these values entirely:

```python
from fapilog import LoggerBuilder

logger = await (
    LoggerBuilder()
    .with_redaction(block_fields=["body", "request_body", "payload", "raw"])
    .build_async()
)

# What you log
await logger.info("Request received", body='{"ssn": "123-45-6789"}', method="POST")

# What appears in logs
{"message": "Request received", "data": {"body": "[REDACTED:HIGH_RISK_FIELD]", "method": "POST"}}
```

Each blocked field emits a policy-violation diagnostic, so you can monitor violations via the `fapilog_policy_violations_total` metric.

The `hardened` preset enables `field_blocker` by default with a sensible blocklist. To exempt a specific field, use `allowed_fields` in the redactor config:

```python
from fapilog import Settings

settings = Settings(
    redactor_config={
        "field_blocker": {
            "blocked_fields": ["body", "payload"],
            "allowed_fields": ["body"],  # Exempt "body" from blocking
        },
    },
)
```

## Truncating Long Strings

Large string values (stack traces, serialized payloads, base64 blobs) can bloat log events. The `string_truncate` redactor caps string length and appends a `[truncated]` marker:

```python
from fapilog import LoggerBuilder

logger = await (
    LoggerBuilder()
    .with_redaction(max_string_length=500)
    .build_async()
)

# What you log
await logger.info("Error details", traceback="..." * 1000)

# What appears in logs — string truncated to 500 chars + marker
{"message": "Error details", "data": {"traceback": "......[truncated]"}}
```

This redactor is disabled by default (`max_string_length=None`). Set a value to enable it. The truncation happens after all other redactors, so masked values are not affected.

## Going Deeper

- [Redaction Presets](../redaction/presets.md) - Full preset documentation
- [Redaction Configuration](../redaction/configuration.md) - Complete redaction configuration
- [Configuration Reference](../user-guide/configuration.md) - All settings options
