json-logify 0.1.2__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.2.dist-info → json_logify-0.1.3.dist-info}/METADATA +1 -1
- json_logify-0.1.3.dist-info/RECORD +11 -0
- {json_logify-0.1.2.dist-info → json_logify-0.1.3.dist-info}/WHEEL +1 -1
- logify/core.py +132 -63
- logify/django.py +103 -162
- json_logify-0.1.2.dist-info/RECORD +0 -11
- {json_logify-0.1.2.dist-info → json_logify-0.1.3.dist-info}/licenses/LICENSE +0 -0
- {json_logify-0.1.2.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>
|
|
@@ -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,6 +15,62 @@ 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."""
|
|
@@ -23,15 +79,7 @@ def orjson_serializer(_, __, event_dict):
|
|
|
23
79
|
|
|
24
80
|
def truncate_long_strings(_, __, event_dict):
|
|
25
81
|
"""Truncate long strings in log entries based on max_string_length setting."""
|
|
26
|
-
|
|
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
|
|
82
|
+
max_length = _config["MAX_STRING_LENGTH"]
|
|
35
83
|
|
|
36
84
|
# Truncate long strings - only if max_length is positive
|
|
37
85
|
if max_length > 0:
|
|
@@ -44,14 +92,7 @@ def truncate_long_strings(_, __, event_dict):
|
|
|
44
92
|
|
|
45
93
|
def clean_non_serializable_objects(_, __, event_dict):
|
|
46
94
|
"""Clean non-serializable objects from log entries."""
|
|
47
|
-
|
|
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
|
|
95
|
+
max_length = _config["MAX_STRING_LENGTH"]
|
|
55
96
|
|
|
56
97
|
cleaned = {}
|
|
57
98
|
for key, value in event_dict.items():
|
|
@@ -75,63 +116,78 @@ def clean_non_serializable_objects(_, __, event_dict):
|
|
|
75
116
|
|
|
76
117
|
def mask_sensitive_fields(_, __, event_dict):
|
|
77
118
|
"""Mask sensitive fields in log entries recursively."""
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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)
|
|
104
162
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# Use defaults if django module not available
|
|
108
|
-
pass
|
|
163
|
+
if modified:
|
|
164
|
+
return "&".join(new_parts)
|
|
109
165
|
|
|
110
|
-
|
|
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)
|
|
166
|
+
return text
|
|
117
167
|
|
|
118
168
|
def _mask_recursive(obj):
|
|
119
169
|
"""Recursively mask sensitive fields in dicts and lists."""
|
|
120
170
|
if isinstance(obj, dict):
|
|
121
171
|
masked = {}
|
|
122
172
|
for k, v in obj.items():
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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)):
|
|
131
181
|
masked[k] = _mask_recursive(v)
|
|
182
|
+
elif isinstance(v, str):
|
|
183
|
+
masked[k] = _scrub_string(v)
|
|
184
|
+
else:
|
|
185
|
+
masked[k] = v
|
|
132
186
|
return masked
|
|
133
187
|
elif isinstance(obj, list):
|
|
134
188
|
return [_mask_recursive(item) for item in obj]
|
|
189
|
+
elif isinstance(obj, str):
|
|
190
|
+
return _scrub_string(obj)
|
|
135
191
|
return obj
|
|
136
192
|
|
|
137
193
|
# Apply masking to the entire event_dict (including top-level keys)
|
|
@@ -259,10 +315,23 @@ def get_logger(name: str = "json-logify"):
|
|
|
259
315
|
return bound_logger
|
|
260
316
|
|
|
261
317
|
|
|
262
|
-
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
|
+
):
|
|
263
325
|
"""Configure logging for the application."""
|
|
264
326
|
import logging
|
|
265
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
|
+
|
|
266
335
|
# Set logging level on stdlib logger
|
|
267
336
|
logging.basicConfig(level=getattr(logging, level.upper(), logging.INFO))
|
|
268
337
|
|
logify/django.py
CHANGED
|
@@ -6,21 +6,43 @@ import logging
|
|
|
6
6
|
|
|
7
7
|
import structlog
|
|
8
8
|
|
|
9
|
-
from .core import clear_request_context,
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
|
24
46
|
|
|
25
47
|
|
|
26
48
|
def get_logging_config(
|
|
@@ -29,6 +51,7 @@ def get_logging_config(
|
|
|
29
51
|
json_logs: bool = True,
|
|
30
52
|
excluded_fields: list = None,
|
|
31
53
|
sensitive_fields: list = None,
|
|
54
|
+
replace_sensitive_defaults: bool = False,
|
|
32
55
|
max_string_length: int = None,
|
|
33
56
|
ignore_paths: list = None,
|
|
34
57
|
):
|
|
@@ -41,6 +64,7 @@ def get_logging_config(
|
|
|
41
64
|
json_logs: Whether to enable JSON logging
|
|
42
65
|
excluded_fields: List of fields to exclude from logs
|
|
43
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.
|
|
44
68
|
max_string_length: Maximum length for string truncation
|
|
45
69
|
ignore_paths: List of URL paths to ignore from logging
|
|
46
70
|
|
|
@@ -51,23 +75,31 @@ def get_logging_config(
|
|
|
51
75
|
level="INFO",
|
|
52
76
|
excluded_fields=["custom_field"],
|
|
53
77
|
sensitive_fields=["password", "secret", "authorization"],
|
|
78
|
+
replace_sensitive_defaults=False, # Optional, defaults to False (merge)
|
|
54
79
|
max_string_length=200,
|
|
55
80
|
ignore_paths=["/health/", "/static/"]
|
|
56
81
|
)
|
|
57
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
|
+
|
|
58
90
|
if json_logs:
|
|
59
|
-
|
|
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
|
|
60
96
|
|
|
61
|
-
# Store settings globally for the processors to use
|
|
62
|
-
_set_global_setting("SERVICE_NAME", service_name)
|
|
63
97
|
if excluded_fields is not None:
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if max_string_length is not None:
|
|
68
|
-
_set_global_setting("LOGIFY_MAX_STRING_LENGTH", max_string_length)
|
|
98
|
+
default_excluded = _django_settings["EXCLUDED_FIELDS"]
|
|
99
|
+
_django_settings["EXCLUDED_FIELDS"] = set(default_excluded) | set(excluded_fields)
|
|
100
|
+
|
|
69
101
|
if ignore_paths is not None:
|
|
70
|
-
|
|
102
|
+
_django_settings["IGNORE_PATHS"] = ignore_paths
|
|
71
103
|
|
|
72
104
|
return {
|
|
73
105
|
"version": 1,
|
|
@@ -116,15 +148,12 @@ class LogifyMiddleware:
|
|
|
116
148
|
if self._should_ignore_path(request.path):
|
|
117
149
|
return self.get_response(request)
|
|
118
150
|
|
|
119
|
-
# Import here to avoid circular imports
|
|
120
|
-
from .core import info
|
|
121
|
-
|
|
122
151
|
# Generate request ID and set context
|
|
123
152
|
request_id = generate_request_id()
|
|
124
153
|
request.logify_request_id = request_id
|
|
125
154
|
|
|
126
|
-
# Get service name
|
|
127
|
-
service_name =
|
|
155
|
+
# Get service name
|
|
156
|
+
service_name = _django_settings.get("SERVICE_NAME", "django-app")
|
|
128
157
|
|
|
129
158
|
# Get user info
|
|
130
159
|
user_info = self._get_user_info(request)
|
|
@@ -132,7 +161,7 @@ class LogifyMiddleware:
|
|
|
132
161
|
# Get and scrub headers
|
|
133
162
|
scrubbed_headers = self._scrub_headers(dict(request.headers))
|
|
134
163
|
|
|
135
|
-
# Get request body (no scrubbing - core processors will handle)
|
|
164
|
+
# Get request body (no scrubbing needed here - core processors will handle masking)
|
|
136
165
|
request_body = self._get_request_body(request)
|
|
137
166
|
|
|
138
167
|
# Log request start with all context information ONCE
|
|
@@ -154,7 +183,7 @@ class LogifyMiddleware:
|
|
|
154
183
|
try:
|
|
155
184
|
response = self.get_response(request)
|
|
156
185
|
|
|
157
|
-
# Get response body with content type filtering
|
|
186
|
+
# Get response body with content type filtering
|
|
158
187
|
response_body = self._get_response_body(response)
|
|
159
188
|
|
|
160
189
|
# Log request completion with response info
|
|
@@ -173,13 +202,7 @@ class LogifyMiddleware:
|
|
|
173
202
|
|
|
174
203
|
def _should_ignore_path(self, path):
|
|
175
204
|
"""Check if path should be ignored from logging."""
|
|
176
|
-
|
|
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
|
|
205
|
+
ignore_paths = _django_settings.get("IGNORE_PATHS", [])
|
|
183
206
|
|
|
184
207
|
if isinstance(ignore_paths, (list, tuple)):
|
|
185
208
|
for ignore_path in ignore_paths:
|
|
@@ -195,37 +218,13 @@ class LogifyMiddleware:
|
|
|
195
218
|
|
|
196
219
|
def _scrub_headers(self, headers):
|
|
197
220
|
"""Mask sensitive headers using sensitive_fields settings."""
|
|
198
|
-
# Get sensitive fields
|
|
199
|
-
|
|
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
|
|
221
|
+
# Get sensitive fields from core config
|
|
222
|
+
sensitive_fields = get_config_value("SENSITIVE_FIELDS")
|
|
224
223
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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.
|
|
229
228
|
|
|
230
229
|
scrubbed = {}
|
|
231
230
|
for key, value in headers.items():
|
|
@@ -238,7 +237,7 @@ class LogifyMiddleware:
|
|
|
238
237
|
return scrubbed
|
|
239
238
|
|
|
240
239
|
def _get_request_body(self, request):
|
|
241
|
-
"""Get request body with size limits
|
|
240
|
+
"""Get request body with size limits."""
|
|
242
241
|
if not request.body:
|
|
243
242
|
return None
|
|
244
243
|
|
|
@@ -249,27 +248,32 @@ class LogifyMiddleware:
|
|
|
249
248
|
if request.content_type and "json" in request.content_type:
|
|
250
249
|
import json
|
|
251
250
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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]
|
|
267
271
|
|
|
268
272
|
except Exception:
|
|
269
273
|
return "<non-readable body>"
|
|
270
274
|
|
|
271
275
|
def _get_response_body(self, response):
|
|
272
|
-
"""Get response body with content type filtering
|
|
276
|
+
"""Get response body with content type filtering."""
|
|
273
277
|
if not hasattr(response, "content") or not response.content:
|
|
274
278
|
return None
|
|
275
279
|
|
|
@@ -298,11 +302,14 @@ class LogifyMiddleware:
|
|
|
298
302
|
if "json" in content_type:
|
|
299
303
|
import json
|
|
300
304
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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]
|
|
306
313
|
|
|
307
314
|
except Exception:
|
|
308
315
|
return "<non-readable body>"
|
|
@@ -314,7 +321,7 @@ def setup_django_logging(service_name: str = "django"):
|
|
|
314
321
|
Call this in your Django settings or apps.py
|
|
315
322
|
"""
|
|
316
323
|
# Configure json-logify structlog
|
|
317
|
-
|
|
324
|
+
configure_core_logging(service_name=service_name)
|
|
318
325
|
|
|
319
326
|
# Configure structlog to intercept standard logging
|
|
320
327
|
structlog.configure(
|
|
@@ -349,85 +356,19 @@ class StructlogHandler(logging.Handler):
|
|
|
349
356
|
structlog_method = getattr(self.structlog_logger, level_name, self.structlog_logger.info)
|
|
350
357
|
|
|
351
358
|
# Get excluded fields from settings
|
|
352
|
-
|
|
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)
|
|
359
|
+
excluded_fields = _django_settings.get("EXCLUDED_FIELDS", set())
|
|
414
360
|
|
|
415
|
-
# Extract extra fields, excluding specified fields
|
|
361
|
+
# Extract extra fields, excluding specified fields
|
|
362
|
+
# Note: We rely on core processors to mask sensitive data!
|
|
416
363
|
extra = {}
|
|
417
364
|
for key, value in record.__dict__.items():
|
|
418
365
|
if key not in excluded_fields:
|
|
419
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
|
|
420
368
|
if hasattr(value, "__dict__") and not isinstance(value, (str, int, float, bool, list, dict)):
|
|
421
369
|
continue
|
|
422
370
|
|
|
423
|
-
|
|
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
|
|
371
|
+
extra[key] = value
|
|
431
372
|
|
|
432
373
|
# Log with structlog
|
|
433
374
|
structlog_method(
|
|
@@ -1,11 +0,0 @@
|
|
|
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,,
|
|
File without changes
|
|
File without changes
|