Metadata-Version: 2.4
Name: fastapi-logkit
Version: 0.1.3
Summary: FastAPI logging bootstrap: dev-friendly console, production structlog JSON
Project-URL: Homepage, https://pypi.org/project/fastapi-logkit/
Project-URL: Repository, https://github.com/RevolutionaryWarrior/fastapi-logkit
Project-URL: Issues, https://github.com/RevolutionaryWarrior/fastapi-logkit/issues
Author: Byuckchon
License: MIT
Keywords: asgi,fastapi,logging,starlette,structlog,uvicorn
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: FastAPI
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: fastapi>=0.100.0
Requires-Dist: starlette>=0.27.0
Requires-Dist: structlog>=24.1.0
Provides-Extra: dev
Requires-Dist: httpx>=0.27.0; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: uvicorn>=0.23.0; extra == 'dev'
Provides-Extra: uvicorn
Requires-Dist: uvicorn>=0.23.0; extra == 'uvicorn'
Description-Content-Type: text/markdown

# 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

```bash
pip install fastapi-logkit
```

PyPI: [fastapi-logkit](https://pypi.org/project/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.

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

```python
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`**).

```python
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%**.

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

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

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

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

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