Skip to main content

FastAPI logging bootstrap: dev-friendly console, production structlog JSON

Project description

fastapi-logkit

FastAPI logging bootstrap: readable console logs in development, structured structlog JSON to stdout in production-minded environments.

  • Prefer turning off Uvicorn/Gunicorn access logs and emitting HTTP access as structured middleware logs instead.
  • The global exception handler logs unhandled request errors; process-level logs are normalized via Uvicorn/Gunicorn logger wiring.

Installation

pip install fastapi-logkit

PyPI: fastapi-logkit

Usage

fastapi_logkit does not read OS environment variables. Read deploy env, log level, etc. in your app and pass values into helpers.

import os
from fastapi import FastAPI
from fastapi_logkit import setup_logging
from fastapi_logkit.fastapi import install_logkit

def app_env() -> str:
    return (os.getenv("APP_ENV") or os.getenv("ENVIRONMENT") or "development").strip()

cfg = setup_logging(
    env=app_env(),
    level=os.getenv("LOG_LEVEL", "INFO").strip().upper(),
)

app = FastAPI()
install_logkit(app, config=cfg)
  • With install_logkit(..., config=cfg), the exception handler mirrors cfg.json_logs (structured JSON vs app.error text).
  • If access_log is omitted, it defaults from is_production_environment(cfg.env).

When running Uvicorn, pass log_config from get_uvicorn_log_config_for_run. After setup_logging / configure_logging, the returned LogKitConfig is kept as the active config; get_uvicorn_log_config_for_run() reads it (via get_active_logkit_config) so silence_uvicorn matches bootstrap without repeating config=:

from fastapi_logkit.integrations.uvicorn import get_uvicorn_log_config_for_run

cfg = setup_logging(env=app_env(), level=os.getenv("LOG_LEVEL", "INFO").strip().upper())
uvicorn.run(
    "main:app",
    log_config=get_uvicorn_log_config_for_run(),
)

Pass config= or silence_uvicorn= / env= when you need a one-off that differs from the last setup_logging result. If nothing has called setup_logging yet, omitting config falls back to resolving env (default "development") and is_json_logging_environment for silence, same as before.

For isolated tests, clear_active_logkit_config() (in fastapi_logkit.bootstrap) clears the stored config.

Production-style run: uvicorn main:app --host 0.0.0.0 --port 8000 --no-access-log


Behaviour summary (arguments / helpers)

API / helper Description
setup_logging(env=..., level="INFO", json_logs=None, ...) Omit json_logs to derive it from is_json_logging_environment(env) (production / prod / staging → JSON pipeline). Stores the resulting LogKitConfig as the active config for helpers below.
get_active_logkit_config() The LogKitConfig from the last successful setup_logging / configure_logging, or None. Also exported from fastapi_logkit.
clear_active_logkit_config() Clears the active config (fastapi_logkit.bootstrap; useful between test cases).
get_uvicorn_log_config_for_run(...) Uvicorn log_config dict. With no args after setup_logging, uses active silence_uvicorn; otherwise config= / silence_uvicorn= / env= as documented in the integration module.
is_json_logging_environment(env) Whether env is treated as a JSON-logging env; string only (does not touch os).
is_production_environment(env) True only for production / prod — default access middleware install in install_logkit.
resolve_env_name(env) If env is None, returns "development" (no env var lookup).
configure_logging(...) Alias of setup_logging.

Body redaction, proxy IP trust, and which access lines are emitted are configured via AccessLogOptions + install_logkit(..., access_log_options=...).

Proxy headers and originIP

Access logs include clientIP (the immediate TCP peer from ASGI scope["client"]) and originIP (intended “client behind proxies” when enabled).

  • AccessLogOptions.trust_proxy_headers defaults to False. In that case originIP matches clientIP (forwarded headers are ignored), which avoids spoofed X-Forwarded-For when the app is not behind a trusted edge proxy.
  • With trust_proxy_headers=True, originIP is resolved in order: first hop of X-Forwarded-For (comma-separated list), else X-Real-IP, else the same value as clientIP. Header names are compared case-insensitively as received in the ASGI scope.

Only set trust_proxy_headers=True when a trusted reverse proxy (nginx, a load balancer, etc.) terminates TLS and sets or overwrites these headers for you; otherwise clients can forge them.

Pass the flag via install_logkit(..., access_log_options=AccessLogOptions(trust_proxy_headers=True)) and/or setup_logging(..., access_log_options=AccessLogOptions(trust_proxy_headers=True)) so it lives on LogKitConfig.access_log when you reuse config=.

HTTP access line threshold

Each access line is classified with a stdlib severity: 2xx/3xx → INFO, 4xx → WARNING, 5xx → ERROR. A line is emitted only if that severity is the configured minimum.

  • By default, install_logkit(..., config=cfg) sets the access minimum from cfg.level (same string as setup_logging(..., level=...)). For example, level="WARNING" suppresses successful 2xx/3xx access lines but still records 4xx and 5xx.
  • Set AccessLogOptions(access_min_level=logging.WARNING) (or another numeric level) to override the threshold without changing the global structlog floor.

Access log sampling (optional)

After the access line passes access_min_level, you may thin lines with access_sample_rate_by_status and/or access_sample_rate_by_endpoint on AccessLogOptions.

  • By status (priority): mapping from an exact HTTP status (int) and/or class keys "2xx", "3xx", "4xx", "5xx" (case-insensitive) to a rate in [0.0, 1.0]. Lookup order: exact code, then class for that response. Built-in defaults are always merged: "4xx": 1.0, "5xx": 1.0 (override in your map if needed). Recommended: keep "4xx" and "5xx" at 1.0 so client/server errors are not dropped when endpoints are throttled.
  • By endpoint: ordered (pattern, rate) pairs. Used only when no status rule matched for that response (typical for 2xx/3xx). The endPoint string (route template when available) is matched by prefix; the longest matching pattern wins, with earlier entries breaking ties at the same length. Empty patterns are ignored. None or an empty sequence means 1.0 on this axis.
  • Both set: if a status rule applies, effective = r_status (endpoint ignored). Otherwise effective = r_endpoint. Example: ("/health", 0.0) plus 500: 1.0 (or built-in 5xx) — a 500 on /health is still logged; 200 on /health is not.
  • A line is kept iff random.random() < effective (rates clamped; effective >= 1.0 always keeps, effective <= 0.0 always drops).

Wildcard/fnmatch is not supported; use prefix strings only.

Sampling configuration examples

Import AccessLogOptions from fastapi_logkit.config and pass it to install_logkit and/or setup_logging(..., access_log_options=...).

1. Silence health-check success lines, still log errors

GET /health 200 is dropped; 4xx/5xx on the same path are still logged (built-in "4xx" / "5xx" at 1.0, status rules win over endpoint 0).

from fastapi_logkit.config import AccessLogOptions

access_opts = AccessLogOptions(
    access_sample_rate_by_endpoint=[
        ("/health", 0.0),
        ("/ready", 0.0),
    ],
    # Optional but recommended — documents intent; same as built-in defaults:
    access_sample_rate_by_status={
        "4xx": 1.0,
        "5xx": 1.0,
    },
)

install_logkit(app, config=cfg, access_log_options=access_opts)

2. Sample most successful traffic, keep all errors

Log about 10% of 2xx responses; 4xx/5xx stay at 100%.

access_opts = AccessLogOptions(
    access_sample_rate_by_status={
        "2xx": 0.1,
        "4xx": 1.0,
        "5xx": 1.0,
    },
)

3. Status-only: drop a specific code, sample a class

access_opts = AccessLogOptions(
    access_sample_rate_by_status={
        200: 0.0,      # never log 200
        201: 0.25,     # 25% of 201
        "3xx": 0.5,    # 50% of redirects
    },
)

4. Endpoint-only: throttle noisy paths (2xx/3xx)

Status map omitted; only 2xx/3xx without a status rule use endpoint rates. Longer prefixes win.

access_opts = AccessLogOptions(
    access_sample_rate_by_endpoint=[
        ("/metrics", 0.0),
        ("/api/v1/events", 0.05),
        ("/api/v1", 0.2),           # other /api/v1/* routes ~20%
    ],
)

5. Both axes: status wins when it matches

200 uses status rate 0.5; endpoint ("/", 0.1) is ignored for 200. A 404 uses built-in 4xx: 1.0, not the / endpoint rate.

access_opts = AccessLogOptions(
    access_sample_rate_by_status={200: 0.5, "4xx": 1.0, "5xx": 1.0},
    access_sample_rate_by_endpoint=[("/", 0.1)],
)

6. Bootstrap once on LogKitConfig

from fastapi_logkit import setup_logging
from fastapi_logkit.config import AccessLogOptions
from fastapi_logkit.fastapi import install_logkit

access_opts = AccessLogOptions(
    access_sample_rate_by_endpoint=[("/health", 0.0)],
    access_sample_rate_by_status={"4xx": 1.0, "5xx": 1.0},
)

cfg = setup_logging(
    env="production",
    level="INFO",
    access_log_options=access_opts,
)
install_logkit(app, config=cfg)  # reuses cfg.access_log unless access_log_options= overrides
Goal Typical settings
No health 200 lines access_sample_rate_by_endpoint=[("/health", 0.0)]
Still log health 500 Default "5xx": 1.0 (or explicit in status map)
Thin 2xx globally "2xx": 0.1 in access_sample_rate_by_status
Never thin 4xx/5xx Keep "4xx": 1.0, "5xx": 1.0 (recommended)

JSON line fields (JSON mode)

Each stdout line is produced by structlog.processors.JSONRenderer(), with fields merged as follows:

Category Fields
Common (processors) logLevel — numeric Pino-like level (e.g. info=30). The event key is stripped right before rendering.
Request context (RequestContextMiddleware) request_id, method merged from structlog contextvars.
Access log (AccessLogMiddleware) statusCode, userAgent, clientIP, originIP, dateTime (ms timestamp), endPoint, pathParams, method, traceBack when present. For 4xx/5xx with body logging enabled, requestBody.
JSON unhandled_exception_handler without access middleware on the stack traceBack, endPoint, method, statusCode (500). May skip when access middleware already logged the same failure.
App kwargs Any keys passed to logger.info("name", **fields) are included verbatim.

See the fastapi_logkit/ source for processors and middleware details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

fastapi_logkit-0.1.3.tar.gz (18.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

fastapi_logkit-0.1.3-py3-none-any.whl (18.6 kB view details)

Uploaded Python 3

File details

Details for the file fastapi_logkit-0.1.3.tar.gz.

File metadata

  • Download URL: fastapi_logkit-0.1.3.tar.gz
  • Upload date:
  • Size: 18.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for fastapi_logkit-0.1.3.tar.gz
Algorithm Hash digest
SHA256 607bdc0402f121aa843d3b9bf006d2a98a84e95f020864d57bcfab00eb18c12b
MD5 396fc4ecb8d1dad0b4d97619240e02b6
BLAKE2b-256 33ed7cf007dcdfe2e5690865728d2e573d28fd5da2d2ff80e9156e709a40bf48

See more details on using hashes here.

File details

Details for the file fastapi_logkit-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: fastapi_logkit-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 18.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.14.5

File hashes

Hashes for fastapi_logkit-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 ee79e27f079833c7769a128c04b08f55f4b42010308e2c258859bb4bdaac3bd1
MD5 97c0915fbaa0c7ca2834ba366f7f32ce
BLAKE2b-256 2c0a7934a51f862b0da8f7d19147cff0c8d52daea234b4296520008684c3d6fd

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page