json-logify 0.1.0__py3-none-any.whl → 0.1.2__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.2.dist-info}/METADATA +154 -43
- json_logify-0.1.2.dist-info/RECORD +11 -0
- logify/core.py +142 -9
- logify/django.py +363 -11
- 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.2.dist-info}/WHEEL +0 -0
- {json_logify-0.1.0.dist-info → json_logify-0.1.2.dist-info}/licenses/LICENSE +0 -0
- {json_logify-0.1.0.dist-info → json_logify-0.1.2.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.2
|
|
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.2.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=Th9JXajJ1ciX1DEcMH88412-CMyIpYMCGIw1h2zeLq4,10249
|
|
4
|
+
logify/django.py,sha256=X5S8lG3yow13XI-GAHC2tT7WRg-yjXcdRwip03GZOR8,14816
|
|
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.2.dist-info/METADATA,sha256=ZZQVSFVGb2ZSgEcdMDVfxTdBypXXKAcBIKdewCRKnYA,11563
|
|
9
|
+
json_logify-0.1.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
+
json_logify-0.1.2.dist-info/top_level.txt,sha256=MRERkeaav8J-KtwGpiLDCnSNQ5e2JqKUMAlJnYVntao,7
|
|
11
|
+
json_logify-0.1.2.dist-info/RECORD,,
|
logify/core.py
CHANGED
|
@@ -21,6 +21,125 @@ def orjson_serializer(_, __, event_dict):
|
|
|
21
21
|
return orjson.dumps(event_dict).decode("utf-8")
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
def truncate_long_strings(_, __, event_dict):
|
|
25
|
+
"""Truncate long strings in log entries based on max_string_length setting."""
|
|
26
|
+
# Get max length from global settings only
|
|
27
|
+
max_length = 100
|
|
28
|
+
try:
|
|
29
|
+
from .django import _get_setting
|
|
30
|
+
|
|
31
|
+
max_length = _get_setting("LOGIFY_MAX_STRING_LENGTH", 100)
|
|
32
|
+
except ImportError:
|
|
33
|
+
# Use default if django module not available
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
# Truncate long strings - only if max_length is positive
|
|
37
|
+
if max_length > 0:
|
|
38
|
+
for key, value in event_dict.items():
|
|
39
|
+
if isinstance(value, str) and len(value) > max_length:
|
|
40
|
+
event_dict[key] = value[:max_length] + "..."
|
|
41
|
+
|
|
42
|
+
return event_dict
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def clean_non_serializable_objects(_, __, event_dict):
|
|
46
|
+
"""Clean non-serializable objects from log entries."""
|
|
47
|
+
# Get settings for string length limits
|
|
48
|
+
max_length = 100
|
|
49
|
+
try:
|
|
50
|
+
from .django import _get_setting
|
|
51
|
+
|
|
52
|
+
max_length = _get_setting("LOGIFY_MAX_STRING_LENGTH", 100)
|
|
53
|
+
except ImportError:
|
|
54
|
+
pass
|
|
55
|
+
|
|
56
|
+
cleaned = {}
|
|
57
|
+
for key, value in event_dict.items():
|
|
58
|
+
# Skip 'error' field - it will be handled specially in format_log_entry
|
|
59
|
+
if key == "error":
|
|
60
|
+
cleaned[key] = value
|
|
61
|
+
continue
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
# Test if value is JSON serializable
|
|
65
|
+
orjson.dumps(value)
|
|
66
|
+
cleaned[key] = value
|
|
67
|
+
except TypeError:
|
|
68
|
+
# Convert non-serializable objects to their string representation
|
|
69
|
+
if hasattr(value, "__class__"):
|
|
70
|
+
cleaned[key] = f"<{value.__class__.__name__}: {str(value)[:max_length]}>"
|
|
71
|
+
else:
|
|
72
|
+
cleaned[key] = str(value)[:max_length]
|
|
73
|
+
return cleaned
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def mask_sensitive_fields(_, __, event_dict):
|
|
77
|
+
"""Mask sensitive fields in log entries recursively."""
|
|
78
|
+
# Default sensitive fields
|
|
79
|
+
default_sensitive = [
|
|
80
|
+
"password",
|
|
81
|
+
"passwd",
|
|
82
|
+
"pass",
|
|
83
|
+
"pwd",
|
|
84
|
+
"secret",
|
|
85
|
+
"token",
|
|
86
|
+
"key",
|
|
87
|
+
"api_key",
|
|
88
|
+
"access_token",
|
|
89
|
+
"refresh_token",
|
|
90
|
+
"auth_token",
|
|
91
|
+
"session_key",
|
|
92
|
+
"private_key",
|
|
93
|
+
"credit_card",
|
|
94
|
+
"card_number",
|
|
95
|
+
"cvv",
|
|
96
|
+
"ssn",
|
|
97
|
+
"social_security_number",
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
# Get sensitive fields from global settings only
|
|
101
|
+
sensitive_fields = default_sensitive
|
|
102
|
+
try:
|
|
103
|
+
from .django import _get_setting
|
|
104
|
+
|
|
105
|
+
sensitive_fields = _get_setting("LOGIFY_SENSITIVE_FIELDS", default_sensitive)
|
|
106
|
+
except ImportError:
|
|
107
|
+
# Use defaults if django module not available
|
|
108
|
+
pass
|
|
109
|
+
|
|
110
|
+
# Convert to set of lowercase strings
|
|
111
|
+
if isinstance(sensitive_fields, (list, tuple)):
|
|
112
|
+
sensitive_fields = set(field.lower() for field in sensitive_fields)
|
|
113
|
+
elif isinstance(sensitive_fields, set):
|
|
114
|
+
sensitive_fields = set(field.lower() for field in sensitive_fields)
|
|
115
|
+
else:
|
|
116
|
+
sensitive_fields = set(field.lower() for field in default_sensitive)
|
|
117
|
+
|
|
118
|
+
def _mask_recursive(obj):
|
|
119
|
+
"""Recursively mask sensitive fields in dicts and lists."""
|
|
120
|
+
if isinstance(obj, dict):
|
|
121
|
+
masked = {}
|
|
122
|
+
for k, v in obj.items():
|
|
123
|
+
key_lower = k.lower()
|
|
124
|
+
# Check if key contains any sensitive substring
|
|
125
|
+
if any(s in key_lower for s in sensitive_fields):
|
|
126
|
+
if v and str(v).strip():
|
|
127
|
+
masked[k] = "***"
|
|
128
|
+
else:
|
|
129
|
+
masked[k] = v
|
|
130
|
+
else:
|
|
131
|
+
masked[k] = _mask_recursive(v)
|
|
132
|
+
return masked
|
|
133
|
+
elif isinstance(obj, list):
|
|
134
|
+
return [_mask_recursive(item) for item in obj]
|
|
135
|
+
return obj
|
|
136
|
+
|
|
137
|
+
# Apply masking to the entire event_dict (including top-level keys)
|
|
138
|
+
event_dict = _mask_recursive(event_dict)
|
|
139
|
+
|
|
140
|
+
return event_dict
|
|
141
|
+
|
|
142
|
+
|
|
24
143
|
def add_timestamp(_, __, event_dict):
|
|
25
144
|
"""Add ISO timestamp to log entries."""
|
|
26
145
|
# Unresolved attribute reference 'UTC' for class 'datetime'
|
|
@@ -39,11 +158,12 @@ def format_log_entry(logger, name, event_dict):
|
|
|
39
158
|
# Remove level from event_dict to avoid duplication
|
|
40
159
|
event_dict.pop("level", None)
|
|
41
160
|
|
|
42
|
-
# Get context
|
|
161
|
+
# Get only request_id from context to avoid duplication
|
|
43
162
|
try:
|
|
44
163
|
context = _request_context.get({})
|
|
164
|
+
request_id_only = {"request_id": context.get("request_id")} if context.get("request_id") else {}
|
|
45
165
|
except LookupError:
|
|
46
|
-
|
|
166
|
+
request_id_only = {}
|
|
47
167
|
|
|
48
168
|
# Extract error from event_dict if present
|
|
49
169
|
error_field = event_dict.pop("error", None)
|
|
@@ -53,12 +173,19 @@ def format_log_entry(logger, name, event_dict):
|
|
|
53
173
|
"timestamp": event_dict.pop("timestamp", datetime.now(UTC).isoformat().replace("+00:00", "Z")),
|
|
54
174
|
"message": message,
|
|
55
175
|
"level": level.upper(),
|
|
56
|
-
"payload": {**
|
|
176
|
+
"payload": {**request_id_only, **event_dict},
|
|
57
177
|
}
|
|
58
178
|
|
|
59
179
|
# Add optional error field at top level
|
|
60
180
|
if error_field:
|
|
61
|
-
|
|
181
|
+
# Format as "ErrorType: message"
|
|
182
|
+
if isinstance(error_field, Exception):
|
|
183
|
+
if error_field.args:
|
|
184
|
+
formatted_entry["error"] = f"{error_field.__class__.__name__}: {error_field.args[0]}"
|
|
185
|
+
else:
|
|
186
|
+
formatted_entry["error"] = f"{error_field.__class__.__name__}: No message"
|
|
187
|
+
else:
|
|
188
|
+
formatted_entry["error"] = str(error_field)
|
|
62
189
|
|
|
63
190
|
return formatted_entry
|
|
64
191
|
|
|
@@ -67,6 +194,9 @@ def format_log_entry(logger, name, event_dict):
|
|
|
67
194
|
structlog.configure(
|
|
68
195
|
processors=[
|
|
69
196
|
structlog.stdlib.add_log_level,
|
|
197
|
+
clean_non_serializable_objects, # Clean non-serializable objects FIRST
|
|
198
|
+
truncate_long_strings, # Truncate long strings SECOND
|
|
199
|
+
mask_sensitive_fields, # Mask sensitive fields THIRD
|
|
70
200
|
add_timestamp,
|
|
71
201
|
format_log_entry,
|
|
72
202
|
orjson_serializer,
|
|
@@ -106,7 +236,7 @@ def error(message: str, error: Exception = None, **kwargs):
|
|
|
106
236
|
"""Log error message."""
|
|
107
237
|
if error:
|
|
108
238
|
kwargs["error"] = error # Will be moved to top-level by format_log_entry
|
|
109
|
-
|
|
239
|
+
# error_type removed - now included in error field as "Type: message"
|
|
110
240
|
logger.error(message, **kwargs)
|
|
111
241
|
|
|
112
242
|
|
|
@@ -123,10 +253,10 @@ def exception(message: str, **kwargs):
|
|
|
123
253
|
logger.error(message, **kwargs)
|
|
124
254
|
|
|
125
255
|
|
|
126
|
-
def get_logger(name: str = "json-logify"
|
|
256
|
+
def get_logger(name: str = "json-logify"):
|
|
127
257
|
"""Get a named logger instance."""
|
|
128
258
|
bound_logger = structlog.get_logger(name)
|
|
129
|
-
return bound_logger
|
|
259
|
+
return bound_logger
|
|
130
260
|
|
|
131
261
|
|
|
132
262
|
def configure_logging(service_name: str = "app", level: str = "INFO"):
|
|
@@ -138,6 +268,9 @@ def configure_logging(service_name: str = "app", level: str = "INFO"):
|
|
|
138
268
|
|
|
139
269
|
structlog.configure(
|
|
140
270
|
processors=[
|
|
271
|
+
clean_non_serializable_objects, # Clean non-serializable objects FIRST
|
|
272
|
+
truncate_long_strings, # Truncate long strings SECOND
|
|
273
|
+
mask_sensitive_fields, # Mask sensitive fields THIRD
|
|
141
274
|
add_timestamp,
|
|
142
275
|
format_log_entry,
|
|
143
276
|
orjson_serializer,
|
|
@@ -147,9 +280,9 @@ def configure_logging(service_name: str = "app", level: str = "INFO"):
|
|
|
147
280
|
cache_logger_on_first_use=True,
|
|
148
281
|
)
|
|
149
282
|
|
|
150
|
-
#
|
|
283
|
+
# Set default logger without service binding
|
|
151
284
|
global logger
|
|
152
|
-
logger = structlog.get_logger("json-logify")
|
|
285
|
+
logger = structlog.get_logger("json-logify")
|
|
153
286
|
|
|
154
287
|
|
|
155
288
|
def bind(**kwargs):
|
logify/django.py
CHANGED
|
@@ -2,44 +2,102 @@
|
|
|
2
2
|
Django integration for json-logify structured logging.
|
|
3
3
|
"""
|
|
4
4
|
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import structlog
|
|
8
|
+
|
|
5
9
|
from .core import clear_request_context, configure_logging, generate_request_id, set_request_context
|
|
6
10
|
|
|
11
|
+
# Global settings storage for when Django settings are not available
|
|
12
|
+
_global_settings = {}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _set_global_setting(key: str, value):
|
|
16
|
+
"""Set a global setting value."""
|
|
17
|
+
_global_settings[key] = value
|
|
7
18
|
|
|
8
|
-
|
|
19
|
+
|
|
20
|
+
def _get_setting(key: str, default=None):
|
|
21
|
+
"""Get setting value from global settings only."""
|
|
22
|
+
# Return value from global settings (set by get_logging_config parameters)
|
|
23
|
+
return _global_settings.get(key, default)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_logging_config(
|
|
27
|
+
service_name: str = "django",
|
|
28
|
+
level: str = "INFO",
|
|
29
|
+
json_logs: bool = True,
|
|
30
|
+
excluded_fields: list = None,
|
|
31
|
+
sensitive_fields: list = None,
|
|
32
|
+
max_string_length: int = None,
|
|
33
|
+
ignore_paths: list = None,
|
|
34
|
+
):
|
|
9
35
|
"""
|
|
10
36
|
Get Django logging configuration for json-logify.
|
|
11
37
|
|
|
38
|
+
Args:
|
|
39
|
+
service_name: Name of the service for logging
|
|
40
|
+
level: Logging level (DEBUG, INFO, WARNING, ERROR)
|
|
41
|
+
json_logs: Whether to enable JSON logging
|
|
42
|
+
excluded_fields: List of fields to exclude from logs
|
|
43
|
+
sensitive_fields: List of fields to mask with *** (also used for sensitive headers)
|
|
44
|
+
max_string_length: Maximum length for string truncation
|
|
45
|
+
ignore_paths: List of URL paths to ignore from logging
|
|
46
|
+
|
|
12
47
|
Usage in settings.py:
|
|
13
48
|
from logify.django import get_logging_config
|
|
14
|
-
LOGGING = get_logging_config(
|
|
49
|
+
LOGGING = get_logging_config(
|
|
50
|
+
service_name="myapp",
|
|
51
|
+
level="INFO",
|
|
52
|
+
excluded_fields=["custom_field"],
|
|
53
|
+
sensitive_fields=["password", "secret", "authorization"],
|
|
54
|
+
max_string_length=200,
|
|
55
|
+
ignore_paths=["/health/", "/static/"]
|
|
56
|
+
)
|
|
15
57
|
"""
|
|
16
58
|
if json_logs:
|
|
17
59
|
configure_logging(service_name=service_name, level=level)
|
|
18
60
|
|
|
61
|
+
# Store settings globally for the processors to use
|
|
62
|
+
_set_global_setting("SERVICE_NAME", service_name)
|
|
63
|
+
if excluded_fields is not None:
|
|
64
|
+
_set_global_setting("LOGIFY_EXCLUDED_FIELDS", excluded_fields)
|
|
65
|
+
if sensitive_fields is not None:
|
|
66
|
+
_set_global_setting("LOGIFY_SENSITIVE_FIELDS", sensitive_fields)
|
|
67
|
+
if max_string_length is not None:
|
|
68
|
+
_set_global_setting("LOGIFY_MAX_STRING_LENGTH", max_string_length)
|
|
69
|
+
if ignore_paths is not None:
|
|
70
|
+
_set_global_setting("LOGIFY_IGNORE_PATHS", ignore_paths)
|
|
71
|
+
|
|
19
72
|
return {
|
|
20
73
|
"version": 1,
|
|
21
74
|
"disable_existing_loggers": False,
|
|
22
75
|
"formatters": {
|
|
23
|
-
"json": {
|
|
76
|
+
"json": {
|
|
77
|
+
"format": "%(asctime)s %(name)s %(levelname)s %(message)s",
|
|
78
|
+
},
|
|
24
79
|
},
|
|
25
80
|
"handlers": {
|
|
26
81
|
"console": {
|
|
27
82
|
"class": "logging.StreamHandler",
|
|
28
83
|
"formatter": "json",
|
|
29
84
|
},
|
|
85
|
+
"structlog": {
|
|
86
|
+
"()": "logify.django.StructlogHandler",
|
|
87
|
+
},
|
|
30
88
|
},
|
|
31
89
|
"root": {
|
|
32
|
-
"handlers": ["
|
|
90
|
+
"handlers": ["structlog"],
|
|
33
91
|
"level": level,
|
|
34
92
|
},
|
|
35
93
|
"loggers": {
|
|
36
94
|
"django": {
|
|
37
|
-
"handlers": ["
|
|
95
|
+
"handlers": ["structlog"],
|
|
38
96
|
"level": level,
|
|
39
97
|
"propagate": False,
|
|
40
98
|
},
|
|
41
99
|
service_name: {
|
|
42
|
-
"handlers": ["
|
|
100
|
+
"handlers": ["structlog"],
|
|
43
101
|
"level": level,
|
|
44
102
|
"propagate": False,
|
|
45
103
|
},
|
|
@@ -54,35 +112,329 @@ class LogifyMiddleware:
|
|
|
54
112
|
self.get_response = get_response
|
|
55
113
|
|
|
56
114
|
def __call__(self, request):
|
|
115
|
+
# Skip logging for ignored paths
|
|
116
|
+
if self._should_ignore_path(request.path):
|
|
117
|
+
return self.get_response(request)
|
|
118
|
+
|
|
119
|
+
# Import here to avoid circular imports
|
|
120
|
+
from .core import info
|
|
121
|
+
|
|
57
122
|
# Generate request ID and set context
|
|
58
123
|
request_id = generate_request_id()
|
|
59
124
|
request.logify_request_id = request_id
|
|
60
125
|
|
|
61
|
-
|
|
126
|
+
# Get service name from global settings or use default
|
|
127
|
+
service_name = _get_setting("SERVICE_NAME", "django-app")
|
|
128
|
+
|
|
129
|
+
# Get user info
|
|
130
|
+
user_info = self._get_user_info(request)
|
|
131
|
+
|
|
132
|
+
# Get and scrub headers
|
|
133
|
+
scrubbed_headers = self._scrub_headers(dict(request.headers))
|
|
134
|
+
|
|
135
|
+
# Get request body (no scrubbing - core processors will handle)
|
|
136
|
+
request_body = self._get_request_body(request)
|
|
137
|
+
|
|
138
|
+
# Log request start with all context information ONCE
|
|
139
|
+
info(
|
|
140
|
+
"Request started",
|
|
62
141
|
request_id=request_id,
|
|
142
|
+
service=service_name,
|
|
63
143
|
method=request.method,
|
|
64
144
|
path=request.path,
|
|
65
|
-
|
|
66
|
-
|
|
145
|
+
user_info=user_info,
|
|
146
|
+
headers=scrubbed_headers,
|
|
147
|
+
query_params=dict(request.GET) if request.GET else None,
|
|
148
|
+
request_body=request_body,
|
|
67
149
|
)
|
|
68
150
|
|
|
151
|
+
# Set minimal context for other logs (only request_id)
|
|
152
|
+
set_request_context(request_id=request_id)
|
|
153
|
+
|
|
69
154
|
try:
|
|
70
155
|
response = self.get_response(request)
|
|
71
156
|
|
|
72
|
-
#
|
|
73
|
-
|
|
157
|
+
# Get response body with content type filtering (no scrubbing)
|
|
158
|
+
response_body = self._get_response_body(response)
|
|
159
|
+
|
|
160
|
+
# Log request completion with response info
|
|
161
|
+
info(
|
|
162
|
+
"Request completed",
|
|
163
|
+
request_id=request_id,
|
|
164
|
+
user_info=user_info,
|
|
74
165
|
status_code=response.status_code,
|
|
75
166
|
content_length=len(response.content) if hasattr(response, "content") else None,
|
|
167
|
+
response_body=response_body,
|
|
76
168
|
)
|
|
77
169
|
|
|
78
170
|
return response
|
|
79
171
|
finally:
|
|
80
172
|
clear_request_context()
|
|
81
173
|
|
|
174
|
+
def _should_ignore_path(self, path):
|
|
175
|
+
"""Check if path should be ignored from logging."""
|
|
176
|
+
# Get ignore paths from global settings
|
|
177
|
+
default_ignore_paths = ["/health/", "/healthz/", "/api/schema/", "/static/", "/favicon.ico", "/robots.txt"]
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
ignore_paths = _get_setting("LOGIFY_IGNORE_PATHS", default_ignore_paths)
|
|
181
|
+
except Exception:
|
|
182
|
+
ignore_paths = default_ignore_paths
|
|
183
|
+
|
|
184
|
+
if isinstance(ignore_paths, (list, tuple)):
|
|
185
|
+
for ignore_path in ignore_paths:
|
|
186
|
+
if path.startswith(ignore_path):
|
|
187
|
+
return True
|
|
188
|
+
return False
|
|
189
|
+
|
|
190
|
+
def _get_user_info(self, request):
|
|
191
|
+
"""Get user information from request."""
|
|
192
|
+
if hasattr(request, "user") and request.user.is_authenticated:
|
|
193
|
+
return f"User ID: {request.user.id}: {request.user.username}"
|
|
194
|
+
return "Anonymous user"
|
|
195
|
+
|
|
196
|
+
def _scrub_headers(self, headers):
|
|
197
|
+
"""Mask sensitive headers using sensitive_fields settings."""
|
|
198
|
+
# Get sensitive fields which will be used for headers too
|
|
199
|
+
default_sensitive = [
|
|
200
|
+
"password",
|
|
201
|
+
"passwd",
|
|
202
|
+
"pass",
|
|
203
|
+
"pwd",
|
|
204
|
+
"secret",
|
|
205
|
+
"token",
|
|
206
|
+
"key",
|
|
207
|
+
"api_key",
|
|
208
|
+
"access_token",
|
|
209
|
+
"refresh_token",
|
|
210
|
+
"auth_token",
|
|
211
|
+
"session_key",
|
|
212
|
+
"private_key",
|
|
213
|
+
"authorization",
|
|
214
|
+
"cookie",
|
|
215
|
+
"x-api-key",
|
|
216
|
+
"x-auth-token",
|
|
217
|
+
"x-csrf-token",
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
try:
|
|
221
|
+
sensitive_fields = _get_setting("LOGIFY_SENSITIVE_FIELDS", default_sensitive)
|
|
222
|
+
except Exception:
|
|
223
|
+
sensitive_fields = default_sensitive
|
|
224
|
+
|
|
225
|
+
if isinstance(sensitive_fields, (list, tuple)):
|
|
226
|
+
sensitive_fields = set(field.lower() for field in sensitive_fields)
|
|
227
|
+
else:
|
|
228
|
+
sensitive_fields = set(field.lower() for field in default_sensitive)
|
|
229
|
+
|
|
230
|
+
scrubbed = {}
|
|
231
|
+
for key, value in headers.items():
|
|
232
|
+
key_lower = key.lower().replace("-", "_") # Convert dashes to underscores for matching
|
|
233
|
+
# Check if header name contains any sensitive substring
|
|
234
|
+
if any(s in key_lower for s in sensitive_fields):
|
|
235
|
+
scrubbed[key] = "[FILTERED]"
|
|
236
|
+
else:
|
|
237
|
+
scrubbed[key] = value
|
|
238
|
+
return scrubbed
|
|
239
|
+
|
|
240
|
+
def _get_request_body(self, request):
|
|
241
|
+
"""Get request body with size limits (no scrubbing - core processors will handle)."""
|
|
242
|
+
if not request.body:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
# Limit body size for logging (max 10KB)
|
|
247
|
+
body_bytes = request.body[:10240]
|
|
248
|
+
|
|
249
|
+
if request.content_type and "json" in request.content_type:
|
|
250
|
+
import json
|
|
251
|
+
|
|
252
|
+
request_body = json.loads(body_bytes.decode("utf-8"))
|
|
253
|
+
return request_body
|
|
254
|
+
else:
|
|
255
|
+
# For non-JSON data, try to parse as form data
|
|
256
|
+
if request.method in ["POST", "PUT", "PATCH"]:
|
|
257
|
+
try:
|
|
258
|
+
# Try form data first
|
|
259
|
+
form_data = dict(request.POST)
|
|
260
|
+
if form_data:
|
|
261
|
+
return form_data
|
|
262
|
+
except Exception:
|
|
263
|
+
pass
|
|
264
|
+
|
|
265
|
+
# Fall back to raw string (truncated)
|
|
266
|
+
return body_bytes.decode("utf-8", errors="replace")[:1000]
|
|
267
|
+
|
|
268
|
+
except Exception:
|
|
269
|
+
return "<non-readable body>"
|
|
270
|
+
|
|
271
|
+
def _get_response_body(self, response):
|
|
272
|
+
"""Get response body with content type filtering (no scrubbing - core processors will handle)."""
|
|
273
|
+
if not hasattr(response, "content") or not response.content:
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
content_type = response.get("Content-Type", "") if hasattr(response, "get") else ""
|
|
277
|
+
|
|
278
|
+
# Skip HTML, JavaScript, CSS, and other non-data content types
|
|
279
|
+
skip_content_types = [
|
|
280
|
+
"text/html",
|
|
281
|
+
"text/css",
|
|
282
|
+
"application/javascript",
|
|
283
|
+
"text/javascript",
|
|
284
|
+
"image/",
|
|
285
|
+
"video/",
|
|
286
|
+
"audio/",
|
|
287
|
+
"application/pdf",
|
|
288
|
+
"application/octet-stream",
|
|
289
|
+
]
|
|
290
|
+
|
|
291
|
+
if any(skip_type in content_type for skip_type in skip_content_types):
|
|
292
|
+
return f"<skipped: {content_type}>"
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
# Limit response body size for logging (max 10KB)
|
|
296
|
+
body_bytes = response.content[:10240]
|
|
297
|
+
|
|
298
|
+
if "json" in content_type:
|
|
299
|
+
import json
|
|
300
|
+
|
|
301
|
+
response_body = json.loads(body_bytes.decode("utf-8"))
|
|
302
|
+
return response_body
|
|
303
|
+
else:
|
|
304
|
+
# For non-JSON responses, return truncated text
|
|
305
|
+
return body_bytes.decode("utf-8", errors="replace")[:1000]
|
|
306
|
+
|
|
307
|
+
except Exception:
|
|
308
|
+
return "<non-readable body>"
|
|
309
|
+
|
|
82
310
|
|
|
83
311
|
def setup_django_logging(service_name: str = "django"):
|
|
84
312
|
"""
|
|
85
313
|
Set up Django logging with json-logify.
|
|
86
314
|
Call this in your Django settings or apps.py
|
|
87
315
|
"""
|
|
316
|
+
# Configure json-logify structlog
|
|
88
317
|
configure_logging(service_name=service_name)
|
|
318
|
+
|
|
319
|
+
# Configure structlog to intercept standard logging
|
|
320
|
+
structlog.configure(
|
|
321
|
+
processors=[
|
|
322
|
+
structlog.stdlib.filter_by_level,
|
|
323
|
+
structlog.stdlib.add_logger_name,
|
|
324
|
+
structlog.stdlib.add_log_level,
|
|
325
|
+
structlog.stdlib.PositionalArgumentsFormatter(),
|
|
326
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
327
|
+
structlog.processors.StackInfoRenderer(),
|
|
328
|
+
structlog.processors.format_exc_info,
|
|
329
|
+
structlog.processors.UnicodeDecoder(),
|
|
330
|
+
structlog.processors.JSONRenderer(),
|
|
331
|
+
],
|
|
332
|
+
context_class=dict,
|
|
333
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
334
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
335
|
+
cache_logger_on_first_use=True,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
class StructlogHandler(logging.Handler):
|
|
340
|
+
"""Custom handler that routes standard logging to structlog."""
|
|
341
|
+
|
|
342
|
+
def __init__(self):
|
|
343
|
+
super().__init__()
|
|
344
|
+
self.structlog_logger = structlog.get_logger()
|
|
345
|
+
|
|
346
|
+
def emit(self, record):
|
|
347
|
+
# Convert logging record to structlog format
|
|
348
|
+
level_name = record.levelname.lower()
|
|
349
|
+
structlog_method = getattr(self.structlog_logger, level_name, self.structlog_logger.info)
|
|
350
|
+
|
|
351
|
+
# Get excluded fields from settings
|
|
352
|
+
default_excluded = [
|
|
353
|
+
"name",
|
|
354
|
+
"msg",
|
|
355
|
+
"args",
|
|
356
|
+
"levelname",
|
|
357
|
+
"levelno",
|
|
358
|
+
"pathname",
|
|
359
|
+
"filename",
|
|
360
|
+
"module",
|
|
361
|
+
"lineno",
|
|
362
|
+
"funcName",
|
|
363
|
+
"created",
|
|
364
|
+
"msecs",
|
|
365
|
+
"relativeCreated",
|
|
366
|
+
"thread",
|
|
367
|
+
"threadName",
|
|
368
|
+
"processName",
|
|
369
|
+
"process",
|
|
370
|
+
"message",
|
|
371
|
+
"exc_info",
|
|
372
|
+
"exc_text",
|
|
373
|
+
"stack_info",
|
|
374
|
+
"getMessage",
|
|
375
|
+
# Add Django-specific problematic fields
|
|
376
|
+
"request",
|
|
377
|
+
"response",
|
|
378
|
+
"server_time",
|
|
379
|
+
"status_code",
|
|
380
|
+
]
|
|
381
|
+
|
|
382
|
+
excluded_fields = _get_setting("LOGIFY_EXCLUDED_FIELDS", default_excluded)
|
|
383
|
+
if isinstance(excluded_fields, (list, tuple)):
|
|
384
|
+
excluded_fields = set(excluded_fields)
|
|
385
|
+
elif not isinstance(excluded_fields, set):
|
|
386
|
+
excluded_fields = set(default_excluded)
|
|
387
|
+
|
|
388
|
+
# Get sensitive fields from settings
|
|
389
|
+
default_sensitive = [
|
|
390
|
+
"password",
|
|
391
|
+
"passwd",
|
|
392
|
+
"pass",
|
|
393
|
+
"pwd",
|
|
394
|
+
"secret",
|
|
395
|
+
"token",
|
|
396
|
+
"key",
|
|
397
|
+
"api_key",
|
|
398
|
+
"access_token",
|
|
399
|
+
"refresh_token",
|
|
400
|
+
"auth_token",
|
|
401
|
+
"session_key",
|
|
402
|
+
"private_key",
|
|
403
|
+
"credit_card",
|
|
404
|
+
"card_number",
|
|
405
|
+
"cvv",
|
|
406
|
+
"ssn",
|
|
407
|
+
"social_security_number",
|
|
408
|
+
]
|
|
409
|
+
sensitive_fields = _get_setting("LOGIFY_SENSITIVE_FIELDS", default_sensitive)
|
|
410
|
+
if isinstance(sensitive_fields, (list, tuple)):
|
|
411
|
+
sensitive_fields = set(field.lower() for field in sensitive_fields)
|
|
412
|
+
elif not isinstance(sensitive_fields, set):
|
|
413
|
+
sensitive_fields = set(field.lower() for field in default_sensitive)
|
|
414
|
+
|
|
415
|
+
# Extract extra fields, excluding specified fields and masking sensitive ones
|
|
416
|
+
extra = {}
|
|
417
|
+
for key, value in record.__dict__.items():
|
|
418
|
+
if key not in excluded_fields:
|
|
419
|
+
# Skip complex objects that can't be JSON serialized
|
|
420
|
+
if hasattr(value, "__dict__") and not isinstance(value, (str, int, float, bool, list, dict)):
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
# Mask sensitive fields
|
|
424
|
+
if key.lower() in sensitive_fields:
|
|
425
|
+
if value and str(value).strip():
|
|
426
|
+
extra[key] = "***"
|
|
427
|
+
else:
|
|
428
|
+
extra[key] = value
|
|
429
|
+
else:
|
|
430
|
+
extra[key] = value
|
|
431
|
+
|
|
432
|
+
# Log with structlog
|
|
433
|
+
structlog_method(
|
|
434
|
+
record.getMessage(),
|
|
435
|
+
logger=record.name,
|
|
436
|
+
module=record.module,
|
|
437
|
+
funcName=record.funcName,
|
|
438
|
+
lineno=record.lineno,
|
|
439
|
+
**extra,
|
|
440
|
+
)
|
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
|
|
File without changes
|