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:
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:
# 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 |
|---|---|---|
|
Yes |
Strips |
|
No (Yes with preset) |
Masks specific field names |
|
No (Yes with preset) |
Masks fields matching regex patterns |
|
No (Yes with |
Blocks high-risk field names ( |
|
No (explicit opt-in) |
Truncates long strings and appends |
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,pwdAPI keys:
api_key,apikey,api_tokenTokens:
token,access_token,refresh_token,auth_tokenSecrets:
secret,api_secret,client_secret,private_keyAuth headers:
authorization,auth_header
Adding Field-Based Redaction
To redact specific fields by name, use with_redaction():
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:
# These are equivalent:
.with_redaction(fields=["password"]) # Auto-prefixed to data.password
.with_redaction(fields=["data.password"], auto_prefix=False)
To disable auto-prefixing:
.with_redaction(fields=["password"], auto_prefix=False)
Nested Field Paths
Field paths support dot notation for nested objects:
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:
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:
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 dataGDPR_PII_UK- UK GDPR (includes NHS numbers, NI numbers)CCPA_PII- California Consumer Privacy ActHIPAA_PHI- HIPAA Protected Health InformationPCI_DSS- Payment card dataCREDENTIALS- Authentication secrets
Discovering Presets
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:
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:
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:
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:
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:
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:
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 |
|---|---|---|
|
16 |
Maximum nesting level to traverse |
|
1000 |
Maximum keys to examine |
Configure these via with_redaction():
logger = await (
LoggerBuilder()
.with_redaction(fields=["password"], max_depth=32, max_keys=5000)
.build_async()
)
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:
# 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:
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:
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 |
|---|---|
|
Data the developer knows is sensitive at log time |
|
Safety net for fields developers might forget to mark |
|
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:
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:
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:
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 - Full preset documentation
Redaction Configuration - Complete redaction configuration
Configuration Reference - All settings options