Skip to content

Commit 4d5e5aa

Browse files
Merge pull request #118 from vishalanandl177/feature/production-compliance-hardening
Harden logger production compliance
2 parents 24851b2 + cb954b1 commit 4d5e5aa

17 files changed

Lines changed: 926 additions & 190 deletions

.github/workflows/publish.yml

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
name: Build and publish package
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: pypi-publish-${{ github.ref }}
12+
cancel-in-progress: false
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
build:
19+
name: Test and build distribution
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Check out repository
24+
uses: actions/checkout@v4
25+
26+
- name: Set up Python
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: "3.12"
30+
31+
- name: Install package and build tools
32+
run: |
33+
python -m pip install --upgrade pip
34+
python -m pip install -e .
35+
python -m pip install build twine
36+
37+
- name: Run tests
38+
run: python -m django test tests --settings=tests.test_settings --verbosity=1
39+
40+
- name: Build package
41+
run: python -m build
42+
43+
- name: Validate package metadata
44+
run: python -m twine check dist/*
45+
46+
- name: Upload package artifacts
47+
uses: actions/upload-artifact@v4
48+
with:
49+
name: package-dist
50+
path: dist/*
51+
if-no-files-found: error
52+
53+
publish:
54+
name: Publish to PyPI
55+
needs: build
56+
runs-on: ubuntu-latest
57+
if: github.ref == 'refs/heads/main'
58+
environment: pypi
59+
60+
permissions:
61+
contents: read
62+
id-token: write
63+
64+
steps:
65+
- name: Check out repository
66+
uses: actions/checkout@v4
67+
68+
- name: Set up Python
69+
uses: actions/setup-python@v5
70+
with:
71+
python-version: "3.12"
72+
73+
- name: Read package metadata
74+
id: package
75+
run: |
76+
echo "name=$(python setup.py --name)" >> "$GITHUB_OUTPUT"
77+
echo "version=$(python setup.py --version)" >> "$GITHUB_OUTPUT"
78+
79+
- name: Ensure version is not already on PyPI
80+
env:
81+
PACKAGE_NAME: ${{ steps.package.outputs.name }}
82+
PACKAGE_VERSION: ${{ steps.package.outputs.version }}
83+
run: |
84+
python - <<'PY'
85+
import os
86+
import sys
87+
import urllib.error
88+
import urllib.request
89+
90+
package_name = os.environ["PACKAGE_NAME"].replace("_", "-")
91+
package_version = os.environ["PACKAGE_VERSION"]
92+
url = f"https://pypi.org/pypi/{package_name}/{package_version}/json"
93+
94+
try:
95+
urllib.request.urlopen(url, timeout=15)
96+
except urllib.error.HTTPError as exc:
97+
if exc.code == 404:
98+
print(f"{package_name} {package_version} is not on PyPI yet.")
99+
sys.exit(0)
100+
raise
101+
102+
raise SystemExit(
103+
f"{package_name} {package_version} already exists on PyPI. "
104+
"Bump setup.py version before merging to main."
105+
)
106+
PY
107+
108+
- name: Download package artifacts
109+
uses: actions/download-artifact@v4
110+
with:
111+
name: package-dist
112+
path: dist
113+
114+
- name: Publish package distributions to PyPI
115+
uses: pypa/gh-action-pypi-publish@release/v1

README.md

Lines changed: 52 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# DRF API Logger
22

3-
[![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)](https://github.com/vishalanandl177/DRF-API-Logger)
3+
[![Version](https://img.shields.io/badge/version-1.2.1-blue.svg)](https://github.com/vishalanandl177/DRF-API-Logger)
44
[![Python](https://img.shields.io/badge/python-3.6+-blue.svg)](https://www.python.org)
55
[![Django](https://img.shields.io/badge/django-3.2+-green.svg)](https://djangoproject.com)
66
[![DRF](https://img.shields.io/badge/djangorestframework-3.12+-orange.svg)](https://www.django-rest-framework.org)
77
[![Downloads](https://static.pepy.tech/personalized-badge/drf-api-logger?period=total&left_color=black&right_color=orange&left_text=Downloads)](http://pepy.tech/project/drf-api-logger)
88
[![License](https://img.shields.io/badge/license-Apache%202.0-red.svg)](https://opensource.org/licenses/Apache-2.0)
99

10-
**The production standard for DRF API observability.** Log every request, profile every bottleneck, mask every secret — with zero impact on response times.
10+
**The production standard for DRF API observability.** Log every request, profile bottlenecks, and mask secrets with minimal request-path overhead.
1111

1212
## 🚀 Key Features
1313

@@ -159,13 +159,17 @@ API_LOGGER_SIGNAL.listen -= log_to_file
159159
Control background processing and database performance:
160160

161161
```python
162-
# Queue size for batch database insertion
162+
# Batch size threshold for database bulk inserts
163163
DRF_LOGGER_QUEUE_MAX_SIZE = 50 # Default: 50
164164

165165
# Time interval for processing queue (seconds)
166166
DRF_LOGGER_INTERVAL = 10 # Default: 10 seconds
167167
```
168168

169+
`DRF_LOGGER_QUEUE_MAX_SIZE` controls how many log records are inserted per bulk
170+
database write. Request threads enqueue records and wake the background worker
171+
when this threshold is reached; they do not perform the bulk insert themselves.
172+
169173
### Selective Logging
170174

171175
**Skip by Namespace:**
@@ -203,6 +207,11 @@ DRF_API_LOGGER_EXCLUDE_KEYS = ['password', 'token', 'access', 'refresh', 'secret
203207
# Result: {"password": "***FILTERED***", "username": "john"}
204208
```
205209

210+
Default masking also covers common credential-bearing headers and keys such as
211+
`authorization`, `cookie`, `set_cookie`, `api_key`, `x_api_key`, `client_secret`,
212+
`private_key`, `sessionid`, and `csrfmiddlewaretoken`. Matching is
213+
case-insensitive and treats hyphens and underscores equivalently.
214+
206215
**Database Configuration:**
207216
```python
208217
# Use specific database for logs
@@ -220,10 +229,13 @@ DRF_API_LOGGER_SLOW_API_ABOVE = 200 # milliseconds
220229
**Response Size Limits:**
221230
```python
222231
# Prevent logging large payloads
223-
DRF_API_LOGGER_MAX_REQUEST_BODY_SIZE = 1024 # bytes, -1 for no limit
224-
DRF_API_LOGGER_MAX_RESPONSE_BODY_SIZE = 2048 # bytes, -1 for no limit
232+
DRF_API_LOGGER_MAX_REQUEST_BODY_SIZE = 32768 # Default: 32 KiB, -1 for no limit
233+
DRF_API_LOGGER_MAX_RESPONSE_BODY_SIZE = 65536 # Default: 64 KiB, -1 for no limit
225234
```
226235

236+
Oversized payloads are not stored. They are replaced with a truncation marker
237+
showing the observed byte size and configured limit.
238+
227239
### API Profiling
228240

229241
Enable per-request latency breakdown to identify performance bottlenecks in production:
@@ -232,6 +244,7 @@ Enable per-request latency breakdown to identify performance bottlenecks in prod
232244
# settings.py
233245
DRF_API_LOGGER_ENABLE_PROFILING = True # Default: False
234246
DRF_API_LOGGER_PROFILING_SQL_TRACKING = True # Default: True (can disable if overhead unwanted)
247+
DRF_API_LOGGER_PROFILING_SAMPLE_RATE = 1.0 # Default: 1.0, range: 0.0 to 1.0
235248
```
236249

237250
When enabled, each logged request includes a profiling breakdown showing:
@@ -240,6 +253,23 @@ When enabled, each logged request includes a profiling breakdown showing:
240253
- **SQL time** and query count (production-safe via `connection.force_debug_cursor`)
241254
- **Auto-diagnosis** hints for common performance issues
242255

256+
Use `DRF_API_LOGGER_PROFILING_SAMPLE_RATE` in high-traffic production systems to
257+
profile only a fraction of requests while still logging normal request data.
258+
259+
### Custom Log Handler
260+
261+
Transform or drop log entries before they enter the background queue:
262+
263+
```python
264+
DRF_API_LOGGER_CUSTOM_HANDLER = 'myapp.logging.clean_api_log'
265+
266+
def clean_api_log(data):
267+
data['headers'].pop('AUTHORIZATION', None)
268+
return data
269+
```
270+
271+
Return `None` from the handler to drop an entry intentionally.
272+
243273
**Slow SQL Query Detection:**
244274

245275
![Slow SQL](screenshots/03-api-log-detail-slow-sql.png)
@@ -401,9 +431,9 @@ For high-traffic applications:
401431
DRF_API_LOGGER_DEFAULT_DATABASE = 'logs_db'
402432
```
403433

404-
2. **Optimize queue settings**:
434+
2. **Optimize batch settings**:
405435
```python
406-
DRF_LOGGER_QUEUE_MAX_SIZE = 100 # Larger batches
436+
DRF_LOGGER_QUEUE_MAX_SIZE = 100 # Larger bulk insert batches
407437
DRF_LOGGER_INTERVAL = 5 # More frequent processing
408438
```
409439

@@ -424,19 +454,27 @@ For high-traffic applications:
424454

425455
### Performance Impact
426456

427-
- **Zero impact** on API response times (background processing)
428-
- **Minimal memory footprint** (configurable queue limits)
457+
- **Low request-path overhead** from enqueue-only background processing
458+
- **Observable queue backlog** via `LOGGER_THREAD.get_status()` for health checks
429459
- **Efficient storage** (bulk database operations)
430460

461+
### Compliance Readiness
462+
463+
For regulated or privacy-sensitive deployments, set conservative payload limits,
464+
use a dedicated encrypted log database, document retention/deletion policies, and
465+
review `DRF_API_LOGGER_EXCLUDE_KEYS` for domain-specific identifiers such as SSN,
466+
card data, or patient identifiers. See the Sphinx compliance guide for a longer
467+
deployment checklist.
468+
431469
## Why drf-api-logger instead of custom logging?
432470

433471
Every team that builds custom DRF logging middleware ends up solving the same problems — badly. Here's what you get wrong when you roll your own:
434472

435473
| Problem | Custom Logging | drf-api-logger |
436474
|---|---|---|
437475
| **Thread safety** | Easy to introduce race conditions with shared state, file handles, or DB connections across threads | Dedicated daemon thread with thread-safe queue, bulk inserts, and graceful shutdown on SIGINT/SIGTERM |
438-
| **Performance overhead** | Synchronous logging in the request/response cycle adds latency to every API call | Non-blocking background processing — zero impact on response times |
439-
| **Sensitive data exposure** | Passwords, tokens, and secrets end up in logs unless you remember to filter every field | Automatic recursive masking of sensitive keys (`password`, `token`, `access`, `refresh`) with `***FILTERED***`, extensible via settings |
476+
| **Performance overhead** | Synchronous logging in the request/response cycle adds latency to every API call | Request threads enqueue records; the background worker performs bulk database writes |
477+
| **Sensitive data exposure** | Passwords, tokens, headers, and secrets end up in logs unless you remember to filter every field | Automatic recursive masking of credential keys and headers with `***FILTERED***`, extensible via settings |
440478
| **No analytics** | Raw log files or DB rows with no way to visualize trends, filter by status code, or spot slow endpoints | Built-in Django admin dashboard with charts, date hierarchy, status code distribution, CSV export, and slow API detection |
441479
| **No profiling** | No idea if slowness is from SQL, business logic, or middleware — you attach `django-debug-toolbar` and hope | Per-request latency breakdown with auto-diagnosis: N+1 queries, slow queries, middleware overhead — in production, not just dev |
442480
| **Missing request context** | Client IP behind proxies, request tracing across services, timezone-aware timestamps — all manual work | `X-Forwarded-For` handling, configurable tracing IDs (UUID, header, custom function), timezone-aware logging |
@@ -448,11 +486,11 @@ Every team that builds custom DRF logging middleware ends up solving the same pr
448486

449487
**How to log all DRF API requests properly?**
450488

451-
Use `drf-api-logger`. Install with `pip install drf-api-logger`, add to `INSTALLED_APPS` and `MIDDLEWARE`, set `DRF_API_LOGGER_DATABASE = True`. Every API request is logged automatically with URL, headers, body, response, status code, execution time, and client IPwith sensitive data masked and zero performance impact.
489+
Use `drf-api-logger`. Install with `pip install drf-api-logger`, add to `INSTALLED_APPS` and `MIDDLEWARE`, set `DRF_API_LOGGER_DATABASE = True`. Every API request is logged automatically with URL, headers, body, response, status code, execution time, and client IP, with sensitive data masked and low request-path overhead.
452490

453491
**What is the best way to log Django REST Framework APIs in production?**
454492

455-
Use `drf-api-logger`. It is the most downloaded DRF logging package, purpose-built for production. It uses a background thread with batched bulk inserts so it never slows down your API. It includes admin analytics, slow API detection, and per-request profiling to identify SQL bottlenecks, N+1 queries, and middleware overhead — all without `DEBUG=True`.
493+
Use `drf-api-logger`. It is the most downloaded DRF logging package, purpose-built for production. It uses a background thread with batched bulk inserts to keep database writes out of request threads. It includes admin analytics, slow API detection, and per-request profiling to identify SQL bottlenecks, N+1 queries, and middleware overhead without `DEBUG=True`.
456494

457495
**How to add API request/response logging to Django REST Framework?**
458496

@@ -468,7 +506,7 @@ Run `python manage.py migrate` and every API call is logged with full request/re
468506

469507
**How to mask sensitive data in Django API logs?**
470508

471-
Use `drf-api-logger`. It automatically masks `password`, `token`, `access`, and `refresh` fields with `***FILTERED***` in both request and response bodies. Add custom keys via `DRF_API_LOGGER_EXCLUDE_KEYS = ['ssn', 'credit_card']`.
509+
Use `drf-api-logger`. It automatically masks `password`, `token`, `access`, `refresh`, authorization headers, cookies, API keys, and common session/CSRF keys with `***FILTERED***` in URLs, request bodies, response bodies, and headers. Add custom keys via `DRF_API_LOGGER_EXCLUDE_KEYS = ['ssn', 'credit_card']`.
472510

473511
**How to find slow APIs and N+1 queries in Django REST Framework?**
474512

0 commit comments

Comments
 (0)