auditlog-fastapi 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- auditlog_fastapi-0.2.0/LICENSE +21 -0
- auditlog_fastapi-0.2.0/PKG-INFO +260 -0
- auditlog_fastapi-0.2.0/README.md +227 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/__init__.py +36 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/config.py +86 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/context.py +33 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/db/beanie_document.py +42 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/db/sqlalchemy_table.py +62 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/db/sqlmodel_model.py +33 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/db/tortoise_model.py +33 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/dependencies.py +17 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/exceptions.py +22 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/filters.py +34 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/middleware.py +170 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/models.py +48 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/registry.py +63 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/routes.py +39 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/storage/__init__.py +15 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/storage/asyncpg_storage.py +168 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/storage/base.py +44 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/storage/beanie_storage.py +70 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/storage/sqlalchemy_storage.py +135 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/storage/sqlmodel_storage.py +116 -0
- auditlog_fastapi-0.2.0/fastapi_audit_log/storage/tortoise_storage.py +69 -0
- auditlog_fastapi-0.2.0/pyproject.toml +64 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ahmed Elgamal
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: auditlog-fastapi
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Production-grade, reusable audit logging for FastAPI applications.
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Author: Ahmed Elgamal
|
|
7
|
+
Author-email: ahmed@example.com
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Provides-Extra: all
|
|
15
|
+
Provides-Extra: asyncpg
|
|
16
|
+
Provides-Extra: mongodb
|
|
17
|
+
Provides-Extra: sqlalchemy
|
|
18
|
+
Provides-Extra: sqlmodel
|
|
19
|
+
Provides-Extra: tortoise
|
|
20
|
+
Requires-Dist: aiomysql ; extra == "sqlalchemy" or extra == "tortoise" or extra == "sqlmodel" or extra == "all"
|
|
21
|
+
Requires-Dist: aiosqlite ; extra == "sqlalchemy" or extra == "tortoise" or extra == "sqlmodel" or extra == "all"
|
|
22
|
+
Requires-Dist: asyncpg (>=0.29) ; extra == "sqlalchemy" or extra == "tortoise" or extra == "sqlmodel" or extra == "asyncpg" or extra == "all"
|
|
23
|
+
Requires-Dist: beanie (>=1.20) ; extra == "mongodb" or extra == "all"
|
|
24
|
+
Requires-Dist: fastapi (>=0.100.0)
|
|
25
|
+
Requires-Dist: motor (>=3.0) ; extra == "mongodb" or extra == "all"
|
|
26
|
+
Requires-Dist: pydantic (>=2.0,<3.0)
|
|
27
|
+
Requires-Dist: pydantic-settings (>=2.0)
|
|
28
|
+
Requires-Dist: sqlalchemy[asyncio] (>=2.0.0) ; extra == "sqlalchemy" or extra == "all"
|
|
29
|
+
Requires-Dist: sqlmodel (>=0.0.14) ; extra == "sqlmodel" or extra == "all"
|
|
30
|
+
Requires-Dist: tortoise-orm (>=0.20) ; extra == "tortoise" or extra == "all"
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# auditlog-fastapi
|
|
34
|
+
|
|
35
|
+
Production-grade, reusable audit logging for FastAPI applications.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- **Multi-ORM Support:** SQLAlchemy 2 (async), Tortoise ORM, SQLModel, Beanie (MongoDB), and raw asyncpg.
|
|
40
|
+
- **Flexible Storage:** PostgreSQL, MySQL, MariaDB, SQLite, and MongoDB.
|
|
41
|
+
- **Explicit Configuration:** Clean API with `AuditConfig` and `configure()`.
|
|
42
|
+
- **Middleware Integration:** Capture request/response data automatically.
|
|
43
|
+
- **PII Masking:** Redact sensitive fields from logs.
|
|
44
|
+
- **Context Helpers:** Enrich audit logs from route handlers.
|
|
45
|
+
- **Lifespan Integration:** Automatic startup/shutdown of database connections.
|
|
46
|
+
- **Async & Non-blocking:** Built with performance and safety in mind.
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Basic install
|
|
52
|
+
pip install auditlog-fastapi
|
|
53
|
+
|
|
54
|
+
# With SQLAlchemy support (Postgres, MySQL, SQLite)
|
|
55
|
+
pip install auditlog-fastapi[sqlalchemy]
|
|
56
|
+
|
|
57
|
+
# With Tortoise ORM support
|
|
58
|
+
pip install auditlog-fastapi[tortoise]
|
|
59
|
+
|
|
60
|
+
# With SQLModel support
|
|
61
|
+
pip install auditlog-fastapi[sqlmodel]
|
|
62
|
+
|
|
63
|
+
# With MongoDB (Beanie) support
|
|
64
|
+
pip install auditlog-fastapi[mongodb]
|
|
65
|
+
|
|
66
|
+
# With raw asyncpg (PostgreSQL only) support
|
|
67
|
+
pip install auditlog-fastapi[asyncpg]
|
|
68
|
+
|
|
69
|
+
# Everything
|
|
70
|
+
pip install auditlog-fastapi[all]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick Start (SQLAlchemy + SQLite)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from fastapi import FastAPI
|
|
77
|
+
from fastapi_audit_log import AuditMiddleware, AuditConfig, create_audit_lifespan
|
|
78
|
+
|
|
79
|
+
# 1. Configure the audit log
|
|
80
|
+
config = AuditConfig(
|
|
81
|
+
orm="sqlalchemy",
|
|
82
|
+
dsn="sqlite+aiosqlite:///./audit.db",
|
|
83
|
+
table_name="audit_logs",
|
|
84
|
+
auto_create_table=True,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# 2. Create lifespan handler
|
|
88
|
+
app = FastAPI(lifespan=create_audit_lifespan(config))
|
|
89
|
+
|
|
90
|
+
# 3. Add middleware
|
|
91
|
+
app.add_middleware(
|
|
92
|
+
AuditMiddleware,
|
|
93
|
+
log_request_body=True
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
@app.get("/")
|
|
97
|
+
async def root():
|
|
98
|
+
return {"message": "Hello World"}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## User Configuration Guide
|
|
102
|
+
|
|
103
|
+
### SQLAlchemy + PostgreSQL
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
config = AuditConfig(
|
|
107
|
+
orm="sqlalchemy",
|
|
108
|
+
dsn="postgresql+asyncpg://user:pass@localhost:5432/mydb",
|
|
109
|
+
table_name="audit_logs",
|
|
110
|
+
auto_create_table=True,
|
|
111
|
+
sqlalchemy_pool_size=10,
|
|
112
|
+
mask_fields=["password", "token"],
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### SQLAlchemy + MySQL
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
config = AuditConfig(
|
|
120
|
+
orm="sqlalchemy",
|
|
121
|
+
dsn="mysql+aiomysql://user:pass@localhost:3306/mydb",
|
|
122
|
+
auto_create_table=True,
|
|
123
|
+
)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Tortoise ORM + PostgreSQL
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
config = AuditConfig(
|
|
130
|
+
orm="tortoise",
|
|
131
|
+
dsn="postgres://user:pass@localhost:5432/mydb",
|
|
132
|
+
auto_create_table=True,
|
|
133
|
+
)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### MongoDB via Beanie
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
config = AuditConfig(
|
|
140
|
+
orm="beanie",
|
|
141
|
+
dsn="mongodb://localhost:27017",
|
|
142
|
+
mongodb_database="myapp",
|
|
143
|
+
table_name="audit_logs", # becomes collection name
|
|
144
|
+
)
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Raw asyncpg (PostgreSQL, maximum performance)
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
config = AuditConfig(
|
|
151
|
+
orm="asyncpg",
|
|
152
|
+
dsn="postgresql://user:pass@localhost:5432/mydb",
|
|
153
|
+
batch_size=200,
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Using with Alembic (SQLAlchemy only)
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
# In alembic/env.py — include audit table in your migrations
|
|
161
|
+
from fastapi_audit_log.db.sqlalchemy_table import AuditBase
|
|
162
|
+
target_metadata = [YourBase.metadata, AuditBase.metadata]
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Enriching Logs from Routes
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from fastapi_audit_log import set_audit_action, set_audit_resource, set_audit_extra
|
|
169
|
+
|
|
170
|
+
@app.post("/items")
|
|
171
|
+
async def create_item(item_id: str):
|
|
172
|
+
set_audit_action("item.create")
|
|
173
|
+
set_audit_resource("item", item_id)
|
|
174
|
+
set_audit_extra("metadata", {"source": "admin_panel"})
|
|
175
|
+
return {"status": "ok"}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Retrieving Audit Logs
|
|
179
|
+
|
|
180
|
+
`fastapi-audit-log` provides a built-in helper to add a route for querying and filtering your audit logs.
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
from fastapi import FastAPI
|
|
184
|
+
from fastapi_audit_log import add_audit_log_routes
|
|
185
|
+
|
|
186
|
+
app = FastAPI(...)
|
|
187
|
+
|
|
188
|
+
# Register the GET /audit-logs route
|
|
189
|
+
add_audit_log_routes(
|
|
190
|
+
app,
|
|
191
|
+
path="/audit-logs", # default
|
|
192
|
+
tags=["Audit Logs"] # optional tags for OpenAPI
|
|
193
|
+
)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Filtering and Pagination
|
|
197
|
+
|
|
198
|
+
The added route supports several query parameters:
|
|
199
|
+
|
|
200
|
+
* **Pagination:** `limit` (default 100, max 1000) and `offset` (default 0).
|
|
201
|
+
* **Filters:** `method`, `path`, `status_code`, `user_id`, and `action`.
|
|
202
|
+
|
|
203
|
+
Example request:
|
|
204
|
+
`GET /audit-logs?method=POST&status_code=201&limit=20`
|
|
205
|
+
|
|
206
|
+
## Configuration Reference (AuditConfig)
|
|
207
|
+
|
|
208
|
+
| Parameter | Type | Default | Description |
|
|
209
|
+
|-----------|------|---------|-------------|
|
|
210
|
+
| `orm` | `str` | **Required** | One of: `sqlalchemy`, `tortoise`, `sqlmodel`, `beanie`, `asyncpg`. |
|
|
211
|
+
| `dsn` | `str` | **Required** | Connection string for the database. |
|
|
212
|
+
| `table_name` | `str` | `"audit_logs"` | Name of the table or collection. |
|
|
213
|
+
| `auto_create_table`| `bool` | `True` | Whether to create the table on startup. |
|
|
214
|
+
| `batch_size` | `int` | `1` | Set > 1 to enable batching (not all backends yet). |
|
|
215
|
+
| `mask_fields` | `list[str]` | `[]` | PII fields to mask in request bodies. |
|
|
216
|
+
| `on_storage_error`| `Callable` | `None` | Optional callback for storage errors. |
|
|
217
|
+
|
|
218
|
+
## Development and Examples
|
|
219
|
+
|
|
220
|
+
To run the examples that require a real database (PostgreSQL, MongoDB, MySQL), you can use the provided Docker Compose file:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
docker-compose up -d
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
This will start:
|
|
227
|
+
|
|
228
|
+
- **PostgreSQL** at `localhost:5432` (user: `user`, pass: `pass`, db: `audit_db`)
|
|
229
|
+
- **MongoDB** at `localhost:27017`
|
|
230
|
+
- **MySQL** at `localhost:3306` (user: `user`, pass: `pass`, db: `audit_db`)
|
|
231
|
+
|
|
232
|
+
### Running the MongoDB Example
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
poetry run python examples/beanie_mongodb_usage.py
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Running the Asyncpg Example
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
poetry run python examples/asyncpg_usage.py
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Future Features
|
|
245
|
+
|
|
246
|
+
- **Admin UI:** Build a simple web UI for viewing/searching audit logs.
|
|
247
|
+
- **Custom Storage Backends:** Allow users to plug in custom storage backends (e.g., S3, Redis, external APIs).
|
|
248
|
+
- **Event Hooks:** Add hooks for pre/post log processing (e.g., for enrichment, notifications).
|
|
249
|
+
- **Log Export:** Support exporting logs to CSV, JSON, or external log management systems.
|
|
250
|
+
- **Retention Policies:** Add configurable log retention and automatic cleanup.
|
|
251
|
+
- **Multi-Tenancy:** Support tenant-aware logging for SaaS apps.
|
|
252
|
+
- **Security:** Encrypt sensitive log fields at rest, and add role-based access for log viewing.
|
|
253
|
+
- **CLI Tooling:** Provide CLI commands for log inspection, export, and management.
|
|
254
|
+
- **OpenTelemetry Integration:** Integrate with OpenTelemetry for distributed tracing and correlation.
|
|
255
|
+
- **Documentation:** Expand usage examples and add troubleshooting guides.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT
|
|
260
|
+
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# auditlog-fastapi
|
|
2
|
+
|
|
3
|
+
Production-grade, reusable audit logging for FastAPI applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Multi-ORM Support:** SQLAlchemy 2 (async), Tortoise ORM, SQLModel, Beanie (MongoDB), and raw asyncpg.
|
|
8
|
+
- **Flexible Storage:** PostgreSQL, MySQL, MariaDB, SQLite, and MongoDB.
|
|
9
|
+
- **Explicit Configuration:** Clean API with `AuditConfig` and `configure()`.
|
|
10
|
+
- **Middleware Integration:** Capture request/response data automatically.
|
|
11
|
+
- **PII Masking:** Redact sensitive fields from logs.
|
|
12
|
+
- **Context Helpers:** Enrich audit logs from route handlers.
|
|
13
|
+
- **Lifespan Integration:** Automatic startup/shutdown of database connections.
|
|
14
|
+
- **Async & Non-blocking:** Built with performance and safety in mind.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Basic install
|
|
20
|
+
pip install auditlog-fastapi
|
|
21
|
+
|
|
22
|
+
# With SQLAlchemy support (Postgres, MySQL, SQLite)
|
|
23
|
+
pip install auditlog-fastapi[sqlalchemy]
|
|
24
|
+
|
|
25
|
+
# With Tortoise ORM support
|
|
26
|
+
pip install auditlog-fastapi[tortoise]
|
|
27
|
+
|
|
28
|
+
# With SQLModel support
|
|
29
|
+
pip install auditlog-fastapi[sqlmodel]
|
|
30
|
+
|
|
31
|
+
# With MongoDB (Beanie) support
|
|
32
|
+
pip install auditlog-fastapi[mongodb]
|
|
33
|
+
|
|
34
|
+
# With raw asyncpg (PostgreSQL only) support
|
|
35
|
+
pip install auditlog-fastapi[asyncpg]
|
|
36
|
+
|
|
37
|
+
# Everything
|
|
38
|
+
pip install auditlog-fastapi[all]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Quick Start (SQLAlchemy + SQLite)
|
|
42
|
+
|
|
43
|
+
```python
|
|
44
|
+
from fastapi import FastAPI
|
|
45
|
+
from fastapi_audit_log import AuditMiddleware, AuditConfig, create_audit_lifespan
|
|
46
|
+
|
|
47
|
+
# 1. Configure the audit log
|
|
48
|
+
config = AuditConfig(
|
|
49
|
+
orm="sqlalchemy",
|
|
50
|
+
dsn="sqlite+aiosqlite:///./audit.db",
|
|
51
|
+
table_name="audit_logs",
|
|
52
|
+
auto_create_table=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# 2. Create lifespan handler
|
|
56
|
+
app = FastAPI(lifespan=create_audit_lifespan(config))
|
|
57
|
+
|
|
58
|
+
# 3. Add middleware
|
|
59
|
+
app.add_middleware(
|
|
60
|
+
AuditMiddleware,
|
|
61
|
+
log_request_body=True
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@app.get("/")
|
|
65
|
+
async def root():
|
|
66
|
+
return {"message": "Hello World"}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## User Configuration Guide
|
|
70
|
+
|
|
71
|
+
### SQLAlchemy + PostgreSQL
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
config = AuditConfig(
|
|
75
|
+
orm="sqlalchemy",
|
|
76
|
+
dsn="postgresql+asyncpg://user:pass@localhost:5432/mydb",
|
|
77
|
+
table_name="audit_logs",
|
|
78
|
+
auto_create_table=True,
|
|
79
|
+
sqlalchemy_pool_size=10,
|
|
80
|
+
mask_fields=["password", "token"],
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### SQLAlchemy + MySQL
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
config = AuditConfig(
|
|
88
|
+
orm="sqlalchemy",
|
|
89
|
+
dsn="mysql+aiomysql://user:pass@localhost:3306/mydb",
|
|
90
|
+
auto_create_table=True,
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Tortoise ORM + PostgreSQL
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
config = AuditConfig(
|
|
98
|
+
orm="tortoise",
|
|
99
|
+
dsn="postgres://user:pass@localhost:5432/mydb",
|
|
100
|
+
auto_create_table=True,
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### MongoDB via Beanie
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
config = AuditConfig(
|
|
108
|
+
orm="beanie",
|
|
109
|
+
dsn="mongodb://localhost:27017",
|
|
110
|
+
mongodb_database="myapp",
|
|
111
|
+
table_name="audit_logs", # becomes collection name
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Raw asyncpg (PostgreSQL, maximum performance)
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
config = AuditConfig(
|
|
119
|
+
orm="asyncpg",
|
|
120
|
+
dsn="postgresql://user:pass@localhost:5432/mydb",
|
|
121
|
+
batch_size=200,
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Using with Alembic (SQLAlchemy only)
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
# In alembic/env.py — include audit table in your migrations
|
|
129
|
+
from fastapi_audit_log.db.sqlalchemy_table import AuditBase
|
|
130
|
+
target_metadata = [YourBase.metadata, AuditBase.metadata]
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Enriching Logs from Routes
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from fastapi_audit_log import set_audit_action, set_audit_resource, set_audit_extra
|
|
137
|
+
|
|
138
|
+
@app.post("/items")
|
|
139
|
+
async def create_item(item_id: str):
|
|
140
|
+
set_audit_action("item.create")
|
|
141
|
+
set_audit_resource("item", item_id)
|
|
142
|
+
set_audit_extra("metadata", {"source": "admin_panel"})
|
|
143
|
+
return {"status": "ok"}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Retrieving Audit Logs
|
|
147
|
+
|
|
148
|
+
`fastapi-audit-log` provides a built-in helper to add a route for querying and filtering your audit logs.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from fastapi import FastAPI
|
|
152
|
+
from fastapi_audit_log import add_audit_log_routes
|
|
153
|
+
|
|
154
|
+
app = FastAPI(...)
|
|
155
|
+
|
|
156
|
+
# Register the GET /audit-logs route
|
|
157
|
+
add_audit_log_routes(
|
|
158
|
+
app,
|
|
159
|
+
path="/audit-logs", # default
|
|
160
|
+
tags=["Audit Logs"] # optional tags for OpenAPI
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Filtering and Pagination
|
|
165
|
+
|
|
166
|
+
The added route supports several query parameters:
|
|
167
|
+
|
|
168
|
+
* **Pagination:** `limit` (default 100, max 1000) and `offset` (default 0).
|
|
169
|
+
* **Filters:** `method`, `path`, `status_code`, `user_id`, and `action`.
|
|
170
|
+
|
|
171
|
+
Example request:
|
|
172
|
+
`GET /audit-logs?method=POST&status_code=201&limit=20`
|
|
173
|
+
|
|
174
|
+
## Configuration Reference (AuditConfig)
|
|
175
|
+
|
|
176
|
+
| Parameter | Type | Default | Description |
|
|
177
|
+
|-----------|------|---------|-------------|
|
|
178
|
+
| `orm` | `str` | **Required** | One of: `sqlalchemy`, `tortoise`, `sqlmodel`, `beanie`, `asyncpg`. |
|
|
179
|
+
| `dsn` | `str` | **Required** | Connection string for the database. |
|
|
180
|
+
| `table_name` | `str` | `"audit_logs"` | Name of the table or collection. |
|
|
181
|
+
| `auto_create_table`| `bool` | `True` | Whether to create the table on startup. |
|
|
182
|
+
| `batch_size` | `int` | `1` | Set > 1 to enable batching (not all backends yet). |
|
|
183
|
+
| `mask_fields` | `list[str]` | `[]` | PII fields to mask in request bodies. |
|
|
184
|
+
| `on_storage_error`| `Callable` | `None` | Optional callback for storage errors. |
|
|
185
|
+
|
|
186
|
+
## Development and Examples
|
|
187
|
+
|
|
188
|
+
To run the examples that require a real database (PostgreSQL, MongoDB, MySQL), you can use the provided Docker Compose file:
|
|
189
|
+
|
|
190
|
+
```bash
|
|
191
|
+
docker-compose up -d
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
This will start:
|
|
195
|
+
|
|
196
|
+
- **PostgreSQL** at `localhost:5432` (user: `user`, pass: `pass`, db: `audit_db`)
|
|
197
|
+
- **MongoDB** at `localhost:27017`
|
|
198
|
+
- **MySQL** at `localhost:3306` (user: `user`, pass: `pass`, db: `audit_db`)
|
|
199
|
+
|
|
200
|
+
### Running the MongoDB Example
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
poetry run python examples/beanie_mongodb_usage.py
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### Running the Asyncpg Example
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
poetry run python examples/asyncpg_usage.py
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Future Features
|
|
213
|
+
|
|
214
|
+
- **Admin UI:** Build a simple web UI for viewing/searching audit logs.
|
|
215
|
+
- **Custom Storage Backends:** Allow users to plug in custom storage backends (e.g., S3, Redis, external APIs).
|
|
216
|
+
- **Event Hooks:** Add hooks for pre/post log processing (e.g., for enrichment, notifications).
|
|
217
|
+
- **Log Export:** Support exporting logs to CSV, JSON, or external log management systems.
|
|
218
|
+
- **Retention Policies:** Add configurable log retention and automatic cleanup.
|
|
219
|
+
- **Multi-Tenancy:** Support tenant-aware logging for SaaS apps.
|
|
220
|
+
- **Security:** Encrypt sensitive log fields at rest, and add role-based access for log viewing.
|
|
221
|
+
- **CLI Tooling:** Provide CLI commands for log inspection, export, and management.
|
|
222
|
+
- **OpenTelemetry Integration:** Integrate with OpenTelemetry for distributed tracing and correlation.
|
|
223
|
+
- **Documentation:** Expand usage examples and add troubleshooting guides.
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from contextlib import asynccontextmanager
|
|
2
|
+
|
|
3
|
+
from .config import AuditConfig, configure, get_storage
|
|
4
|
+
from .context import set_audit_action, set_audit_extra, set_audit_resource
|
|
5
|
+
from .middleware import AuditMiddleware
|
|
6
|
+
from .routes import add_audit_log_routes
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def create_audit_lifespan(config: AuditConfig):
|
|
10
|
+
storage = configure(config)
|
|
11
|
+
|
|
12
|
+
@asynccontextmanager
|
|
13
|
+
async def lifespan(app):
|
|
14
|
+
# Store config in app for testing/access if needed
|
|
15
|
+
if not hasattr(app, "extra"):
|
|
16
|
+
app.extra = {}
|
|
17
|
+
app.extra["audit_config"] = config
|
|
18
|
+
|
|
19
|
+
await storage.startup()
|
|
20
|
+
yield
|
|
21
|
+
await storage.shutdown()
|
|
22
|
+
|
|
23
|
+
return lifespan
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"AuditConfig",
|
|
28
|
+
"configure",
|
|
29
|
+
"get_storage",
|
|
30
|
+
"AuditMiddleware",
|
|
31
|
+
"create_audit_lifespan",
|
|
32
|
+
"set_audit_action",
|
|
33
|
+
"set_audit_resource",
|
|
34
|
+
"set_audit_extra",
|
|
35
|
+
"add_audit_log_routes",
|
|
36
|
+
]
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import typing
|
|
2
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
5
|
+
|
|
6
|
+
from .exceptions import AuditAlreadyConfiguredError, AuditNotConfiguredError
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .storage.base import AuditStorage
|
|
10
|
+
|
|
11
|
+
ORMBackend = Literal["sqlalchemy", "tortoise", "sqlmodel", "beanie", "asyncpg"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AuditConfig(BaseModel):
|
|
15
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
16
|
+
|
|
17
|
+
# Required
|
|
18
|
+
orm: ORMBackend
|
|
19
|
+
dsn: str # full connection string e.g. "postgresql+asyncpg://user:pass@host/db"
|
|
20
|
+
|
|
21
|
+
# Table / collection config
|
|
22
|
+
table_name: str = "audit_logs"
|
|
23
|
+
auto_create_table: bool = True # CREATE TABLE IF NOT EXISTS on startup
|
|
24
|
+
|
|
25
|
+
# SQLAlchemy-specific
|
|
26
|
+
sqlalchemy_pool_size: int = 10
|
|
27
|
+
sqlalchemy_max_overflow: int = 20
|
|
28
|
+
sqlalchemy_pool_timeout: int = 30
|
|
29
|
+
sqlalchemy_echo: bool = False
|
|
30
|
+
expose_metadata: bool = False # if True, exposes Base.metadata for Alembic
|
|
31
|
+
|
|
32
|
+
# Tortoise-specific
|
|
33
|
+
tortoise_modules: dict[str, list[str]] | None = None
|
|
34
|
+
|
|
35
|
+
# MongoDB / Beanie-specific
|
|
36
|
+
mongodb_database: str = "audit"
|
|
37
|
+
|
|
38
|
+
# Batching (for all backends)
|
|
39
|
+
batch_size: int = 1 # set > 1 to enable batch inserts
|
|
40
|
+
batch_flush_interval: float = 5.0 # seconds, used if batch_size > 1
|
|
41
|
+
|
|
42
|
+
# PII masking
|
|
43
|
+
mask_fields: list[str] = Field(default_factory=list)
|
|
44
|
+
|
|
45
|
+
# Error handling
|
|
46
|
+
on_storage_error: Any = None # callable(exc: Exception, entry: AuditEntry) -> None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# Global registry holding the configured storage instance
|
|
50
|
+
_registry: dict[str, Any] = {}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def configure(config: AuditConfig) -> "AuditStorage":
|
|
54
|
+
"""
|
|
55
|
+
Initialize and register the audit storage backend.
|
|
56
|
+
Must be called once at app startup before serving requests.
|
|
57
|
+
Returns the configured storage instance.
|
|
58
|
+
Raises AuditConfigurationError if config is invalid or backend cannot connect.
|
|
59
|
+
"""
|
|
60
|
+
from .registry import resolve_storage
|
|
61
|
+
|
|
62
|
+
if "storage" in _registry:
|
|
63
|
+
existing_config = _registry["config"]
|
|
64
|
+
if existing_config == config:
|
|
65
|
+
return typing.cast("AuditStorage", _registry["storage"])
|
|
66
|
+
raise AuditAlreadyConfiguredError(
|
|
67
|
+
"Audit storage is already configured with a different configuration."
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
storage = resolve_storage(config)
|
|
71
|
+
_registry["storage"] = storage
|
|
72
|
+
_registry["config"] = config
|
|
73
|
+
return typing.cast("AuditStorage", storage)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_storage() -> "AuditStorage":
|
|
77
|
+
"""
|
|
78
|
+
Retrieve the globally registered storage instance.
|
|
79
|
+
Raises AuditNotConfiguredError if configure() has not been called.
|
|
80
|
+
"""
|
|
81
|
+
if "storage" not in _registry:
|
|
82
|
+
raise AuditNotConfiguredError(
|
|
83
|
+
"Audit storage has not been configured. Call configure() first."
|
|
84
|
+
)
|
|
85
|
+
return typing.cast("AuditStorage", _registry["storage"])
|
|
86
|
+
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from .models import AuditEntry
|
|
5
|
+
|
|
6
|
+
_current_entry: ContextVar[AuditEntry | None] = ContextVar("audit_entry", default=None)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_current_audit_entry() -> AuditEntry | None:
|
|
10
|
+
"""Returns the current audit entry from the context."""
|
|
11
|
+
return _current_entry.get()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def set_audit_action(action: str) -> None:
|
|
15
|
+
"""Sets the action field on the current audit entry."""
|
|
16
|
+
entry = get_current_audit_entry()
|
|
17
|
+
if entry:
|
|
18
|
+
entry.action = action
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def set_audit_resource(resource_type: str, resource_id: str) -> None:
|
|
22
|
+
"""Sets resource_type and resource_id fields on the current audit entry."""
|
|
23
|
+
entry = get_current_audit_entry()
|
|
24
|
+
if entry:
|
|
25
|
+
entry.resource_type = resource_type
|
|
26
|
+
entry.resource_id = resource_id
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_audit_extra(key: str, value: Any) -> None:
|
|
30
|
+
"""Adds a key-value pair to the extra field of the current audit entry."""
|
|
31
|
+
entry = get_current_audit_entry()
|
|
32
|
+
if entry:
|
|
33
|
+
entry.extra[key] = value
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
|
|
4
|
+
from beanie import Document
|
|
5
|
+
from pydantic import Field
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AuditLogDocument(Document):
|
|
9
|
+
"""Beanie Document model for audit logs, with dynamic collection name set at runtime.""" # noqa: E501
|
|
10
|
+
|
|
11
|
+
id: uuid.UUID = Field(default_factory=uuid.uuid4)
|
|
12
|
+
timestamp: datetime
|
|
13
|
+
user_id: str | None = None
|
|
14
|
+
username: str | None = None
|
|
15
|
+
ip_address: str | None = None
|
|
16
|
+
user_agent: str | None = None
|
|
17
|
+
method: str
|
|
18
|
+
path: str
|
|
19
|
+
query_params: dict | None = None
|
|
20
|
+
status_code: int | None = None
|
|
21
|
+
request_body: dict | None = None
|
|
22
|
+
response_body: dict | None = None
|
|
23
|
+
duration_ms: float | None = None
|
|
24
|
+
action: str | None = None
|
|
25
|
+
resource_type: str | None = None
|
|
26
|
+
resource_id: str | None = None
|
|
27
|
+
extra: dict | None = None
|
|
28
|
+
error: str | None = None
|
|
29
|
+
|
|
30
|
+
class Settings:
|
|
31
|
+
"""Settings for Beanie Document, with collection name set dynamically at runtime.""" # noqa: E501
|
|
32
|
+
|
|
33
|
+
name = "audit_logs" # overridden by config at runtime
|
|
34
|
+
indexes = [
|
|
35
|
+
"timestamp",
|
|
36
|
+
"user_id",
|
|
37
|
+
"path",
|
|
38
|
+
"status_code",
|
|
39
|
+
"action",
|
|
40
|
+
"resource_type",
|
|
41
|
+
"resource_id",
|
|
42
|
+
]
|