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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: json-logify
3
- Version: 0.1.0
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
- - <� **Exact JSON Schema**: Consistent log format across all frameworks
87
- - **High Performance**: Built with structlog and orjson for maximum speed
88
- - < **Universal**: Works with Django, FastAPI, Flask, and standalone Python
89
- - =' **Easy Setup**: One-line configuration for most use cases
90
- - =� **Rich Context**: Request IDs, user tracking, and custom payload support
91
- - =
92
- **Modern Python**: Full type hints and async support
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
- # Simple logging
117
- info("User logged in", user_id="12345", action="login")
112
+ # Basic logging with message
113
+ info("User logged in")
118
114
 
119
- # Error logging with exception
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
- raise ValueError("Something went wrong")
125
+ # Some code that might fail
126
+ result = some_function()
122
127
  except Exception as e:
123
- error("Operation failed", error=e, operation="data_processing")
128
+ error("Operation failed", exception=str(e), operation="some_function")
124
129
  ```
125
130
 
126
- Output:
127
- ```json
128
- {
129
- "timestamp": "2025-01-15T10:30:00.123Z",
130
- "message": "User logged in",
131
- "level": "INFO",
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
- ### Django Integration
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
- LOGGING = get_logging_config(
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
- ### FastAPI Integration
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
- context = {}
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": {**context, **event_dict},
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
- formatted_entry["error"] = str(error_field)
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
- kwargs["error_type"] = type(error).__name__
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", service: str = "app"):
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.bind(service=service)
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
- # Bind service name to the default logger
283
+ # Set default logger without service binding
151
284
  global logger
152
- logger = structlog.get_logger("json-logify").bind(service=service_name)
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
- def get_logging_config(service_name: str = "django", level: str = "INFO", json_logs: bool = True):
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(service_name="myapp")
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": {"format": "%(message)s"},
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": ["console"],
90
+ "handlers": ["structlog"],
33
91
  "level": level,
34
92
  },
35
93
  "loggers": {
36
94
  "django": {
37
- "handlers": ["console"],
95
+ "handlers": ["structlog"],
38
96
  "level": level,
39
97
  "propagate": False,
40
98
  },
41
99
  service_name: {
42
- "handlers": ["console"],
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
- set_request_context(
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
- user_agent=request.META.get("HTTP_USER_AGENT", ""),
66
- remote_addr=request.META.get("REMOTE_ADDR", ""),
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
- # Add response info to context
73
- set_request_context(
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
- """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)
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
- 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)
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.0"
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,,