json-logify 0.1.0__py3-none-any.whl → 0.1.3__py3-none-any.whl
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.
- {json_logify-0.1.0.dist-info → json_logify-0.1.3.dist-info}/METADATA +154 -43
- json_logify-0.1.3.dist-info/RECORD +11 -0
- {json_logify-0.1.0.dist-info → json_logify-0.1.3.dist-info}/WHEEL +1 -1
- logify/core.py +212 -10
- logify/django.py +307 -14
- logify/fastapi.py +82 -82
- logify/flask.py +88 -88
- logify/version.py +1 -1
- json_logify-0.1.0.dist-info/RECORD +0 -11
- {json_logify-0.1.0.dist-info → json_logify-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {json_logify-0.1.0.dist-info → json_logify-0.1.3.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: json-logify
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
4
4
|
Summary: Universal structured logging with exact JSON schema for Python frameworks
|
|
5
5
|
Author-email: Bakdoolot Kulbarakov <kulbarakovbh@gmail.com>
|
|
6
6
|
Maintainer-email: Bakdoolot Kulbarakov <kulbarakovbh@gmail.com>
|
|
@@ -54,11 +54,6 @@ Requires-Dist: structlog>=23.0.0
|
|
|
54
54
|
Requires-Dist: orjson>=3.8.0
|
|
55
55
|
Provides-Extra: django
|
|
56
56
|
Requires-Dist: django>=5.2.6; extra == "django"
|
|
57
|
-
Provides-Extra: fastapi
|
|
58
|
-
Requires-Dist: fastapi>=0.116.1; extra == "fastapi"
|
|
59
|
-
Requires-Dist: uvicorn>=0.35.0; extra == "fastapi"
|
|
60
|
-
Provides-Extra: flask
|
|
61
|
-
Requires-Dist: flask>=3.1.2; extra == "flask"
|
|
62
57
|
Provides-Extra: dev
|
|
63
58
|
Requires-Dist: pytest>=8.4.2; extra == "dev"
|
|
64
59
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -83,13 +78,14 @@ Universal structured logging with exact JSON schema for Python frameworks.
|
|
|
83
78
|
|
|
84
79
|
## Features
|
|
85
80
|
|
|
86
|
-
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
**
|
|
81
|
+
- **Exact JSON Schema**: Consistent log format across all frameworks
|
|
82
|
+
- **High Performance**: Built with structlog and orjson for maximum speed
|
|
83
|
+
- **Universal**: Works with Django, FastAPI, Flask and standalone Python
|
|
84
|
+
- **Security First**: Automatic masking of sensitive data (passwords, tokens, etc.)
|
|
85
|
+
- **Easy Setup**: One-line configuration for most use cases
|
|
86
|
+
- **Rich Context**: Request IDs, user tracking, and custom payload support
|
|
87
|
+
- **Smart Filtering**: Configurable path ignoring and request/response body logging
|
|
88
|
+
- **Modern Python**: Full type hints and async support
|
|
93
89
|
|
|
94
90
|
## Quick Start
|
|
95
91
|
|
|
@@ -101,8 +97,8 @@ pip install json-logify
|
|
|
101
97
|
|
|
102
98
|
# For specific frameworks
|
|
103
99
|
pip install json-logify[django]
|
|
104
|
-
pip install json-logify[fastapi]
|
|
105
|
-
pip install json-logify[flask]
|
|
100
|
+
# pip install json-logify[fastapi] # Coming soon
|
|
101
|
+
# pip install json-logify[flask] # Coming soon
|
|
106
102
|
|
|
107
103
|
# Everything
|
|
108
104
|
pip install json-logify[all]
|
|
@@ -111,50 +107,164 @@ pip install json-logify[all]
|
|
|
111
107
|
### Basic Usage
|
|
112
108
|
|
|
113
109
|
```python
|
|
114
|
-
from logify import info, error
|
|
110
|
+
from logify import info, error, debug, warning
|
|
115
111
|
|
|
116
|
-
#
|
|
117
|
-
info("User logged in"
|
|
112
|
+
# Basic logging with message
|
|
113
|
+
info("User logged in")
|
|
118
114
|
|
|
119
|
-
#
|
|
115
|
+
# With structured context
|
|
116
|
+
info("Payment processed", amount=100.0, currency="USD", user_id="user123")
|
|
117
|
+
|
|
118
|
+
# Different log levels
|
|
119
|
+
debug("Debug information", query_time=0.023)
|
|
120
|
+
warning("Slow database query detected", query_time=1.52, query_id="a1b2c3")
|
|
121
|
+
error("Payment failed", error_code="CARD_DECLINED", user_id="user123")
|
|
122
|
+
|
|
123
|
+
# Exception handling
|
|
120
124
|
try:
|
|
121
|
-
|
|
125
|
+
# Some code that might fail
|
|
126
|
+
result = some_function()
|
|
122
127
|
except Exception as e:
|
|
123
|
-
error("Operation failed",
|
|
128
|
+
error("Operation failed", exception=str(e), operation="some_function")
|
|
124
129
|
```
|
|
125
130
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"payload": {
|
|
133
|
-
"user_id": "12345",
|
|
134
|
-
"action": "login"
|
|
135
|
-
}
|
|
136
|
-
}
|
|
131
|
+
### Django Integration
|
|
132
|
+
|
|
133
|
+
#### 1. Install with Django extras:
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
pip install json-logify[django]
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
|
|
139
|
+
#### 2. Configure in settings.py:
|
|
140
140
|
|
|
141
141
|
```python
|
|
142
|
-
# settings.py
|
|
143
142
|
from logify.django import get_logging_config
|
|
144
143
|
|
|
145
|
-
|
|
146
|
-
service_name="myapp",
|
|
147
|
-
json_logs=True
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
# Add middleware (optional for request tracking)
|
|
144
|
+
# Add middleware to MIDDLEWARE list
|
|
151
145
|
MIDDLEWARE = [
|
|
152
|
-
'logify.django.LogifyMiddleware',
|
|
153
146
|
# ... other middleware
|
|
147
|
+
'logify.django.LogifyMiddleware', # ← Add this
|
|
154
148
|
]
|
|
149
|
+
|
|
150
|
+
# Configure logging with json-logify
|
|
151
|
+
LOGGING = get_logging_config(
|
|
152
|
+
service_name="my-django-app",
|
|
153
|
+
level="INFO",
|
|
154
|
+
max_string_length=200, # String truncation limit
|
|
155
|
+
sensitive_fields=[ # Fields to mask with "***"
|
|
156
|
+
"password", "passwd", "secret", "token", "api_key",
|
|
157
|
+
"access_token", "refresh_token", "session_key",
|
|
158
|
+
"credit_card", "cvv", "ssn", "authorization",
|
|
159
|
+
"cookie", "x-api-key", "custom_sensitive_field"
|
|
160
|
+
],
|
|
161
|
+
ignore_paths=[ # Paths to skip logging
|
|
162
|
+
"/health/", "/static/", "/favicon.ico",
|
|
163
|
+
"/admin/jsi18n/", "/metrics/"
|
|
164
|
+
]
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
# Optional: Reduce Django built-in logger noise
|
|
168
|
+
LOGGING['loggers'].update({
|
|
169
|
+
'django.utils.autoreload': {'level': 'WARNING'},
|
|
170
|
+
'django.db.backends': {'level': 'WARNING'},
|
|
171
|
+
'django.server': {'level': 'WARNING'},
|
|
172
|
+
'django.request': {'level': 'WARNING'},
|
|
173
|
+
})
|
|
155
174
|
```
|
|
156
175
|
|
|
157
|
-
|
|
176
|
+
#### 3. Use in your views:
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from logify import info, error, debug, warning
|
|
180
|
+
from django.http import JsonResponse
|
|
181
|
+
|
|
182
|
+
def process_payment(request):
|
|
183
|
+
# Log with automatic request context
|
|
184
|
+
info("Payment processing started",
|
|
185
|
+
user_id=request.user.id,
|
|
186
|
+
amount=request.POST.get('amount'))
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
# Sensitive data gets automatically masked
|
|
190
|
+
info("User data received",
|
|
191
|
+
username=request.user.username, # ← Visible
|
|
192
|
+
password=request.POST.get('password'), # ← Masked: "***"
|
|
193
|
+
credit_card=request.POST.get('card'), # ← Masked: "***"
|
|
194
|
+
email=request.user.email) # ← Visible
|
|
195
|
+
|
|
196
|
+
# Your business logic
|
|
197
|
+
payment = process_payment_logic(request.POST)
|
|
198
|
+
|
|
199
|
+
# Log success
|
|
200
|
+
info("Payment completed",
|
|
201
|
+
payment_id=payment.id,
|
|
202
|
+
status="success",
|
|
203
|
+
amount=payment.amount)
|
|
204
|
+
|
|
205
|
+
return JsonResponse({"status": "success", "payment_id": payment.id})
|
|
206
|
+
|
|
207
|
+
except ValidationError as e:
|
|
208
|
+
error("Payment validation failed", error=e, user_id=request.user.id)
|
|
209
|
+
return JsonResponse({"status": "error", "message": str(e)}, status=400)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
error("Payment processing failed", error=e)
|
|
212
|
+
return JsonResponse({"status": "error"}, status=500)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
#### 4. What you get automatically:
|
|
216
|
+
|
|
217
|
+
**Request logging:**
|
|
218
|
+
```json
|
|
219
|
+
{
|
|
220
|
+
"timestamp": "2025-09-09T08:09:35.933Z",
|
|
221
|
+
"message": "Request started",
|
|
222
|
+
"level": "INFO",
|
|
223
|
+
"payload": {
|
|
224
|
+
"request_id": "b62e59b6-bae7-4a96-821d",
|
|
225
|
+
"service": "my-django-app",
|
|
226
|
+
"method": "POST",
|
|
227
|
+
"path": "/api/payment/",
|
|
228
|
+
"user_info": "User ID: 123: john_doe",
|
|
229
|
+
"headers": {
|
|
230
|
+
"Content-Type": "application/json",
|
|
231
|
+
"Authorization": "***", // ← Automatically masked
|
|
232
|
+
"User-Agent": "curl/8.7.1"
|
|
233
|
+
},
|
|
234
|
+
"request_body": {
|
|
235
|
+
"username": "john_doe",
|
|
236
|
+
"password": "***", // ← Automatically masked
|
|
237
|
+
"credit_card": "***" // ← Automatically masked
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**Your application logs:**
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"timestamp": "2025-09-09T08:09:35.934Z",
|
|
247
|
+
"message": "Payment completed",
|
|
248
|
+
"level": "INFO",
|
|
249
|
+
"payload": {
|
|
250
|
+
"request_id": "b62e59b6-bae7-4a96-821d", // ← Auto-linked to request
|
|
251
|
+
"payment_id": "pay_123456",
|
|
252
|
+
"status": "success",
|
|
253
|
+
"amount": 99.99
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**🔒 Security Features:**
|
|
259
|
+
- **Automatic masking**: Passwords, tokens, API keys, credit cards → `"***"`
|
|
260
|
+
- **Header filtering**: Authorization, Cookie, X-API-Key → `"***"`
|
|
261
|
+
- **Recursive masking**: Works in nested objects and arrays
|
|
262
|
+
- **Request/Response body**: Limited size + content-type filtering
|
|
263
|
+
- **Path ignoring**: Skip health checks, static files, etc.
|
|
264
|
+
- Request and response bodies (with sensitive fields masked)
|
|
265
|
+
|
|
266
|
+
<!--
|
|
267
|
+
### FastAPI Integration (Coming Soon)
|
|
158
268
|
|
|
159
269
|
```python
|
|
160
270
|
from fastapi import FastAPI
|
|
@@ -170,7 +280,7 @@ async def root():
|
|
|
170
280
|
return {"message": "Hello World"}
|
|
171
281
|
```
|
|
172
282
|
|
|
173
|
-
### Flask Integration
|
|
283
|
+
### Flask Integration (Coming Soon)
|
|
174
284
|
|
|
175
285
|
```python
|
|
176
286
|
from flask import Flask
|
|
@@ -185,6 +295,7 @@ def hello():
|
|
|
185
295
|
info("Flask endpoint called", endpoint="/")
|
|
186
296
|
return "Hello, World!"
|
|
187
297
|
```
|
|
298
|
+
-->
|
|
188
299
|
|
|
189
300
|
## Advanced Usage
|
|
190
301
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
json_logify-0.1.3.dist-info/licenses/LICENSE,sha256=qRyWd6Y0_db_j_ECsAAEJW8-XMnhYUg_yK3c5UkOfNI,1077
|
|
2
|
+
logify/__init__.py,sha256=t2Jjw7T4jRMIYvXJOTBraaW6MneifmS_gDKDlrRHmeU,1053
|
|
3
|
+
logify/core.py,sha256=6IoiwnIOlghiCPE89lyAG_FK4II9bhyvZRlK75UJ_F8,12603
|
|
4
|
+
logify/django.py,sha256=qy20qkJhhUPz0Nl3yQMPryqVj5H0RXE2DxPk7La6iws,12842
|
|
5
|
+
logify/fastapi.py,sha256=G570BPDdgEv2e0gASI2SZqNjyUNeIyemPcCXH6f-UPg,2710
|
|
6
|
+
logify/flask.py,sha256=TCWBSni8uEWBjvNJTFM87VPUwDRI3PTdAm3HSbP5R2g,2802
|
|
7
|
+
logify/version.py,sha256=YvuYzWnKtqBb-IqG8HAu-nhIYAsgj9Vmc_b9o7vO-js,22
|
|
8
|
+
json_logify-0.1.3.dist-info/METADATA,sha256=ZAxBK34k_XY-G64m6uSFn5isgO-VwUa3RKWi2BpFkOs,11563
|
|
9
|
+
json_logify-0.1.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
10
|
+
json_logify-0.1.3.dist-info/top_level.txt,sha256=MRERkeaav8J-KtwGpiLDCnSNQ5e2JqKUMAlJnYVntao,7
|
|
11
|
+
json_logify-0.1.3.dist-info/RECORD,,
|
logify/core.py
CHANGED
|
@@ -15,12 +15,187 @@ import structlog
|
|
|
15
15
|
# Context variables for request tracking
|
|
16
16
|
_request_context: ContextVar[Dict[str, Any]] = ContextVar("request_context", default={})
|
|
17
17
|
|
|
18
|
+
# Internal configuration storage
|
|
19
|
+
_config = {
|
|
20
|
+
"MAX_STRING_LENGTH": 100,
|
|
21
|
+
"SENSITIVE_FIELDS": {
|
|
22
|
+
"password",
|
|
23
|
+
"passwd",
|
|
24
|
+
"pass",
|
|
25
|
+
"pwd",
|
|
26
|
+
"secret",
|
|
27
|
+
"token",
|
|
28
|
+
"key",
|
|
29
|
+
"api_key",
|
|
30
|
+
"access_token",
|
|
31
|
+
"refresh_token",
|
|
32
|
+
"auth_token",
|
|
33
|
+
"session_key",
|
|
34
|
+
"private_key",
|
|
35
|
+
"credit_card",
|
|
36
|
+
"card_number",
|
|
37
|
+
"cvv",
|
|
38
|
+
"ssn",
|
|
39
|
+
"social_security_number",
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def configure_core(sensitive_fields=None, replace_sensitive_defaults=False, max_string_length=None):
|
|
45
|
+
"""
|
|
46
|
+
Configure core logging settings.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
sensitive_fields: List or set of field names to mask
|
|
50
|
+
replace_sensitive_defaults: If True, replace default sensitive fields with provided ones.
|
|
51
|
+
If False, merge provided fields with defaults.
|
|
52
|
+
max_string_length: Maximum length for string truncation
|
|
53
|
+
"""
|
|
54
|
+
if sensitive_fields is not None:
|
|
55
|
+
new_fields = set()
|
|
56
|
+
if isinstance(sensitive_fields, (list, tuple)):
|
|
57
|
+
new_fields = set(field.lower() for field in sensitive_fields)
|
|
58
|
+
elif isinstance(sensitive_fields, set):
|
|
59
|
+
new_fields = set(field.lower() for field in sensitive_fields)
|
|
60
|
+
|
|
61
|
+
if replace_sensitive_defaults:
|
|
62
|
+
_config["SENSITIVE_FIELDS"] = new_fields
|
|
63
|
+
else:
|
|
64
|
+
_config["SENSITIVE_FIELDS"].update(new_fields)
|
|
65
|
+
|
|
66
|
+
if max_string_length is not None:
|
|
67
|
+
_config["MAX_STRING_LENGTH"] = max_string_length
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def get_config_value(key: str, default=None):
|
|
71
|
+
"""Get a value from core configuration."""
|
|
72
|
+
return _config.get(key, default)
|
|
73
|
+
|
|
18
74
|
|
|
19
75
|
def orjson_serializer(_, __, event_dict):
|
|
20
76
|
"""Serialize log entries using orjson for performance."""
|
|
21
77
|
return orjson.dumps(event_dict).decode("utf-8")
|
|
22
78
|
|
|
23
79
|
|
|
80
|
+
def truncate_long_strings(_, __, event_dict):
|
|
81
|
+
"""Truncate long strings in log entries based on max_string_length setting."""
|
|
82
|
+
max_length = _config["MAX_STRING_LENGTH"]
|
|
83
|
+
|
|
84
|
+
# Truncate long strings - only if max_length is positive
|
|
85
|
+
if max_length > 0:
|
|
86
|
+
for key, value in event_dict.items():
|
|
87
|
+
if isinstance(value, str) and len(value) > max_length:
|
|
88
|
+
event_dict[key] = value[:max_length] + "..."
|
|
89
|
+
|
|
90
|
+
return event_dict
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def clean_non_serializable_objects(_, __, event_dict):
|
|
94
|
+
"""Clean non-serializable objects from log entries."""
|
|
95
|
+
max_length = _config["MAX_STRING_LENGTH"]
|
|
96
|
+
|
|
97
|
+
cleaned = {}
|
|
98
|
+
for key, value in event_dict.items():
|
|
99
|
+
# Skip 'error' field - it will be handled specially in format_log_entry
|
|
100
|
+
if key == "error":
|
|
101
|
+
cleaned[key] = value
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
# Test if value is JSON serializable
|
|
106
|
+
orjson.dumps(value)
|
|
107
|
+
cleaned[key] = value
|
|
108
|
+
except TypeError:
|
|
109
|
+
# Convert non-serializable objects to their string representation
|
|
110
|
+
if hasattr(value, "__class__"):
|
|
111
|
+
cleaned[key] = f"<{value.__class__.__name__}: {str(value)[:max_length]}>"
|
|
112
|
+
else:
|
|
113
|
+
cleaned[key] = str(value)[:max_length]
|
|
114
|
+
return cleaned
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def mask_sensitive_fields(_, __, event_dict):
|
|
118
|
+
"""Mask sensitive fields in log entries recursively."""
|
|
119
|
+
sensitive_fields = _config["SENSITIVE_FIELDS"]
|
|
120
|
+
|
|
121
|
+
def _mask_value_if_sensitive(key, value):
|
|
122
|
+
"""Check if key is sensitive and return masked value if so."""
|
|
123
|
+
key_lower = key.lower()
|
|
124
|
+
if any(s in key_lower for s in sensitive_fields):
|
|
125
|
+
if value and str(value).strip():
|
|
126
|
+
return "***"
|
|
127
|
+
return value
|
|
128
|
+
|
|
129
|
+
def _scrub_string(text):
|
|
130
|
+
"""Scrub sensitive data from strings (e.g. url encoded bodies, query strings)."""
|
|
131
|
+
if not text:
|
|
132
|
+
return text
|
|
133
|
+
|
|
134
|
+
# Handle URLs with query parameters
|
|
135
|
+
if "?" in text:
|
|
136
|
+
base, query = text.split("?", 1)
|
|
137
|
+
# Recursively scrub the query part
|
|
138
|
+
scrubbed_query = _scrub_string(query)
|
|
139
|
+
return f"{base}?{scrubbed_query}"
|
|
140
|
+
|
|
141
|
+
# pattern matching for key=value pairs where key is sensitive
|
|
142
|
+
# We do a simple approach: split by & and =
|
|
143
|
+
if "=" in text:
|
|
144
|
+
# Check if this looks like a query string or form data
|
|
145
|
+
# This is a heuristic
|
|
146
|
+
parts = text.split("&")
|
|
147
|
+
new_parts = []
|
|
148
|
+
modified = False
|
|
149
|
+
|
|
150
|
+
for part in parts:
|
|
151
|
+
if "=" in part:
|
|
152
|
+
k, v = part.split("=", 1)
|
|
153
|
+
# Check if key is sensitive
|
|
154
|
+
k_lower = k.lower()
|
|
155
|
+
if any(s in k_lower for s in sensitive_fields):
|
|
156
|
+
new_parts.append(f"{k}=***")
|
|
157
|
+
modified = True
|
|
158
|
+
else:
|
|
159
|
+
new_parts.append(part)
|
|
160
|
+
else:
|
|
161
|
+
new_parts.append(part)
|
|
162
|
+
|
|
163
|
+
if modified:
|
|
164
|
+
return "&".join(new_parts)
|
|
165
|
+
|
|
166
|
+
return text
|
|
167
|
+
|
|
168
|
+
def _mask_recursive(obj):
|
|
169
|
+
"""Recursively mask sensitive fields in dicts and lists."""
|
|
170
|
+
if isinstance(obj, dict):
|
|
171
|
+
masked = {}
|
|
172
|
+
for k, v in obj.items():
|
|
173
|
+
# First check if the key itself is sensitive
|
|
174
|
+
v_masked = _mask_value_if_sensitive(k, v)
|
|
175
|
+
if v_masked == "***":
|
|
176
|
+
masked[k] = "***"
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
# If not masked by key, recurse or string scrub
|
|
180
|
+
if isinstance(v, (dict, list)):
|
|
181
|
+
masked[k] = _mask_recursive(v)
|
|
182
|
+
elif isinstance(v, str):
|
|
183
|
+
masked[k] = _scrub_string(v)
|
|
184
|
+
else:
|
|
185
|
+
masked[k] = v
|
|
186
|
+
return masked
|
|
187
|
+
elif isinstance(obj, list):
|
|
188
|
+
return [_mask_recursive(item) for item in obj]
|
|
189
|
+
elif isinstance(obj, str):
|
|
190
|
+
return _scrub_string(obj)
|
|
191
|
+
return obj
|
|
192
|
+
|
|
193
|
+
# Apply masking to the entire event_dict (including top-level keys)
|
|
194
|
+
event_dict = _mask_recursive(event_dict)
|
|
195
|
+
|
|
196
|
+
return event_dict
|
|
197
|
+
|
|
198
|
+
|
|
24
199
|
def add_timestamp(_, __, event_dict):
|
|
25
200
|
"""Add ISO timestamp to log entries."""
|
|
26
201
|
# Unresolved attribute reference 'UTC' for class 'datetime'
|
|
@@ -39,11 +214,12 @@ def format_log_entry(logger, name, event_dict):
|
|
|
39
214
|
# Remove level from event_dict to avoid duplication
|
|
40
215
|
event_dict.pop("level", None)
|
|
41
216
|
|
|
42
|
-
# Get context
|
|
217
|
+
# Get only request_id from context to avoid duplication
|
|
43
218
|
try:
|
|
44
219
|
context = _request_context.get({})
|
|
220
|
+
request_id_only = {"request_id": context.get("request_id")} if context.get("request_id") else {}
|
|
45
221
|
except LookupError:
|
|
46
|
-
|
|
222
|
+
request_id_only = {}
|
|
47
223
|
|
|
48
224
|
# Extract error from event_dict if present
|
|
49
225
|
error_field = event_dict.pop("error", None)
|
|
@@ -53,12 +229,19 @@ def format_log_entry(logger, name, event_dict):
|
|
|
53
229
|
"timestamp": event_dict.pop("timestamp", datetime.now(UTC).isoformat().replace("+00:00", "Z")),
|
|
54
230
|
"message": message,
|
|
55
231
|
"level": level.upper(),
|
|
56
|
-
"payload": {**
|
|
232
|
+
"payload": {**request_id_only, **event_dict},
|
|
57
233
|
}
|
|
58
234
|
|
|
59
235
|
# Add optional error field at top level
|
|
60
236
|
if error_field:
|
|
61
|
-
|
|
237
|
+
# Format as "ErrorType: message"
|
|
238
|
+
if isinstance(error_field, Exception):
|
|
239
|
+
if error_field.args:
|
|
240
|
+
formatted_entry["error"] = f"{error_field.__class__.__name__}: {error_field.args[0]}"
|
|
241
|
+
else:
|
|
242
|
+
formatted_entry["error"] = f"{error_field.__class__.__name__}: No message"
|
|
243
|
+
else:
|
|
244
|
+
formatted_entry["error"] = str(error_field)
|
|
62
245
|
|
|
63
246
|
return formatted_entry
|
|
64
247
|
|
|
@@ -67,6 +250,9 @@ def format_log_entry(logger, name, event_dict):
|
|
|
67
250
|
structlog.configure(
|
|
68
251
|
processors=[
|
|
69
252
|
structlog.stdlib.add_log_level,
|
|
253
|
+
clean_non_serializable_objects, # Clean non-serializable objects FIRST
|
|
254
|
+
truncate_long_strings, # Truncate long strings SECOND
|
|
255
|
+
mask_sensitive_fields, # Mask sensitive fields THIRD
|
|
70
256
|
add_timestamp,
|
|
71
257
|
format_log_entry,
|
|
72
258
|
orjson_serializer,
|
|
@@ -106,7 +292,7 @@ def error(message: str, error: Exception = None, **kwargs):
|
|
|
106
292
|
"""Log error message."""
|
|
107
293
|
if error:
|
|
108
294
|
kwargs["error"] = error # Will be moved to top-level by format_log_entry
|
|
109
|
-
|
|
295
|
+
# error_type removed - now included in error field as "Type: message"
|
|
110
296
|
logger.error(message, **kwargs)
|
|
111
297
|
|
|
112
298
|
|
|
@@ -123,21 +309,37 @@ def exception(message: str, **kwargs):
|
|
|
123
309
|
logger.error(message, **kwargs)
|
|
124
310
|
|
|
125
311
|
|
|
126
|
-
def get_logger(name: str = "json-logify"
|
|
312
|
+
def get_logger(name: str = "json-logify"):
|
|
127
313
|
"""Get a named logger instance."""
|
|
128
314
|
bound_logger = structlog.get_logger(name)
|
|
129
|
-
return bound_logger
|
|
315
|
+
return bound_logger
|
|
130
316
|
|
|
131
317
|
|
|
132
|
-
def configure_logging(
|
|
318
|
+
def configure_logging(
|
|
319
|
+
service_name: str = "app",
|
|
320
|
+
level: str = "INFO",
|
|
321
|
+
sensitive_fields=None,
|
|
322
|
+
replace_sensitive_defaults=False,
|
|
323
|
+
max_string_length=None,
|
|
324
|
+
):
|
|
133
325
|
"""Configure logging for the application."""
|
|
134
326
|
import logging
|
|
135
327
|
|
|
328
|
+
# Configure core settings
|
|
329
|
+
configure_core(
|
|
330
|
+
sensitive_fields=sensitive_fields,
|
|
331
|
+
replace_sensitive_defaults=replace_sensitive_defaults,
|
|
332
|
+
max_string_length=max_string_length,
|
|
333
|
+
)
|
|
334
|
+
|
|
136
335
|
# Set logging level on stdlib logger
|
|
137
336
|
logging.basicConfig(level=getattr(logging, level.upper(), logging.INFO))
|
|
138
337
|
|
|
139
338
|
structlog.configure(
|
|
140
339
|
processors=[
|
|
340
|
+
clean_non_serializable_objects, # Clean non-serializable objects FIRST
|
|
341
|
+
truncate_long_strings, # Truncate long strings SECOND
|
|
342
|
+
mask_sensitive_fields, # Mask sensitive fields THIRD
|
|
141
343
|
add_timestamp,
|
|
142
344
|
format_log_entry,
|
|
143
345
|
orjson_serializer,
|
|
@@ -147,9 +349,9 @@ def configure_logging(service_name: str = "app", level: str = "INFO"):
|
|
|
147
349
|
cache_logger_on_first_use=True,
|
|
148
350
|
)
|
|
149
351
|
|
|
150
|
-
#
|
|
352
|
+
# Set default logger without service binding
|
|
151
353
|
global logger
|
|
152
|
-
logger = structlog.get_logger("json-logify")
|
|
354
|
+
logger = structlog.get_logger("json-logify")
|
|
153
355
|
|
|
154
356
|
|
|
155
357
|
def bind(**kwargs):
|
logify/django.py
CHANGED
|
@@ -2,44 +2,134 @@
|
|
|
2
2
|
Django integration for json-logify structured logging.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import logging
|
|
6
6
|
|
|
7
|
+
import structlog
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
from .core import clear_request_context, configure_core
|
|
10
|
+
from .core import configure_logging as configure_core_logging
|
|
11
|
+
from .core import generate_request_id, get_config_value, info, set_request_context
|
|
12
|
+
|
|
13
|
+
# Django-specific settings storage
|
|
14
|
+
_django_settings = {
|
|
15
|
+
"SERVICE_NAME": "django",
|
|
16
|
+
"IGNORE_PATHS": ["/health/", "/healthz/", "/api/schema/", "/static/", "/favicon.ico", "/robots.txt"],
|
|
17
|
+
"EXCLUDED_FIELDS": {
|
|
18
|
+
"name",
|
|
19
|
+
"msg",
|
|
20
|
+
"args",
|
|
21
|
+
"levelname",
|
|
22
|
+
"levelno",
|
|
23
|
+
"pathname",
|
|
24
|
+
"filename",
|
|
25
|
+
"module",
|
|
26
|
+
"lineno",
|
|
27
|
+
"funcName",
|
|
28
|
+
"created",
|
|
29
|
+
"msecs",
|
|
30
|
+
"relativeCreated",
|
|
31
|
+
"thread",
|
|
32
|
+
"threadName",
|
|
33
|
+
"processName",
|
|
34
|
+
"process",
|
|
35
|
+
"message",
|
|
36
|
+
"exc_info",
|
|
37
|
+
"exc_text",
|
|
38
|
+
"stack_info",
|
|
39
|
+
"getMessage",
|
|
40
|
+
"request",
|
|
41
|
+
"response",
|
|
42
|
+
"server_time",
|
|
43
|
+
"status_code",
|
|
44
|
+
},
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_logging_config(
|
|
49
|
+
service_name: str = "django",
|
|
50
|
+
level: str = "INFO",
|
|
51
|
+
json_logs: bool = True,
|
|
52
|
+
excluded_fields: list = None,
|
|
53
|
+
sensitive_fields: list = None,
|
|
54
|
+
replace_sensitive_defaults: bool = False,
|
|
55
|
+
max_string_length: int = None,
|
|
56
|
+
ignore_paths: list = None,
|
|
57
|
+
):
|
|
9
58
|
"""
|
|
10
59
|
Get Django logging configuration for json-logify.
|
|
11
60
|
|
|
61
|
+
Args:
|
|
62
|
+
service_name: Name of the service for logging
|
|
63
|
+
level: Logging level (DEBUG, INFO, WARNING, ERROR)
|
|
64
|
+
json_logs: Whether to enable JSON logging
|
|
65
|
+
excluded_fields: List of fields to exclude from logs
|
|
66
|
+
sensitive_fields: List of fields to mask with *** (also used for sensitive headers)
|
|
67
|
+
replace_sensitive_defaults: If True, replace default sensitive fields. If False, merge.
|
|
68
|
+
max_string_length: Maximum length for string truncation
|
|
69
|
+
ignore_paths: List of URL paths to ignore from logging
|
|
70
|
+
|
|
12
71
|
Usage in settings.py:
|
|
13
72
|
from logify.django import get_logging_config
|
|
14
|
-
LOGGING = get_logging_config(
|
|
73
|
+
LOGGING = get_logging_config(
|
|
74
|
+
service_name="myapp",
|
|
75
|
+
level="INFO",
|
|
76
|
+
excluded_fields=["custom_field"],
|
|
77
|
+
sensitive_fields=["password", "secret", "authorization"],
|
|
78
|
+
replace_sensitive_defaults=False, # Optional, defaults to False (merge)
|
|
79
|
+
max_string_length=200,
|
|
80
|
+
ignore_paths=["/health/", "/static/"]
|
|
81
|
+
)
|
|
15
82
|
"""
|
|
83
|
+
# Configure core settings (this sets global state in core module)
|
|
84
|
+
configure_core(
|
|
85
|
+
sensitive_fields=sensitive_fields,
|
|
86
|
+
replace_sensitive_defaults=replace_sensitive_defaults,
|
|
87
|
+
max_string_length=max_string_length,
|
|
88
|
+
)
|
|
89
|
+
|
|
16
90
|
if json_logs:
|
|
17
|
-
|
|
91
|
+
# This sets up the structlog pipeline
|
|
92
|
+
configure_core_logging(service_name=service_name, level=level)
|
|
93
|
+
|
|
94
|
+
# Store Django-specific settings
|
|
95
|
+
_django_settings["SERVICE_NAME"] = service_name
|
|
96
|
+
|
|
97
|
+
if excluded_fields is not None:
|
|
98
|
+
default_excluded = _django_settings["EXCLUDED_FIELDS"]
|
|
99
|
+
_django_settings["EXCLUDED_FIELDS"] = set(default_excluded) | set(excluded_fields)
|
|
100
|
+
|
|
101
|
+
if ignore_paths is not None:
|
|
102
|
+
_django_settings["IGNORE_PATHS"] = ignore_paths
|
|
18
103
|
|
|
19
104
|
return {
|
|
20
105
|
"version": 1,
|
|
21
106
|
"disable_existing_loggers": False,
|
|
22
107
|
"formatters": {
|
|
23
|
-
"json": {
|
|
108
|
+
"json": {
|
|
109
|
+
"format": "%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
110
|
+
},
|
|
24
111
|
},
|
|
25
112
|
"handlers": {
|
|
26
113
|
"console": {
|
|
27
114
|
"class": "logging.StreamHandler",
|
|
28
115
|
"formatter": "json",
|
|
29
116
|
},
|
|
117
|
+
"structlog": {
|
|
118
|
+
"()": "logify.django.StructlogHandler",
|
|
119
|
+
},
|
|
30
120
|
},
|
|
31
121
|
"root": {
|
|
32
|
-
"handlers": ["
|
|
122
|
+
"handlers": ["structlog"],
|
|
33
123
|
"level": level,
|
|
34
124
|
},
|
|
35
125
|
"loggers": {
|
|
36
126
|
"django": {
|
|
37
|
-
"handlers": ["
|
|
127
|
+
"handlers": ["structlog"],
|
|
38
128
|
"level": level,
|
|
39
129
|
"propagate": False,
|
|
40
130
|
},
|
|
41
131
|
service_name: {
|
|
42
|
-
"handlers": ["
|
|
132
|
+
"handlers": ["structlog"],
|
|
43
133
|
"level": level,
|
|
44
134
|
"propagate": False,
|
|
45
135
|
},
|
|
@@ -54,35 +144,238 @@ class LogifyMiddleware:
|
|
|
54
144
|
self.get_response = get_response
|
|
55
145
|
|
|
56
146
|
def __call__(self, request):
|
|
147
|
+
# Skip logging for ignored paths
|
|
148
|
+
if self._should_ignore_path(request.path):
|
|
149
|
+
return self.get_response(request)
|
|
150
|
+
|
|
57
151
|
# Generate request ID and set context
|
|
58
152
|
request_id = generate_request_id()
|
|
59
153
|
request.logify_request_id = request_id
|
|
60
154
|
|
|
61
|
-
|
|
155
|
+
# Get service name
|
|
156
|
+
service_name = _django_settings.get("SERVICE_NAME", "django-app")
|
|
157
|
+
|
|
158
|
+
# Get user info
|
|
159
|
+
user_info = self._get_user_info(request)
|
|
160
|
+
|
|
161
|
+
# Get and scrub headers
|
|
162
|
+
scrubbed_headers = self._scrub_headers(dict(request.headers))
|
|
163
|
+
|
|
164
|
+
# Get request body (no scrubbing needed here - core processors will handle masking)
|
|
165
|
+
request_body = self._get_request_body(request)
|
|
166
|
+
|
|
167
|
+
# Log request start with all context information ONCE
|
|
168
|
+
info(
|
|
169
|
+
"Request started",
|
|
62
170
|
request_id=request_id,
|
|
171
|
+
service=service_name,
|
|
63
172
|
method=request.method,
|
|
64
173
|
path=request.path,
|
|
65
|
-
|
|
66
|
-
|
|
174
|
+
user_info=user_info,
|
|
175
|
+
headers=scrubbed_headers,
|
|
176
|
+
query_params=dict(request.GET) if request.GET else None,
|
|
177
|
+
request_body=request_body,
|
|
67
178
|
)
|
|
68
179
|
|
|
180
|
+
# Set minimal context for other logs (only request_id)
|
|
181
|
+
set_request_context(request_id=request_id)
|
|
182
|
+
|
|
69
183
|
try:
|
|
70
184
|
response = self.get_response(request)
|
|
71
185
|
|
|
72
|
-
#
|
|
73
|
-
|
|
186
|
+
# Get response body with content type filtering
|
|
187
|
+
response_body = self._get_response_body(response)
|
|
188
|
+
|
|
189
|
+
# Log request completion with response info
|
|
190
|
+
info(
|
|
191
|
+
"Request completed",
|
|
192
|
+
request_id=request_id,
|
|
193
|
+
user_info=user_info,
|
|
74
194
|
status_code=response.status_code,
|
|
75
195
|
content_length=len(response.content) if hasattr(response, "content") else None,
|
|
196
|
+
response_body=response_body,
|
|
76
197
|
)
|
|
77
198
|
|
|
78
199
|
return response
|
|
79
200
|
finally:
|
|
80
201
|
clear_request_context()
|
|
81
202
|
|
|
203
|
+
def _should_ignore_path(self, path):
|
|
204
|
+
"""Check if path should be ignored from logging."""
|
|
205
|
+
ignore_paths = _django_settings.get("IGNORE_PATHS", [])
|
|
206
|
+
|
|
207
|
+
if isinstance(ignore_paths, (list, tuple)):
|
|
208
|
+
for ignore_path in ignore_paths:
|
|
209
|
+
if path.startswith(ignore_path):
|
|
210
|
+
return True
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
def _get_user_info(self, request):
|
|
214
|
+
"""Get user information from request."""
|
|
215
|
+
if hasattr(request, "user") and request.user.is_authenticated:
|
|
216
|
+
return f"User ID: {request.user.id}: {request.user.username}"
|
|
217
|
+
return "Anonymous user"
|
|
218
|
+
|
|
219
|
+
def _scrub_headers(self, headers):
|
|
220
|
+
"""Mask sensitive headers using sensitive_fields settings."""
|
|
221
|
+
# Get sensitive fields from core config
|
|
222
|
+
sensitive_fields = get_config_value("SENSITIVE_FIELDS")
|
|
223
|
+
|
|
224
|
+
# We need to manually scrub here because headers are a dict we are constructing to pass to log
|
|
225
|
+
# And we want to label filtered headers specifically as [FILTERED] sometimes, or just use core logic.
|
|
226
|
+
# But core logic turns things into "***".
|
|
227
|
+
# Let's align with core logic.
|
|
228
|
+
|
|
229
|
+
scrubbed = {}
|
|
230
|
+
for key, value in headers.items():
|
|
231
|
+
key_lower = key.lower().replace("-", "_") # Convert dashes to underscores for matching
|
|
232
|
+
# Check if header name contains any sensitive substring
|
|
233
|
+
if any(s in key_lower for s in sensitive_fields):
|
|
234
|
+
scrubbed[key] = "[FILTERED]"
|
|
235
|
+
else:
|
|
236
|
+
scrubbed[key] = value
|
|
237
|
+
return scrubbed
|
|
238
|
+
|
|
239
|
+
def _get_request_body(self, request):
|
|
240
|
+
"""Get request body with size limits."""
|
|
241
|
+
if not request.body:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
# Limit body size for logging (max 10KB)
|
|
246
|
+
body_bytes = request.body[:10240]
|
|
247
|
+
|
|
248
|
+
if request.content_type and "json" in request.content_type:
|
|
249
|
+
import json
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
request_body = json.loads(body_bytes.decode("utf-8"))
|
|
253
|
+
return request_body
|
|
254
|
+
except Exception:
|
|
255
|
+
# Fallback if JSON parse fails
|
|
256
|
+
pass
|
|
257
|
+
|
|
258
|
+
# For non-JSON data, try to parse as form data
|
|
259
|
+
if request.method in ["POST", "PUT", "PATCH"]:
|
|
260
|
+
try:
|
|
261
|
+
# Try form data first
|
|
262
|
+
form_data = dict(request.POST)
|
|
263
|
+
if form_data:
|
|
264
|
+
return form_data
|
|
265
|
+
except Exception:
|
|
266
|
+
pass
|
|
267
|
+
|
|
268
|
+
# Fall back to raw string (truncated)
|
|
269
|
+
# The CORE processor will mask sensitive data in this string!
|
|
270
|
+
return body_bytes.decode("utf-8", errors="replace")[:1000]
|
|
271
|
+
|
|
272
|
+
except Exception:
|
|
273
|
+
return "<non-readable body>"
|
|
274
|
+
|
|
275
|
+
def _get_response_body(self, response):
|
|
276
|
+
"""Get response body with content type filtering."""
|
|
277
|
+
if not hasattr(response, "content") or not response.content:
|
|
278
|
+
return None
|
|
279
|
+
|
|
280
|
+
content_type = response.get("Content-Type", "") if hasattr(response, "get") else ""
|
|
281
|
+
|
|
282
|
+
# Skip HTML, JavaScript, CSS, and other non-data content types
|
|
283
|
+
skip_content_types = [
|
|
284
|
+
"text/html",
|
|
285
|
+
"text/css",
|
|
286
|
+
"application/javascript",
|
|
287
|
+
"text/javascript",
|
|
288
|
+
"image/",
|
|
289
|
+
"video/",
|
|
290
|
+
"audio/",
|
|
291
|
+
"application/pdf",
|
|
292
|
+
"application/octet-stream",
|
|
293
|
+
]
|
|
294
|
+
|
|
295
|
+
if any(skip_type in content_type for skip_type in skip_content_types):
|
|
296
|
+
return f"<skipped: {content_type}>"
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
# Limit response body size for logging (max 10KB)
|
|
300
|
+
body_bytes = response.content[:10240]
|
|
301
|
+
|
|
302
|
+
if "json" in content_type:
|
|
303
|
+
import json
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
response_body = json.loads(body_bytes.decode("utf-8"))
|
|
307
|
+
return response_body
|
|
308
|
+
except Exception:
|
|
309
|
+
pass
|
|
310
|
+
|
|
311
|
+
# For non-JSON responses, return truncated text
|
|
312
|
+
return body_bytes.decode("utf-8", errors="replace")[:1000]
|
|
313
|
+
|
|
314
|
+
except Exception:
|
|
315
|
+
return "<non-readable body>"
|
|
316
|
+
|
|
82
317
|
|
|
83
318
|
def setup_django_logging(service_name: str = "django"):
|
|
84
319
|
"""
|
|
85
320
|
Set up Django logging with json-logify.
|
|
86
321
|
Call this in your Django settings or apps.py
|
|
87
322
|
"""
|
|
88
|
-
|
|
323
|
+
# Configure json-logify structlog
|
|
324
|
+
configure_core_logging(service_name=service_name)
|
|
325
|
+
|
|
326
|
+
# Configure structlog to intercept standard logging
|
|
327
|
+
structlog.configure(
|
|
328
|
+
processors=[
|
|
329
|
+
structlog.stdlib.filter_by_level,
|
|
330
|
+
structlog.stdlib.add_logger_name,
|
|
331
|
+
structlog.stdlib.add_log_level,
|
|
332
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
333
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
334
|
+
structlog.processors.StackInfoRenderer(),
|
|
335
|
+
structlog.processors.format_exc_info,
|
|
336
|
+
structlog.processors.UnicodeDecoder(),
|
|
337
|
+
structlog.processors.JSONRenderer(),
|
|
338
|
+
],
|
|
339
|
+
context_class=dict,
|
|
340
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
341
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
342
|
+
cache_logger_on_first_use=True,
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
class StructlogHandler(logging.Handler):
|
|
347
|
+
"""Custom handler that routes standard logging to structlog."""
|
|
348
|
+
|
|
349
|
+
def __init__(self):
|
|
350
|
+
super().__init__()
|
|
351
|
+
self.structlog_logger = structlog.get_logger()
|
|
352
|
+
|
|
353
|
+
def emit(self, record):
|
|
354
|
+
# Convert logging record to structlog format
|
|
355
|
+
level_name = record.levelname.lower()
|
|
356
|
+
structlog_method = getattr(self.structlog_logger, level_name, self.structlog_logger.info)
|
|
357
|
+
|
|
358
|
+
# Get excluded fields from settings
|
|
359
|
+
excluded_fields = _django_settings.get("EXCLUDED_FIELDS", set())
|
|
360
|
+
|
|
361
|
+
# Extract extra fields, excluding specified fields
|
|
362
|
+
# Note: We rely on core processors to mask sensitive data!
|
|
363
|
+
extra = {}
|
|
364
|
+
for key, value in record.__dict__.items():
|
|
365
|
+
if key not in excluded_fields:
|
|
366
|
+
# Skip complex objects that can't be JSON serialized
|
|
367
|
+
# Core has a cleaner which handles this too, but we can filtering here for safety
|
|
368
|
+
if hasattr(value, "__dict__") and not isinstance(value, (str, int, float, bool, list, dict)):
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
extra[key] = value
|
|
372
|
+
|
|
373
|
+
# Log with structlog
|
|
374
|
+
structlog_method(
|
|
375
|
+
record.getMessage(),
|
|
376
|
+
logger=record.name,
|
|
377
|
+
module=record.module,
|
|
378
|
+
funcName=record.funcName,
|
|
379
|
+
lineno=record.lineno,
|
|
380
|
+
**extra,
|
|
381
|
+
)
|
logify/fastapi.py
CHANGED
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
"""
|
|
2
|
-
FastAPI integration for json-logify structured logging.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import time
|
|
6
|
-
|
|
7
|
-
from fastapi import Request
|
|
8
|
-
from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
|
-
|
|
10
|
-
from .core import clear_request_context, configure_logging, error, generate_request_id, info, set_request_context
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class LogifyMiddleware(BaseHTTPMiddleware):
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
def setup_fastapi_logging(service_name: str = "fastapi"):
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
1
|
+
# """
|
|
2
|
+
# FastAPI integration for json-logify structured logging.
|
|
3
|
+
# """
|
|
4
|
+
#
|
|
5
|
+
# import time
|
|
6
|
+
#
|
|
7
|
+
# from fastapi import Request
|
|
8
|
+
# from starlette.middleware.base import BaseHTTPMiddleware
|
|
9
|
+
#
|
|
10
|
+
# from .core import clear_request_context, configure_logging, error, generate_request_id, info, set_request_context
|
|
11
|
+
#
|
|
12
|
+
#
|
|
13
|
+
# class LogifyMiddleware(BaseHTTPMiddleware):
|
|
14
|
+
# """FastAPI middleware for structured logging with request context."""
|
|
15
|
+
#
|
|
16
|
+
# def __init__(self, app, service_name: str = "fastapi", log_requests: bool = True):
|
|
17
|
+
# super().__init__(app)
|
|
18
|
+
# self.service_name = service_name
|
|
19
|
+
# self.log_requests = log_requests
|
|
20
|
+
# configure_logging(service_name=service_name)
|
|
21
|
+
#
|
|
22
|
+
# async def dispatch(self, request: Request, call_next):
|
|
23
|
+
# # Generate request ID and set context
|
|
24
|
+
# request_id = generate_request_id()
|
|
25
|
+
# start_time = time.time()
|
|
26
|
+
#
|
|
27
|
+
# # Extract client info
|
|
28
|
+
# client_host = getattr(request.client, "host", "unknown") if request.client else "unknown"
|
|
29
|
+
#
|
|
30
|
+
# set_request_context(
|
|
31
|
+
# request_id=request_id,
|
|
32
|
+
# method=request.method,
|
|
33
|
+
# path=request.url.path,
|
|
34
|
+
# query_params=str(request.query_params) if request.query_params else None,
|
|
35
|
+
# client_host=client_host,
|
|
36
|
+
# user_agent=request.headers.get("user-agent", ""),
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
39
|
+
# if self.log_requests:
|
|
40
|
+
# info("Request started", method=request.method, path=request.url.path, request_id=request_id)
|
|
41
|
+
#
|
|
42
|
+
# try:
|
|
43
|
+
# response = await call_next(request)
|
|
44
|
+
#
|
|
45
|
+
# duration = time.time() - start_time
|
|
46
|
+
#
|
|
47
|
+
# # Add response info to context
|
|
48
|
+
# set_request_context(status_code=response.status_code, duration_seconds=duration)
|
|
49
|
+
#
|
|
50
|
+
# if self.log_requests:
|
|
51
|
+
# info(
|
|
52
|
+
# "Request completed",
|
|
53
|
+
# status_code=response.status_code,
|
|
54
|
+
# duration_seconds=duration,
|
|
55
|
+
# request_id=request_id,
|
|
56
|
+
# )
|
|
57
|
+
#
|
|
58
|
+
# return response
|
|
59
|
+
#
|
|
60
|
+
# except Exception as e:
|
|
61
|
+
# duration = time.time() - start_time
|
|
62
|
+
#
|
|
63
|
+
# if self.log_requests:
|
|
64
|
+
# error(
|
|
65
|
+
# "Request failed",
|
|
66
|
+
# error=e,
|
|
67
|
+
# duration_seconds=duration,
|
|
68
|
+
# request_id=request_id,
|
|
69
|
+
# )
|
|
70
|
+
#
|
|
71
|
+
# raise
|
|
72
|
+
#
|
|
73
|
+
# finally:
|
|
74
|
+
# clear_request_context()
|
|
75
|
+
#
|
|
76
|
+
#
|
|
77
|
+
# def setup_fastapi_logging(service_name: str = "fastapi"):
|
|
78
|
+
# """
|
|
79
|
+
# Set up FastAPI logging with json-logify.
|
|
80
|
+
# Call this when creating your FastAPI app.
|
|
81
|
+
# """
|
|
82
|
+
# configure_logging(service_name=service_name)
|
logify/flask.py
CHANGED
|
@@ -1,88 +1,88 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Flask integration for json-logify structured logging.
|
|
3
|
-
"""
|
|
4
|
-
|
|
5
|
-
import time
|
|
6
|
-
|
|
7
|
-
from flask import Flask, g, request
|
|
8
|
-
|
|
9
|
-
from .core import clear_request_context, configure_logging, error, generate_request_id, info, set_request_context
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def init_logify(app: Flask, service_name: str = "flask", log_requests: bool = True):
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def setup_flask_logging(service_name: str = "flask"):
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
1
|
+
# """
|
|
2
|
+
# Flask integration for json-logify structured logging.
|
|
3
|
+
# """
|
|
4
|
+
#
|
|
5
|
+
# import time
|
|
6
|
+
#
|
|
7
|
+
# from flask import Flask, g, request
|
|
8
|
+
#
|
|
9
|
+
# from .core import clear_request_context, configure_logging, error, generate_request_id, info, set_request_context
|
|
10
|
+
#
|
|
11
|
+
#
|
|
12
|
+
# def init_logify(app: Flask, service_name: str = "flask", log_requests: bool = True):
|
|
13
|
+
# """
|
|
14
|
+
# Initialize json-logify for Flask application.
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# from flask import Flask
|
|
18
|
+
# from logify.flask import init_logify
|
|
19
|
+
#
|
|
20
|
+
# app = Flask(__name__)
|
|
21
|
+
# init_logify(app, service_name="myapp")
|
|
22
|
+
# """
|
|
23
|
+
# configure_logging(service_name=service_name)
|
|
24
|
+
#
|
|
25
|
+
# @app.before_request
|
|
26
|
+
# def before_request():
|
|
27
|
+
# # Generate request ID and set context
|
|
28
|
+
# request_id = generate_request_id()
|
|
29
|
+
# g.logify_request_id = request_id
|
|
30
|
+
# g.logify_start_time = time.time()
|
|
31
|
+
#
|
|
32
|
+
# set_request_context(
|
|
33
|
+
# request_id=request_id,
|
|
34
|
+
# method=request.method,
|
|
35
|
+
# path=request.path,
|
|
36
|
+
# query_string=request.query_string.decode() if request.query_string else None,
|
|
37
|
+
# remote_addr=request.remote_addr,
|
|
38
|
+
# user_agent=request.headers.get("User-Agent", ""),
|
|
39
|
+
# )
|
|
40
|
+
#
|
|
41
|
+
# if log_requests:
|
|
42
|
+
# info("Request started", method=request.method, path=request.path, request_id=request_id)
|
|
43
|
+
#
|
|
44
|
+
# @app.after_request
|
|
45
|
+
# def after_request(response):
|
|
46
|
+
# try:
|
|
47
|
+
# # Calculate duration
|
|
48
|
+
# duration = time.time() - g.logify_start_time
|
|
49
|
+
#
|
|
50
|
+
# # Add response info to context
|
|
51
|
+
# set_request_context(
|
|
52
|
+
# status_code=response.status_code, duration_seconds=duration, content_length=response.content_length
|
|
53
|
+
# )
|
|
54
|
+
#
|
|
55
|
+
# if log_requests:
|
|
56
|
+
# info(
|
|
57
|
+
# "Request completed",
|
|
58
|
+
# status_code=response.status_code,
|
|
59
|
+
# duration_seconds=duration,
|
|
60
|
+
# request_id=g.logify_request_id,
|
|
61
|
+
# )
|
|
62
|
+
# except Exception:
|
|
63
|
+
# # If we can't access g, just return the response
|
|
64
|
+
# pass
|
|
65
|
+
#
|
|
66
|
+
# return response
|
|
67
|
+
#
|
|
68
|
+
# def teardown_request(exception):
|
|
69
|
+
# # Clear request context
|
|
70
|
+
# clear_request_context()
|
|
71
|
+
#
|
|
72
|
+
# if exception and log_requests:
|
|
73
|
+
# error(
|
|
74
|
+
# "Request failed",
|
|
75
|
+
# error=exception,
|
|
76
|
+
# request_id=getattr(g, "logify_request_id", "unknown"),
|
|
77
|
+
# )
|
|
78
|
+
#
|
|
79
|
+
# # Register teardown function
|
|
80
|
+
# app.teardown_appcontext(teardown_request)
|
|
81
|
+
#
|
|
82
|
+
#
|
|
83
|
+
# def setup_flask_logging(service_name: str = "flask"):
|
|
84
|
+
# """
|
|
85
|
+
# Set up Flask logging with json-logify.
|
|
86
|
+
# Call this when creating your Flask app.
|
|
87
|
+
# """
|
|
88
|
+
# configure_logging(service_name=service_name)
|
logify/version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.1.
|
|
1
|
+
__version__ = "0.1.2"
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
json_logify-0.1.0.dist-info/licenses/LICENSE,sha256=qRyWd6Y0_db_j_ECsAAEJW8-XMnhYUg_yK3c5UkOfNI,1077
|
|
2
|
-
logify/__init__.py,sha256=t2Jjw7T4jRMIYvXJOTBraaW6MneifmS_gDKDlrRHmeU,1053
|
|
3
|
-
logify/core.py,sha256=kiMraMhpH9GShY0lPhXAnBO0bTMe0a77cIVpWldecZY,5548
|
|
4
|
-
logify/django.py,sha256=i5z9-nzrBF712ba1xkMpAsND-RLFopDLk8M7tsyTPzk,2526
|
|
5
|
-
logify/fastapi.py,sha256=ACQPWGVfTexUUCQX2oh-s5NBT1q0q5UDARbNsKulamU,2567
|
|
6
|
-
logify/flask.py,sha256=YPRsaFiojP-2buZCwNhm5JmpgBAakJAQsKy1hh6r8ek,2645
|
|
7
|
-
logify/version.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
|
|
8
|
-
json_logify-0.1.0.dist-info/METADATA,sha256=Lu5bw9vf8mz16MTG0jkOXmKltqX1osvMl8E03aXRR30,7659
|
|
9
|
-
json_logify-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
json_logify-0.1.0.dist-info/top_level.txt,sha256=MRERkeaav8J-KtwGpiLDCnSNQ5e2JqKUMAlJnYVntao,7
|
|
11
|
-
json_logify-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|