auditry 0.2.0__tar.gz → 0.2.2__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.
- {auditry-0.2.0/src/auditry.egg-info → auditry-0.2.2}/PKG-INFO +34 -4
- {auditry-0.2.0 → auditry-0.2.2}/README.md +33 -3
- {auditry-0.2.0 → auditry-0.2.2}/pyproject.toml +1 -3
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/__init__.py +1 -1
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/core/logger.py +8 -4
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/fastapi/adapters.py +9 -2
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/models.py +9 -1
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/quart/adapters.py +30 -3
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/quart/middleware.py +35 -0
- {auditry-0.2.0 → auditry-0.2.2/src/auditry.egg-info}/PKG-INFO +34 -4
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry.egg-info/SOURCES.txt +1 -0
- auditry-0.2.2/tests/test_body_logging.py +166 -0
- {auditry-0.2.0 → auditry-0.2.2}/LICENSE +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/MANIFEST.in +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/setup.cfg +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/core/__init__.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/core/base.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/correlation.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/fastapi/__init__.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/fastapi/middleware.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/logging_config.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/py.typed +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/quart/__init__.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry/redaction.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry.egg-info/dependency_links.txt +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry.egg-info/requires.txt +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/src/auditry.egg-info/top_level.txt +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/tests/test_correlation.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/tests/test_fastapi_middleware.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/tests/test_quart_middleware.py +0 -0
- {auditry-0.2.0 → auditry-0.2.2}/tests/test_redaction.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: auditry
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Observability middleware for FastAPI and Quart with request/response logging, correlation IDs, and sensitive data redaction
|
|
5
5
|
Author-email: Liv Stark <livstark.work@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -133,11 +133,13 @@ config = ObservabilityConfig(
|
|
|
133
133
|
)
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
**Note:** The `service_name` is required and must be provided. This ensures all services have meaningful names in logs rather than generic defaults.
|
|
137
|
+
|
|
136
138
|
### Full Configuration Options
|
|
137
139
|
|
|
138
140
|
```python
|
|
139
141
|
config = ObservabilityConfig(
|
|
140
|
-
# REQUIRED: Service name for log filtering
|
|
142
|
+
# REQUIRED: Service name for log filtering (no default)
|
|
141
143
|
service_name="my-service-name",
|
|
142
144
|
|
|
143
145
|
# Correlation ID header name (default: X-Correlation-ID)
|
|
@@ -158,6 +160,14 @@ config = ObservabilityConfig(
|
|
|
158
160
|
|
|
159
161
|
# Whether to log query parameters (default: True)
|
|
160
162
|
log_query_params=True,
|
|
163
|
+
|
|
164
|
+
# Whether to log request bodies for all endpoints (default: True)
|
|
165
|
+
# Set to False for applications handling sensitive data
|
|
166
|
+
log_request_body=True,
|
|
167
|
+
|
|
168
|
+
# Whether to log response bodies for all endpoints (default: True)
|
|
169
|
+
# Set to False for applications returning sensitive data
|
|
170
|
+
log_response_body=True,
|
|
161
171
|
)
|
|
162
172
|
|
|
163
173
|
# For FastAPI:
|
|
@@ -355,9 +365,11 @@ All logs are structured JSON, ready for log aggregators:
|
|
|
355
365
|
}
|
|
356
366
|
```
|
|
357
367
|
|
|
358
|
-
## Sensitive Data
|
|
368
|
+
## Sensitive Data Handling
|
|
369
|
+
|
|
370
|
+
### Automatic Redaction
|
|
359
371
|
|
|
360
|
-
Automatically redacts these sensitive field patterns:
|
|
372
|
+
Automatically redacts these sensitive field patterns in all logged requests/responses:
|
|
361
373
|
|
|
362
374
|
- `password`
|
|
363
375
|
- `token`
|
|
@@ -376,6 +388,24 @@ config = ObservabilityConfig(
|
|
|
376
388
|
additional_redaction_patterns=["internal_token", "employee_id"],
|
|
377
389
|
)
|
|
378
390
|
```
|
|
391
|
+
|
|
392
|
+
### Disabling Body Logging (Application-Wide)
|
|
393
|
+
|
|
394
|
+
For applications handling sensitive data, you can disable logging of request and/or response bodies across the entire application:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
config = ObservabilityConfig(
|
|
398
|
+
service_name="my-service-name",
|
|
399
|
+
|
|
400
|
+
# Disable request body logging for all endpoints
|
|
401
|
+
log_request_body=False,
|
|
402
|
+
|
|
403
|
+
# Disable response body logging for all endpoints
|
|
404
|
+
log_response_body=False,
|
|
405
|
+
)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
When body logging is disabled, logs will show `[BODY_LOGGING_DISABLED]` instead of the actual content, while still logging metadata like headers, status codes, and timing information.
|
|
379
409
|
## Best Practices
|
|
380
410
|
|
|
381
411
|
### 1. Configure Logging Early
|
|
@@ -90,11 +90,13 @@ config = ObservabilityConfig(
|
|
|
90
90
|
)
|
|
91
91
|
```
|
|
92
92
|
|
|
93
|
+
**Note:** The `service_name` is required and must be provided. This ensures all services have meaningful names in logs rather than generic defaults.
|
|
94
|
+
|
|
93
95
|
### Full Configuration Options
|
|
94
96
|
|
|
95
97
|
```python
|
|
96
98
|
config = ObservabilityConfig(
|
|
97
|
-
# REQUIRED: Service name for log filtering
|
|
99
|
+
# REQUIRED: Service name for log filtering (no default)
|
|
98
100
|
service_name="my-service-name",
|
|
99
101
|
|
|
100
102
|
# Correlation ID header name (default: X-Correlation-ID)
|
|
@@ -115,6 +117,14 @@ config = ObservabilityConfig(
|
|
|
115
117
|
|
|
116
118
|
# Whether to log query parameters (default: True)
|
|
117
119
|
log_query_params=True,
|
|
120
|
+
|
|
121
|
+
# Whether to log request bodies for all endpoints (default: True)
|
|
122
|
+
# Set to False for applications handling sensitive data
|
|
123
|
+
log_request_body=True,
|
|
124
|
+
|
|
125
|
+
# Whether to log response bodies for all endpoints (default: True)
|
|
126
|
+
# Set to False for applications returning sensitive data
|
|
127
|
+
log_response_body=True,
|
|
118
128
|
)
|
|
119
129
|
|
|
120
130
|
# For FastAPI:
|
|
@@ -312,9 +322,11 @@ All logs are structured JSON, ready for log aggregators:
|
|
|
312
322
|
}
|
|
313
323
|
```
|
|
314
324
|
|
|
315
|
-
## Sensitive Data
|
|
325
|
+
## Sensitive Data Handling
|
|
326
|
+
|
|
327
|
+
### Automatic Redaction
|
|
316
328
|
|
|
317
|
-
Automatically redacts these sensitive field patterns:
|
|
329
|
+
Automatically redacts these sensitive field patterns in all logged requests/responses:
|
|
318
330
|
|
|
319
331
|
- `password`
|
|
320
332
|
- `token`
|
|
@@ -333,6 +345,24 @@ config = ObservabilityConfig(
|
|
|
333
345
|
additional_redaction_patterns=["internal_token", "employee_id"],
|
|
334
346
|
)
|
|
335
347
|
```
|
|
348
|
+
|
|
349
|
+
### Disabling Body Logging (Application-Wide)
|
|
350
|
+
|
|
351
|
+
For applications handling sensitive data, you can disable logging of request and/or response bodies across the entire application:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
config = ObservabilityConfig(
|
|
355
|
+
service_name="my-service-name",
|
|
356
|
+
|
|
357
|
+
# Disable request body logging for all endpoints
|
|
358
|
+
log_request_body=False,
|
|
359
|
+
|
|
360
|
+
# Disable response body logging for all endpoints
|
|
361
|
+
log_response_body=False,
|
|
362
|
+
)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
When body logging is disabled, logs will show `[BODY_LOGGING_DISABLED]` instead of the actual content, while still logging metadata like headers, status codes, and timing information.
|
|
336
366
|
## Best Practices
|
|
337
367
|
|
|
338
368
|
### 1. Configure Logging Early
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "auditry"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.2"
|
|
4
4
|
description = "Observability middleware for FastAPI and Quart with request/response logging, correlation IDs, and sensitive data redaction"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Liv Stark", email = "livstark.work@gmail.com"}
|
|
@@ -23,14 +23,12 @@ classifiers = [
|
|
|
23
23
|
"Topic :: System :: Logging",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
|
-
# Core dependencies needed for all functionality
|
|
27
26
|
dependencies = [
|
|
28
27
|
"pydantic>=2.0.0",
|
|
29
28
|
"structlog>=23.0.0",
|
|
30
29
|
"asgi-correlation-id>=4.0.0",
|
|
31
30
|
]
|
|
32
31
|
|
|
33
|
-
# Optional dependencies for specific frameworks
|
|
34
32
|
[project.optional-dependencies]
|
|
35
33
|
fastapi = [
|
|
36
34
|
"fastapi>=0.100.0",
|
|
@@ -81,9 +81,11 @@ class RequestResponseLogger:
|
|
|
81
81
|
if raw_data.get("path_params"):
|
|
82
82
|
prepared["path_params"] = raw_data["path_params"]
|
|
83
83
|
|
|
84
|
-
# Parse and redact body
|
|
85
|
-
if raw_data.get("body"):
|
|
84
|
+
# Parse and redact body (only if configured to log request body)
|
|
85
|
+
if self.config.log_request_body and raw_data.get("body"):
|
|
86
86
|
prepared["body"] = self._parse_and_redact_body(raw_data["body"])
|
|
87
|
+
elif raw_data.get("body") and not self.config.log_request_body:
|
|
88
|
+
prepared["body"] = "[BODY_LOGGING_DISABLED]"
|
|
87
89
|
|
|
88
90
|
return prepared
|
|
89
91
|
|
|
@@ -110,9 +112,11 @@ class RequestResponseLogger:
|
|
|
110
112
|
if self.config.log_response_headers and raw_data.get("headers"):
|
|
111
113
|
prepared["headers"] = redact_headers(raw_data["headers"])
|
|
112
114
|
|
|
113
|
-
# Parse and redact body
|
|
114
|
-
if raw_data.get("body"):
|
|
115
|
+
# Parse and redact body (only if configured to log response body)
|
|
116
|
+
if self.config.log_response_body and raw_data.get("body"):
|
|
115
117
|
prepared["body"] = self._parse_and_redact_body(raw_data["body"])
|
|
118
|
+
elif raw_data.get("body") and not self.config.log_response_body:
|
|
119
|
+
prepared["body"] = "[BODY_LOGGING_DISABLED]"
|
|
116
120
|
|
|
117
121
|
return prepared
|
|
118
122
|
|
|
@@ -5,7 +5,8 @@ These adapters implement the abstract base classes to extract data from
|
|
|
5
5
|
FastAPI/Starlette request and response objects.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import inspect
|
|
9
|
+
from typing import Dict, Optional
|
|
9
10
|
|
|
10
11
|
from fastapi import Request, Response
|
|
11
12
|
from starlette.responses import StreamingResponse
|
|
@@ -104,8 +105,14 @@ class FastAPIResponseAdapter(BaseResponseAdapter):
|
|
|
104
105
|
if isinstance(response, StreamingResponse):
|
|
105
106
|
return None
|
|
106
107
|
|
|
108
|
+
# Check if response has a body_iterator (generator/async generator)
|
|
109
|
+
if hasattr(response, "body_iterator"):
|
|
110
|
+
# Check if it's a generator or async generator
|
|
111
|
+
if inspect.isasyncgen(response.body_iterator) or inspect.isgenerator(response.body_iterator):
|
|
112
|
+
return None
|
|
113
|
+
|
|
107
114
|
# Get body if available
|
|
108
115
|
if hasattr(response, "body"):
|
|
109
116
|
return response.body
|
|
110
117
|
|
|
111
|
-
return None
|
|
118
|
+
return None
|
|
@@ -68,4 +68,12 @@ class ObservabilityConfig(BaseModel):
|
|
|
68
68
|
log_response_headers: bool = Field(
|
|
69
69
|
default=False, description="Whether to log response headers (redacted)"
|
|
70
70
|
)
|
|
71
|
-
log_query_params: bool = Field(default=True, description="Whether to log query parameters")
|
|
71
|
+
log_query_params: bool = Field(default=True, description="Whether to log query parameters")
|
|
72
|
+
log_request_body: bool = Field(
|
|
73
|
+
default=True,
|
|
74
|
+
description="Whether to log request bodies for the application"
|
|
75
|
+
)
|
|
76
|
+
log_response_body: bool = Field(
|
|
77
|
+
default=True,
|
|
78
|
+
description="Whether to log response bodies for the application"
|
|
79
|
+
)
|
|
@@ -5,7 +5,7 @@ These adapters implement the abstract base classes to extract data from
|
|
|
5
5
|
Quart request and response objects.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
import inspect
|
|
9
9
|
import json
|
|
10
10
|
|
|
11
11
|
from quart import Request, Response, g
|
|
@@ -133,17 +133,44 @@ class QuartResponseAdapter(BaseResponseAdapter):
|
|
|
133
133
|
"""
|
|
134
134
|
Extract body from Quart response.
|
|
135
135
|
|
|
136
|
+
Returns None for streaming responses to avoid consuming the stream.
|
|
136
137
|
Handles both Response objects and tuple returns.
|
|
137
138
|
"""
|
|
138
139
|
try:
|
|
139
140
|
# For Response objects
|
|
141
|
+
if hasattr(response, "response"):
|
|
142
|
+
# Check if the response is an async generator (streaming)
|
|
143
|
+
if inspect.isasyncgen(response.response):
|
|
144
|
+
# This is a streaming response, return None to avoid consuming it
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
# Check if it's a regular generator (also streaming)
|
|
148
|
+
if inspect.isgenerator(response.response):
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
# For Response objects with get_data method (non-streaming)
|
|
140
152
|
if hasattr(response, "get_data"):
|
|
141
|
-
|
|
153
|
+
# Only call get_data if we're sure it's not streaming
|
|
154
|
+
# Double-check for streaming response attribute
|
|
155
|
+
if hasattr(response, "response"):
|
|
156
|
+
resp_attr = response.response
|
|
157
|
+
if inspect.isasyncgen(resp_attr) or inspect.isgenerator(resp_attr):
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
# Safe to get data for non-streaming responses
|
|
161
|
+
data = await response.get_data()
|
|
162
|
+
if isinstance(data, str):
|
|
163
|
+
return data.encode("utf-8")
|
|
164
|
+
return data
|
|
142
165
|
|
|
143
166
|
# For tuple responses (body, status_code, headers)
|
|
144
167
|
if isinstance(response, tuple) and len(response) >= 1:
|
|
145
168
|
body = response[0]
|
|
146
169
|
|
|
170
|
+
# Check if body is a generator (streaming)
|
|
171
|
+
if inspect.isasyncgen(body) or inspect.isgenerator(body):
|
|
172
|
+
return None
|
|
173
|
+
|
|
147
174
|
# Convert various body types to bytes
|
|
148
175
|
if isinstance(body, bytes):
|
|
149
176
|
return body
|
|
@@ -154,4 +181,4 @@ class QuartResponseAdapter(BaseResponseAdapter):
|
|
|
154
181
|
|
|
155
182
|
return None
|
|
156
183
|
except Exception:
|
|
157
|
-
return None
|
|
184
|
+
return None
|
|
@@ -89,6 +89,41 @@ class QuartMiddleware(BaseMiddleware):
|
|
|
89
89
|
if not hasattr(request, "observability_start_time"):
|
|
90
90
|
return response
|
|
91
91
|
|
|
92
|
+
# Check if this is a streaming response - if so, handle it separately
|
|
93
|
+
if hasattr(response, "response"):
|
|
94
|
+
if inspect.isasyncgen(response.response) or inspect.isgenerator(response.response):
|
|
95
|
+
# For streaming responses, log without consuming the stream
|
|
96
|
+
duration_ms = (time.time() - request.observability_start_time) * 1000
|
|
97
|
+
|
|
98
|
+
# Get stored correlation ID from before_request
|
|
99
|
+
correlation_id = getattr(request, "observability_correlation_id", None)
|
|
100
|
+
request_data = getattr(request, "observability_request_data", {})
|
|
101
|
+
|
|
102
|
+
# Re-extract user_id (might have been set during request processing)
|
|
103
|
+
user_id = await self.request_adapter.extract_user_id(request)
|
|
104
|
+
|
|
105
|
+
# Log with minimal response data (no body for streaming)
|
|
106
|
+
self.logger.log_success(
|
|
107
|
+
request_data=request_data,
|
|
108
|
+
response_data={
|
|
109
|
+
"status_code": response.status_code if hasattr(response, "status_code") else 200,
|
|
110
|
+
"headers": dict(response.headers) if hasattr(response, "headers") else {},
|
|
111
|
+
"body": None # Don't try to extract body for streaming
|
|
112
|
+
},
|
|
113
|
+
duration_ms=duration_ms,
|
|
114
|
+
correlation_id=correlation_id,
|
|
115
|
+
user_id=user_id,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Add correlation ID to response headers (same as non-streaming)
|
|
119
|
+
if correlation_id:
|
|
120
|
+
response.headers[self.config.correlation_id_header] = correlation_id
|
|
121
|
+
|
|
122
|
+
# Clear cache and return immediately
|
|
123
|
+
self.request_adapter.clear_cache(request)
|
|
124
|
+
return response
|
|
125
|
+
|
|
126
|
+
# Non-streaming response handling
|
|
92
127
|
# Calculate duration
|
|
93
128
|
duration_ms = (time.time() - request.observability_start_time) * 1000
|
|
94
129
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: auditry
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Observability middleware for FastAPI and Quart with request/response logging, correlation IDs, and sensitive data redaction
|
|
5
5
|
Author-email: Liv Stark <livstark.work@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -133,11 +133,13 @@ config = ObservabilityConfig(
|
|
|
133
133
|
)
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
**Note:** The `service_name` is required and must be provided. This ensures all services have meaningful names in logs rather than generic defaults.
|
|
137
|
+
|
|
136
138
|
### Full Configuration Options
|
|
137
139
|
|
|
138
140
|
```python
|
|
139
141
|
config = ObservabilityConfig(
|
|
140
|
-
# REQUIRED: Service name for log filtering
|
|
142
|
+
# REQUIRED: Service name for log filtering (no default)
|
|
141
143
|
service_name="my-service-name",
|
|
142
144
|
|
|
143
145
|
# Correlation ID header name (default: X-Correlation-ID)
|
|
@@ -158,6 +160,14 @@ config = ObservabilityConfig(
|
|
|
158
160
|
|
|
159
161
|
# Whether to log query parameters (default: True)
|
|
160
162
|
log_query_params=True,
|
|
163
|
+
|
|
164
|
+
# Whether to log request bodies for all endpoints (default: True)
|
|
165
|
+
# Set to False for applications handling sensitive data
|
|
166
|
+
log_request_body=True,
|
|
167
|
+
|
|
168
|
+
# Whether to log response bodies for all endpoints (default: True)
|
|
169
|
+
# Set to False for applications returning sensitive data
|
|
170
|
+
log_response_body=True,
|
|
161
171
|
)
|
|
162
172
|
|
|
163
173
|
# For FastAPI:
|
|
@@ -355,9 +365,11 @@ All logs are structured JSON, ready for log aggregators:
|
|
|
355
365
|
}
|
|
356
366
|
```
|
|
357
367
|
|
|
358
|
-
## Sensitive Data
|
|
368
|
+
## Sensitive Data Handling
|
|
369
|
+
|
|
370
|
+
### Automatic Redaction
|
|
359
371
|
|
|
360
|
-
Automatically redacts these sensitive field patterns:
|
|
372
|
+
Automatically redacts these sensitive field patterns in all logged requests/responses:
|
|
361
373
|
|
|
362
374
|
- `password`
|
|
363
375
|
- `token`
|
|
@@ -376,6 +388,24 @@ config = ObservabilityConfig(
|
|
|
376
388
|
additional_redaction_patterns=["internal_token", "employee_id"],
|
|
377
389
|
)
|
|
378
390
|
```
|
|
391
|
+
|
|
392
|
+
### Disabling Body Logging (Application-Wide)
|
|
393
|
+
|
|
394
|
+
For applications handling sensitive data, you can disable logging of request and/or response bodies across the entire application:
|
|
395
|
+
|
|
396
|
+
```python
|
|
397
|
+
config = ObservabilityConfig(
|
|
398
|
+
service_name="my-service-name",
|
|
399
|
+
|
|
400
|
+
# Disable request body logging for all endpoints
|
|
401
|
+
log_request_body=False,
|
|
402
|
+
|
|
403
|
+
# Disable response body logging for all endpoints
|
|
404
|
+
log_response_body=False,
|
|
405
|
+
)
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
When body logging is disabled, logs will show `[BODY_LOGGING_DISABLED]` instead of the actual content, while still logging metadata like headers, status codes, and timing information.
|
|
379
409
|
## Best Practices
|
|
380
410
|
|
|
381
411
|
### 1. Configure Logging Early
|
|
@@ -22,6 +22,7 @@ src/auditry/fastapi/middleware.py
|
|
|
22
22
|
src/auditry/quart/__init__.py
|
|
23
23
|
src/auditry/quart/adapters.py
|
|
24
24
|
src/auditry/quart/middleware.py
|
|
25
|
+
tests/test_body_logging.py
|
|
25
26
|
tests/test_correlation.py
|
|
26
27
|
tests/test_fastapi_middleware.py
|
|
27
28
|
tests/test_quart_middleware.py
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Tests for request/response body logging configuration."""
|
|
2
|
+
import pytest
|
|
3
|
+
from fastapi import FastAPI, Request
|
|
4
|
+
from fastapi.testclient import TestClient
|
|
5
|
+
from quart import Quart
|
|
6
|
+
|
|
7
|
+
from auditry import ObservabilityConfig, configure_logging
|
|
8
|
+
from auditry.fastapi import create_middleware
|
|
9
|
+
from auditry.quart import create_middleware as create_quart_middleware
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
configure_logging(level="INFO")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_fastapi_disable_request_body_logging():
|
|
16
|
+
"""Test that request body logging can be disabled in FastAPI."""
|
|
17
|
+
app = FastAPI()
|
|
18
|
+
app = create_middleware(
|
|
19
|
+
app,
|
|
20
|
+
config=ObservabilityConfig(
|
|
21
|
+
service_name="test-service",
|
|
22
|
+
log_request_body=False, # Disable request body logging
|
|
23
|
+
log_response_body=True,
|
|
24
|
+
)
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@app.post("/users")
|
|
28
|
+
def create_user():
|
|
29
|
+
return {"id": "123", "status": "created"}
|
|
30
|
+
|
|
31
|
+
client = TestClient(app)
|
|
32
|
+
response = client.post(
|
|
33
|
+
"/users",
|
|
34
|
+
json={"name": "Test User", "password": "secret123", "ssn": "123-45-6789"}
|
|
35
|
+
)
|
|
36
|
+
assert response.status_code == 200
|
|
37
|
+
# Body should not be logged, but response should be
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def test_fastapi_disable_response_body_logging():
|
|
41
|
+
"""Test that response body logging can be disabled in FastAPI."""
|
|
42
|
+
app = FastAPI()
|
|
43
|
+
app = create_middleware(
|
|
44
|
+
app,
|
|
45
|
+
config=ObservabilityConfig(
|
|
46
|
+
service_name="test-service",
|
|
47
|
+
log_request_body=True,
|
|
48
|
+
log_response_body=False, # Disable response body logging
|
|
49
|
+
)
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@app.post("/users")
|
|
53
|
+
def create_user():
|
|
54
|
+
return {
|
|
55
|
+
"id": "123",
|
|
56
|
+
"name": "Test User",
|
|
57
|
+
"ssn": "123-45-6789", # Sensitive data in response
|
|
58
|
+
"api_key": "secret-key"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
client = TestClient(app)
|
|
62
|
+
response = client.post(
|
|
63
|
+
"/users",
|
|
64
|
+
json={"name": "Test User"}
|
|
65
|
+
)
|
|
66
|
+
assert response.status_code == 200
|
|
67
|
+
# Request body should be logged, but response body should not be
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_fastapi_disable_both_body_logging():
|
|
71
|
+
"""Test that both request and response body logging can be disabled."""
|
|
72
|
+
app = FastAPI()
|
|
73
|
+
app = create_middleware(
|
|
74
|
+
app,
|
|
75
|
+
config=ObservabilityConfig(
|
|
76
|
+
service_name="test-service",
|
|
77
|
+
log_request_body=False,
|
|
78
|
+
log_response_body=False,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@app.post("/sensitive")
|
|
83
|
+
def sensitive_endpoint():
|
|
84
|
+
return {"secret": "data"}
|
|
85
|
+
|
|
86
|
+
client = TestClient(app)
|
|
87
|
+
response = client.post(
|
|
88
|
+
"/sensitive",
|
|
89
|
+
json={"password": "secret", "credit_card": "1234-5678-9012-3456"}
|
|
90
|
+
)
|
|
91
|
+
assert response.status_code == 200
|
|
92
|
+
# Neither request nor response bodies should be logged
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.mark.asyncio
|
|
96
|
+
async def test_quart_disable_request_body_logging():
|
|
97
|
+
"""Test that request body logging can be disabled in Quart."""
|
|
98
|
+
app = Quart(__name__)
|
|
99
|
+
app = create_quart_middleware(
|
|
100
|
+
app,
|
|
101
|
+
config=ObservabilityConfig(
|
|
102
|
+
service_name="test-quart-service",
|
|
103
|
+
log_request_body=False, # Disable request body logging
|
|
104
|
+
log_response_body=True,
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@app.route("/users", methods=["POST"])
|
|
109
|
+
async def create_user():
|
|
110
|
+
return {"id": "123", "status": "created"}
|
|
111
|
+
|
|
112
|
+
client = app.test_client()
|
|
113
|
+
response = await client.post(
|
|
114
|
+
"/users",
|
|
115
|
+
json={"name": "Test User", "password": "secret123", "ssn": "123-45-6789"}
|
|
116
|
+
)
|
|
117
|
+
assert response.status_code == 200
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.asyncio
|
|
121
|
+
async def test_quart_disable_response_body_logging():
|
|
122
|
+
"""Test that response body logging can be disabled in Quart."""
|
|
123
|
+
app = Quart(__name__)
|
|
124
|
+
app = create_quart_middleware(
|
|
125
|
+
app,
|
|
126
|
+
config=ObservabilityConfig(
|
|
127
|
+
service_name="test-quart-service",
|
|
128
|
+
log_request_body=True,
|
|
129
|
+
log_response_body=False, # Disable response body logging
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
@app.route("/users", methods=["POST"])
|
|
134
|
+
async def create_user():
|
|
135
|
+
return {
|
|
136
|
+
"id": "123",
|
|
137
|
+
"name": "Test User",
|
|
138
|
+
"ssn": "123-45-6789", # Sensitive data in response
|
|
139
|
+
"api_key": "secret-key"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
client = app.test_client()
|
|
143
|
+
response = await client.post(
|
|
144
|
+
"/users",
|
|
145
|
+
json={"name": "Test User"}
|
|
146
|
+
)
|
|
147
|
+
assert response.status_code == 200
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_service_name_required():
|
|
151
|
+
"""Test that service_name is required and cannot be empty."""
|
|
152
|
+
# Should raise validation error without service_name
|
|
153
|
+
with pytest.raises(Exception): # Pydantic ValidationError
|
|
154
|
+
ObservabilityConfig()
|
|
155
|
+
|
|
156
|
+
# Should raise validation error with empty service_name
|
|
157
|
+
with pytest.raises(Exception): # Pydantic ValidationError
|
|
158
|
+
ObservabilityConfig(service_name="")
|
|
159
|
+
|
|
160
|
+
# Should raise validation error with whitespace-only service_name
|
|
161
|
+
with pytest.raises(Exception): # Pydantic ValidationError
|
|
162
|
+
ObservabilityConfig(service_name=" ")
|
|
163
|
+
|
|
164
|
+
# Should work with valid service_name
|
|
165
|
+
config = ObservabilityConfig(service_name="valid-service")
|
|
166
|
+
assert config.service_name == "valid-service"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|