Skip to content

Commit 60a76dc

Browse files
committed
initial import
Signed-off-by: Damien Degois <[email protected]>
0 parents  commit 60a76dc

13 files changed

+1298
-0
lines changed

.github/workflows/release.yml

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
name: "Build and release"
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
tags:
7+
- "v*"
8+
9+
jobs:
10+
build-and-release:
11+
runs-on: "ubuntu-latest"
12+
permissions:
13+
contents: write
14+
id-token: write
15+
16+
steps:
17+
- uses: actions/checkout@v3
18+
19+
- uses: actions/setup-python@v4
20+
21+
- name: Install requirements
22+
run: |
23+
pip install --upgrade pip
24+
pip install -r requirements-release.txt
25+
26+
- name: Build
27+
run: |
28+
VERSION=${GITHUB_REF_NAME##v}
29+
echo VERSION=$VERSION
30+
echo $VERSION > VERSION
31+
python -m build -s -w
32+
33+
- name: Publish package distributions to ${{ vars.PYPI_REPOSITORY }}
34+
uses: pypa/gh-action-pypi-publish@release/v1
35+
with:
36+
repository-url: ${{ vars.PYPI_REPOSITORY }}
37+
38+
- name: Create release
39+
env:
40+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
41+
tag: ${{ github.ref_name }}
42+
run: |
43+
gh release create "$tag" \
44+
--repo="$GITHUB_REPOSITORY" \
45+
--title="${GITHUB_REPOSITORY#*/} ${tag}" \
46+
--generate-notes

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
dist
2+
build
3+
fastapi_structured_logging.egg-info
4+
venv
5+
__pycache__
6+
.mypy_cache/
7+
*.pyc
8+
.vscode
9+
.coverage
10+
cov.xml
11+
VERSION

.pre-commit-config.yaml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# See https://pre-commit.com for more information
2+
# See https://pre-commit.com/hooks.html for more hooks
3+
repos:
4+
- repo: https://github.com/psf/black
5+
rev: 22.3.0
6+
hooks:
7+
- id: black
8+
args: [--safe, --line-length=110]
9+
10+
- repo: https://github.com/pre-commit/pre-commit-hooks
11+
rev: v3.2.0
12+
hooks:
13+
- id: trailing-whitespace
14+
- id: end-of-file-fixer
15+
- id: check-yaml
16+
args: [--allow-multiple-documents]
17+
- id: check-toml
18+
- id: check-added-large-files
19+
- id: check-merge-conflict
20+
- id: detect-private-key
21+
- id: debug-statements
22+
language_version: python3
23+
24+
- repo: https://github.com/PyCQA/flake8
25+
rev: 5.0.4
26+
hooks:
27+
- id: flake8
28+
args: [--max-line-length, "110"]
29+
language_version: python3
30+
31+
32+
- repo: https://github.com/asottile/reorder_python_imports
33+
rev: v2.6.0
34+
hooks:
35+
- id: reorder-python-imports
36+
args: ["--application-directories=.:src", "--py36-plus"]
37+
38+
- repo: https://github.com/asottile/pyupgrade
39+
rev: v2.29.0
40+
hooks:
41+
- id: pyupgrade
42+
args: [--py36-plus]
43+
44+
- repo: https://github.com/Yelp/detect-secrets
45+
rev: v1.3.0
46+
hooks:
47+
- id: detect-secrets
48+
49+
- repo: https://github.com/pre-commit/mirrors-mypy
50+
rev: v0.910
51+
hooks:
52+
- id: mypy
53+
files: ^(src/|examples/)
54+
args:
55+
- --check-untyped-defs
56+
- --disallow-any-generics
57+
- --ignore-missing-imports
58+
- --no-implicit-optional
59+
- --show-error-codes
60+
- --strict-equality
61+
- --warn-redundant-casts
62+
- --warn-return-any
63+
- --warn-unreachable
64+
- --warn-unused-configs
65+
- --no-implicit-reexport
66+
additional_dependencies:
67+
- types-orjson

README.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# fastapi-structured-logging
2+
3+
fastapi-structured-logging
4+
5+
fastapi-structured-logging is a lightweight Python module that provides structured logging utilities and a configurable FastAPI access logging middleware. It configures `structlog` for JSON or console output, enriches log events with OpenTelemetry trace and span identifiers, and exposes an `AccessLogMiddleware` that can record request method, path, query parameters, client IP, user agent, status codes, processing time and more.
6+
7+
The middleware supports filtering, trusted-proxy handling, custom fields and messages, and integrates cleanly with existing Python logging to produce consistent, machine-readable access logs for observability and tracing.
8+
9+
## Usage
10+
11+
```python
12+
from fastapi import FastAPI
13+
14+
import fastapi_structured_logging
15+
16+
# Set output to text if stdout is a tty, structured json if not
17+
fastapi_structured_logging.setup_logging()
18+
19+
logger = fastapi_structured_logging.get_logger()
20+
21+
app = FastAPI()
22+
23+
app.add_middleware(fastapi_structured_logging.AccessLogMiddleware)
24+
25+
```
26+
27+
## Configuration Options
28+
29+
The library provides extensive configuration options to customize logging and access logging behavior.
30+
31+
### setup_logging() Options
32+
33+
- `json_logs` (Optional[bool]): Forces JSON output if True, console output if False. Defaults to JSON if stdout is not a tty (e.g., in containers or files). Example: `setup_logging(json_logs=True)` for always JSON logs.
34+
- `log_level` (str): Sets the logging level (e.g., "DEBUG", "INFO", "WARNING"). Defaults to "INFO". Example: `setup_logging(log_level="DEBUG")` to enable debug logging.
35+
36+
### AccessLogMiddleware Options
37+
38+
The middleware can be configured via an `AccessLogConfig` object passed to the middleware constructor. Example:
39+
40+
```python
41+
from fastapi_structured_logging import AccessLogConfig
42+
43+
config = AccessLogConfig(
44+
log_level="info",
45+
include_user_agent=False,
46+
exclude_paths={"/health"},
47+
custom_fields={"app_version": "1.0.0"}
48+
)
49+
app.add_middleware(fastapi_structured_logging.AccessLogMiddleware, config=config)
50+
```
51+
52+
Key options include:
53+
54+
- `enabled` (bool): Enables or disables access logging. Default: True.
55+
- `log_level` (str): Log level for access logs ("debug", "info", etc.). Default: "info".
56+
- `include_*` flags: Control which fields are logged, such as `include_method` (request method), `include_path` (request path), `include_query_params` (query parameters), `include_client_ip` (client IP), `include_user_agent` (user agent string), `include_forwarded_headers` (proxy headers), `include_status_code` (response status), `include_process_time` (processing time in ms), `include_content_length` (response content length), `include_referer` (referer header). All default to True except `include_referer` (False).
57+
- `exclude_*` sets: Filter out logs for specific paths (`exclude_paths`), methods (`exclude_methods`), status codes (`exclude_status_codes`), or paths only if status is 200 or 404 (`exclude_paths_if_ok_or_missing`).
58+
- `min_process_time` / `max_process_time` (Optional[float]): Only log requests with processing time within these bounds (in seconds).
59+
- `custom_message` (str): Custom log message. Default: "Access".
60+
- `custom_fields` (Dict[str, Any]): Additional fields to include in every log entry. Example: `{"app_version": "1.0.0"}`.
61+
- `logger_name` (str): Name of the logger to use. Default: "access_log".
62+
- `trusted_proxy` (List[str]): List of CIDR ranges for trusted proxies to extract real client IP. Example: `["10.0.0.0/8", "192.168.0.0/16"]`.
63+
64+
## Convenience functions
65+
66+
- `setup_logging()` initialize `structlog` to use line logging if stdout is a tty or JSONL if not (file, container output etc...)
67+
68+
- `get_logger()` return a `structlog` logger
69+
70+
- `json_serializer()` for fast serialization in `orjson`
71+
72+
```python
73+
74+
@app.exception_handler(RequestValidationError)
75+
async def validation_exception_handler(request: Request, exc: RequestValidationError):
76+
exc_str = f"{exc}".replace("\n", " ").replace(" ", " ")
77+
logger.error("validation exception", validation_error=json_serializer(exc.errors()))
78+
content = {"status_code": 10422, "message": exc_str, "data": None}
79+
return JSONResponse(content=content, status_code=status.HTTP_422_UNPROCESSABLE_ENTITY)
80+
81+
```
82+
83+
## Setup dev env
84+
85+
```bash
86+
uv venv
87+
uv pip install -r requirements-dev.txt
88+
pre-commit install
89+
```
90+
91+
## Test
92+
93+
```bash
94+
. venv/bin/activate
95+
uv pip install -e .[full]
96+
pytest --cov-report html --cov-report term --cov-report xml:cov.xml
97+
```
98+
99+
## Build
100+
101+
```bash
102+
echo x.y.z > VERSION
103+
uv pip install -r requirements-release.txt
104+
uv run python -m build -s -w
105+
```

examples/README.md

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
# fastapi-structured-logging Example
2+
3+
This example demonstrates how to integrate structured logging into a FastAPI application using the `fastapi-structured-logging` library. It showcases:
4+
5+
- Setting up structured logging with JSON output
6+
- Adding access log middleware for request logging
7+
- Custom exception handling with structured error logging
8+
- Basic FastAPI endpoints with contextual logging
9+
10+
## Prerequisites
11+
12+
- Python 3.9+
13+
- [uv](https://github.com/astral-sh/uv) package manager
14+
15+
## Installation
16+
17+
1. Create a virtual environment:
18+
19+
```bash
20+
uv venv
21+
```
22+
23+
2. Install dependencies:
24+
25+
```bash
26+
uv pip install -r requirements.txt
27+
```
28+
29+
3. Install OpenTelemetry auto-instrumentation:
30+
31+
```bash
32+
uv run opentelemetry-bootstrap -a requirements | uv pip install --requirement -
33+
```
34+
35+
## Integration
36+
37+
```python
38+
from fastapi import FastAPI
39+
40+
import fastapi_structured_logging
41+
42+
# Set output to text if stdout is a tty, structured json if not
43+
fastapi_structured_logging.setup_logging()
44+
45+
logger = fastapi_structured_logging.get_logger()
46+
47+
app = FastAPI()
48+
49+
app.add_middleware(fastapi_structured_logging.AccessLogMiddleware)
50+
51+
```
52+
53+
## Running the Example
54+
55+
Run the FastAPI application with OpenTelemetry instrumentation:
56+
57+
```bash
58+
uv run opentelemetry-instrument uvicorn example1:app
59+
```
60+
61+
The server will start on `http://0.0.0.0:8000`.
62+
63+
## Testing the Example
64+
65+
- Visit `http://localhost:8000/` for the root endpoint
66+
- Visit `http://localhost:8000/hello?who=World` for the hello endpoint
67+
- Try invalid requests to see validation error logging
68+
69+
## Expected Output
70+
71+
When running, you'll see structured JSON logs for:
72+
73+
- HTTP requests (via middleware)
74+
- Endpoint-specific messages
75+
- Error handling
76+
77+
Example log output (formatted for readability):
78+
79+
### Application log
80+
81+
```json
82+
{
83+
"context_info1": "value1",
84+
"context_info2": "value2",
85+
"trace_id": "4c2c057a18cfff3a0709e2c04454a60d",
86+
"span_id": "25d9ce4aab9e8ad9",
87+
"method": "GET",
88+
"client_ip": "127.0.0.1",
89+
"path": "/hello",
90+
"user_agent": "curl/8.5.0",
91+
"remote_host": "127.0.0.1",
92+
"query_params": "who=a",
93+
"logger": "example1",
94+
"level": "info",
95+
"timestamp": "2025-10-01T06:16:28.707054Z",
96+
"message": "log line message"
97+
}
98+
```
99+
100+
### Access log
101+
102+
```json
103+
{
104+
"status_code": 200,
105+
"process_time_ms": 0.985861,
106+
"content_length": 21,
107+
"trace_id": "4c2c057a18cfff3a0709e2c04454a60d",
108+
"span_id": "25d9ce4aab9e8ad9",
109+
"method": "GET",
110+
"client_ip": "127.0.0.1",
111+
"path": "/hello",
112+
"user_agent": "curl/8.5.0",
113+
"remote_host": "127.0.0.1",
114+
"query_params": "who=a",
115+
"logger": "access_log",
116+
"level": "info",
117+
"timestamp": "2025-10-01T06:16:28.707480Z",
118+
"message": "Access"
119+
}
120+
```

examples/example1.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
#!/usr/bin/env python3
2+
import uvicorn
3+
from fastapi import FastAPI
4+
5+
import fastapi_structured_logging
6+
7+
# Set output to text if stdout is a tty, structured json if not
8+
fastapi_structured_logging.setup_logging()
9+
10+
logger = fastapi_structured_logging.get_logger()
11+
12+
app = FastAPI()
13+
14+
app.add_middleware(fastapi_structured_logging.AccessLogMiddleware)
15+
16+
17+
@app.get("/")
18+
async def root():
19+
logger.info("Handling root endpoint accessed")
20+
return {"message": "Hello World"}
21+
22+
23+
@app.get("/hello")
24+
def hello(who: str):
25+
logger.info(
26+
"log line message",
27+
context_info1="value1",
28+
context_info2="value2",
29+
)
30+
return {"message": f"Hello {who}"}
31+
32+
33+
if __name__ == "__main__":
34+
uvicorn.run(app, host="0.0.0.0", port=8000)

examples/requirements.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-r ../requirements.txt
2+
uvicorn[standard]
3+
opentelemetry-api
4+
opentelemetry-sdk
5+
opentelemetry-distro
6+
opentelemetry-exporter-otlp
7+
..

0 commit comments

Comments
 (0)