django-bolt 0.1.0__cp310-abi3-win_amd64.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.
Potentially problematic release.
This version of django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +147 -0
- django_bolt/_core.pyd +0 -0
- django_bolt/admin/__init__.py +25 -0
- django_bolt/admin/admin_detection.py +179 -0
- django_bolt/admin/asgi_bridge.py +267 -0
- django_bolt/admin/routes.py +91 -0
- django_bolt/admin/static.py +155 -0
- django_bolt/admin/static_routes.py +111 -0
- django_bolt/api.py +1011 -0
- django_bolt/apps.py +7 -0
- django_bolt/async_collector.py +228 -0
- django_bolt/auth/README.md +464 -0
- django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
- django_bolt/auth/__init__.py +84 -0
- django_bolt/auth/backends.py +236 -0
- django_bolt/auth/guards.py +224 -0
- django_bolt/auth/jwt_utils.py +212 -0
- django_bolt/auth/revocation.py +286 -0
- django_bolt/auth/token.py +335 -0
- django_bolt/binding.py +363 -0
- django_bolt/bootstrap.py +77 -0
- django_bolt/cli.py +133 -0
- django_bolt/compression.py +104 -0
- django_bolt/decorators.py +159 -0
- django_bolt/dependencies.py +128 -0
- django_bolt/error_handlers.py +305 -0
- django_bolt/exceptions.py +294 -0
- django_bolt/health.py +129 -0
- django_bolt/logging/__init__.py +6 -0
- django_bolt/logging/config.py +357 -0
- django_bolt/logging/middleware.py +296 -0
- django_bolt/management/__init__.py +1 -0
- django_bolt/management/commands/__init__.py +0 -0
- django_bolt/management/commands/runbolt.py +427 -0
- django_bolt/middleware/__init__.py +32 -0
- django_bolt/middleware/compiler.py +131 -0
- django_bolt/middleware/middleware.py +247 -0
- django_bolt/openapi/__init__.py +23 -0
- django_bolt/openapi/config.py +196 -0
- django_bolt/openapi/plugins.py +439 -0
- django_bolt/openapi/routes.py +152 -0
- django_bolt/openapi/schema_generator.py +581 -0
- django_bolt/openapi/spec/__init__.py +68 -0
- django_bolt/openapi/spec/base.py +74 -0
- django_bolt/openapi/spec/callback.py +24 -0
- django_bolt/openapi/spec/components.py +72 -0
- django_bolt/openapi/spec/contact.py +21 -0
- django_bolt/openapi/spec/discriminator.py +25 -0
- django_bolt/openapi/spec/encoding.py +67 -0
- django_bolt/openapi/spec/enums.py +41 -0
- django_bolt/openapi/spec/example.py +36 -0
- django_bolt/openapi/spec/external_documentation.py +21 -0
- django_bolt/openapi/spec/header.py +132 -0
- django_bolt/openapi/spec/info.py +50 -0
- django_bolt/openapi/spec/license.py +28 -0
- django_bolt/openapi/spec/link.py +66 -0
- django_bolt/openapi/spec/media_type.py +51 -0
- django_bolt/openapi/spec/oauth_flow.py +36 -0
- django_bolt/openapi/spec/oauth_flows.py +28 -0
- django_bolt/openapi/spec/open_api.py +87 -0
- django_bolt/openapi/spec/operation.py +105 -0
- django_bolt/openapi/spec/parameter.py +147 -0
- django_bolt/openapi/spec/path_item.py +78 -0
- django_bolt/openapi/spec/paths.py +27 -0
- django_bolt/openapi/spec/reference.py +38 -0
- django_bolt/openapi/spec/request_body.py +38 -0
- django_bolt/openapi/spec/response.py +48 -0
- django_bolt/openapi/spec/responses.py +44 -0
- django_bolt/openapi/spec/schema.py +678 -0
- django_bolt/openapi/spec/security_requirement.py +28 -0
- django_bolt/openapi/spec/security_scheme.py +69 -0
- django_bolt/openapi/spec/server.py +34 -0
- django_bolt/openapi/spec/server_variable.py +32 -0
- django_bolt/openapi/spec/tag.py +32 -0
- django_bolt/openapi/spec/xml.py +44 -0
- django_bolt/pagination.py +669 -0
- django_bolt/param_functions.py +49 -0
- django_bolt/params.py +337 -0
- django_bolt/request_parsing.py +128 -0
- django_bolt/responses.py +214 -0
- django_bolt/router.py +48 -0
- django_bolt/serialization.py +193 -0
- django_bolt/status_codes.py +321 -0
- django_bolt/testing/__init__.py +10 -0
- django_bolt/testing/client.py +274 -0
- django_bolt/testing/helpers.py +93 -0
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +1 -0
- django_bolt/tests/admin_tests/conftest.py +6 -0
- django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
- django_bolt/tests/admin_tests/urls.py +9 -0
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +570 -0
- django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
- django_bolt/tests/cbv/test_class_views_features.py +1173 -0
- django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
- django_bolt/tests/conftest.py +165 -0
- django_bolt/tests/test_action_decorator.py +399 -0
- django_bolt/tests/test_auth_secret_key.py +83 -0
- django_bolt/tests/test_decorator_syntax.py +159 -0
- django_bolt/tests/test_error_handling.py +481 -0
- django_bolt/tests/test_file_response.py +192 -0
- django_bolt/tests/test_global_cors.py +172 -0
- django_bolt/tests/test_guards_auth.py +441 -0
- django_bolt/tests/test_guards_integration.py +303 -0
- django_bolt/tests/test_health.py +283 -0
- django_bolt/tests/test_integration_validation.py +400 -0
- django_bolt/tests/test_json_validation.py +536 -0
- django_bolt/tests/test_jwt_auth.py +327 -0
- django_bolt/tests/test_jwt_token.py +458 -0
- django_bolt/tests/test_logging.py +837 -0
- django_bolt/tests/test_logging_merge.py +419 -0
- django_bolt/tests/test_middleware.py +492 -0
- django_bolt/tests/test_middleware_server.py +230 -0
- django_bolt/tests/test_model_viewset.py +323 -0
- django_bolt/tests/test_models.py +24 -0
- django_bolt/tests/test_pagination.py +1258 -0
- django_bolt/tests/test_parameter_validation.py +178 -0
- django_bolt/tests/test_syntax.py +626 -0
- django_bolt/tests/test_testing_utilities.py +163 -0
- django_bolt/tests/test_testing_utilities_simple.py +123 -0
- django_bolt/tests/test_viewset_unified.py +346 -0
- django_bolt/typing.py +273 -0
- django_bolt/views.py +1110 -0
- django_bolt-0.1.0.dist-info/METADATA +629 -0
- django_bolt-0.1.0.dist-info/RECORD +128 -0
- django_bolt-0.1.0.dist-info/WHEEL +4 -0
- django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Logging configuration for Django-Bolt.
|
|
2
|
+
|
|
3
|
+
Integrates with Django's logging configuration and provides structured logging
|
|
4
|
+
for HTTP requests, responses, and exceptions.
|
|
5
|
+
|
|
6
|
+
Adopts Litestar's queue-based logging approach so request logging stays
|
|
7
|
+
non-blocking and fully controlled by application logging config.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
import atexit
|
|
13
|
+
from queue import Queue
|
|
14
|
+
from logging.handlers import QueueHandler, QueueListener
|
|
15
|
+
import logging
|
|
16
|
+
import logging.config
|
|
17
|
+
from abc import ABC, abstractmethod
|
|
18
|
+
from typing import Callable, Optional, Set, List, Dict, Any
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Global flag to prevent multiple logging reconfigurations
|
|
23
|
+
_LOGGING_CONFIGURED = False
|
|
24
|
+
_QUEUE_LISTENER: Optional[QueueListener] = None
|
|
25
|
+
_QUEUE: Optional[Queue] = None
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class LoggingConfig:
|
|
30
|
+
"""Configuration for request/response logging.
|
|
31
|
+
|
|
32
|
+
Integrates with Django's logging system and uses the configured logger.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
# Logger name - defaults to Django's logger
|
|
36
|
+
logger_name: str = "django.server"
|
|
37
|
+
|
|
38
|
+
# Request logging fields
|
|
39
|
+
request_log_fields: Set[str] = field(default_factory=lambda: {
|
|
40
|
+
"method", "path", "status_code"
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
# Response logging fields
|
|
44
|
+
response_log_fields: Set[str] = field(default_factory=lambda: {
|
|
45
|
+
"status_code"
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
# Headers to obfuscate in logs (for security)
|
|
49
|
+
obfuscate_headers: Set[str] = field(default_factory=lambda: {
|
|
50
|
+
"authorization", "cookie", "x-api-key", "x-auth-token"
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
# Cookies to obfuscate in logs
|
|
54
|
+
obfuscate_cookies: Set[str] = field(default_factory=lambda: {
|
|
55
|
+
"sessionid", "csrftoken"
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
# Log request body (be careful with sensitive data)
|
|
59
|
+
log_request_body: bool = False
|
|
60
|
+
|
|
61
|
+
# Log response body (be careful with large responses)
|
|
62
|
+
log_response_body: bool = False
|
|
63
|
+
|
|
64
|
+
# Maximum body size to log (in bytes)
|
|
65
|
+
max_body_log_size: int = 1024
|
|
66
|
+
|
|
67
|
+
# Note: Individual log levels are determined automatically:
|
|
68
|
+
# - Requests: DEBUG
|
|
69
|
+
# - Successful responses (2xx/3xx): INFO
|
|
70
|
+
# - Client errors (4xx): WARNING
|
|
71
|
+
# - Server errors (5xx): ERROR
|
|
72
|
+
#
|
|
73
|
+
# To control which logs appear, configure Django's LOGGING in settings.py:
|
|
74
|
+
# LOGGING = {
|
|
75
|
+
# "loggers": {
|
|
76
|
+
# "django_bolt": {"level": "INFO"}, # Show INFO and above
|
|
77
|
+
# }
|
|
78
|
+
# }
|
|
79
|
+
|
|
80
|
+
# Deprecated: log_level is no longer used (kept for backward compatibility)
|
|
81
|
+
log_level: str = "INFO"
|
|
82
|
+
|
|
83
|
+
# Log level for exceptions (used by log_exception method)
|
|
84
|
+
error_log_level: str = "ERROR"
|
|
85
|
+
|
|
86
|
+
# Custom exception logging handler
|
|
87
|
+
exception_logging_handler: Optional[Callable] = None
|
|
88
|
+
|
|
89
|
+
# Skip logging for specific paths (e.g., health checks)
|
|
90
|
+
skip_paths: Set[str] = field(default_factory=lambda: {
|
|
91
|
+
"/health", "/ready", "/metrics"
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
# Skip logging for specific status codes
|
|
95
|
+
skip_status_codes: Set[int] = field(default_factory=set)
|
|
96
|
+
|
|
97
|
+
# Optional sampling of logs (0.0-1.0). When set, successful responses (2xx/3xx)
|
|
98
|
+
# will only be logged with this probability. Errors (4xx/5xx) are not sampled.
|
|
99
|
+
sample_rate: Optional[float] = None
|
|
100
|
+
|
|
101
|
+
# Only log successful responses slower than this threshold (milliseconds).
|
|
102
|
+
# Errors (4xx/5xx) are not subject to the slow-only threshold.
|
|
103
|
+
min_duration_ms: Optional[int] = None
|
|
104
|
+
|
|
105
|
+
def get_logger(self) -> logging.Logger:
|
|
106
|
+
"""Get the configured logger.
|
|
107
|
+
|
|
108
|
+
Uses Django's logging configuration if available.
|
|
109
|
+
"""
|
|
110
|
+
return logging.getLogger(self.logger_name)
|
|
111
|
+
|
|
112
|
+
def should_log_request(self, path: str, status_code: Optional[int] = None) -> bool:
|
|
113
|
+
"""Check if a request should be logged.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
path: Request path
|
|
117
|
+
status_code: Response status code (optional)
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if request should be logged
|
|
121
|
+
"""
|
|
122
|
+
if path in self.skip_paths:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
if status_code and status_code in self.skip_status_codes:
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class RequestLogFields:
|
|
133
|
+
"""Available fields for request logging."""
|
|
134
|
+
|
|
135
|
+
# HTTP method (GET, POST, etc.)
|
|
136
|
+
method: str = "method"
|
|
137
|
+
|
|
138
|
+
# Request path
|
|
139
|
+
path: str = "path"
|
|
140
|
+
|
|
141
|
+
# Query string
|
|
142
|
+
query: str = "query"
|
|
143
|
+
|
|
144
|
+
# Request headers
|
|
145
|
+
headers: str = "headers"
|
|
146
|
+
|
|
147
|
+
# Request body
|
|
148
|
+
body: str = "body"
|
|
149
|
+
|
|
150
|
+
# Client IP address
|
|
151
|
+
client_ip: str = "client_ip"
|
|
152
|
+
|
|
153
|
+
# User agent
|
|
154
|
+
user_agent: str = "user_agent"
|
|
155
|
+
|
|
156
|
+
# Request ID (if available)
|
|
157
|
+
request_id: str = "request_id"
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class ResponseLogFields:
|
|
162
|
+
"""Available fields for response logging."""
|
|
163
|
+
|
|
164
|
+
# HTTP status code
|
|
165
|
+
status_code: str = "status_code"
|
|
166
|
+
|
|
167
|
+
# Response headers
|
|
168
|
+
headers: str = "headers"
|
|
169
|
+
|
|
170
|
+
# Response body
|
|
171
|
+
body: str = "body"
|
|
172
|
+
|
|
173
|
+
# Response time (in seconds)
|
|
174
|
+
duration: str = "duration"
|
|
175
|
+
|
|
176
|
+
# Response size (in bytes)
|
|
177
|
+
size: str = "size"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_default_logging_config() -> LoggingConfig:
|
|
181
|
+
"""Get default logging configuration.
|
|
182
|
+
|
|
183
|
+
Uses Django's DEBUG setting to determine log level.
|
|
184
|
+
"""
|
|
185
|
+
log_level = "INFO"
|
|
186
|
+
debug = False
|
|
187
|
+
settings_level = None
|
|
188
|
+
settings_sample = None
|
|
189
|
+
settings_slow_ms = None
|
|
190
|
+
try:
|
|
191
|
+
from django.conf import settings
|
|
192
|
+
if settings.configured:
|
|
193
|
+
debug = settings.DEBUG
|
|
194
|
+
# Optional overrides from Django settings
|
|
195
|
+
settings_level = getattr(settings, "DJANGO_BOLT_LOG_LEVEL", None)
|
|
196
|
+
settings_sample = getattr(settings, "DJANGO_BOLT_LOG_SAMPLE", None)
|
|
197
|
+
settings_slow_ms = getattr(settings, "DJANGO_BOLT_LOG_SLOW_MS", None)
|
|
198
|
+
# Default base level by DEBUG
|
|
199
|
+
log_level = "DEBUG" if debug else "WARNING"
|
|
200
|
+
except (ImportError, AttributeError, Exception):
|
|
201
|
+
# Django not available or not configured, use default
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
# Choose log level: Django settings override > default determined by DEBUG
|
|
205
|
+
if settings_level:
|
|
206
|
+
log_level = str(settings_level).upper()
|
|
207
|
+
|
|
208
|
+
sample_rate: Optional[float] = None
|
|
209
|
+
if settings_sample is not None:
|
|
210
|
+
try:
|
|
211
|
+
sr = float(settings_sample)
|
|
212
|
+
if 0.0 <= sr <= 1.0:
|
|
213
|
+
sample_rate = sr
|
|
214
|
+
except Exception:
|
|
215
|
+
sample_rate = None
|
|
216
|
+
|
|
217
|
+
min_duration_ms: Optional[int] = None
|
|
218
|
+
if settings_slow_ms is not None:
|
|
219
|
+
try:
|
|
220
|
+
min_duration_ms = max(0, int(settings_slow_ms))
|
|
221
|
+
except Exception:
|
|
222
|
+
min_duration_ms = None
|
|
223
|
+
else:
|
|
224
|
+
# Default to slow-only logging in production
|
|
225
|
+
if not debug:
|
|
226
|
+
min_duration_ms = 250
|
|
227
|
+
|
|
228
|
+
return LoggingConfig(
|
|
229
|
+
log_level=log_level,
|
|
230
|
+
sample_rate=sample_rate,
|
|
231
|
+
min_duration_ms=min_duration_ms,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _ensure_queue_logging(base_level: str) -> QueueHandler:
|
|
236
|
+
"""Create or reuse a queue-based logging setup.
|
|
237
|
+
|
|
238
|
+
Returns a QueueHandler that enqueues log records. A singleton QueueListener
|
|
239
|
+
forwards records to a console StreamHandler in the background. Inspired by
|
|
240
|
+
Litestar's standard logging implementation.
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
global _QUEUE_LISTENER, _QUEUE # noqa: PLW0603
|
|
244
|
+
|
|
245
|
+
if _QUEUE is None:
|
|
246
|
+
_QUEUE = Queue(-1)
|
|
247
|
+
|
|
248
|
+
queue_handler = QueueHandler(_QUEUE)
|
|
249
|
+
queue_handler.setLevel(logging.DEBUG)
|
|
250
|
+
|
|
251
|
+
if _QUEUE_LISTENER is None:
|
|
252
|
+
console_handler = logging.StreamHandler()
|
|
253
|
+
console_handler.setLevel(base_level)
|
|
254
|
+
console_handler.setFormatter(
|
|
255
|
+
logging.Formatter(
|
|
256
|
+
fmt="%(levelname)s - %(asctime)s - %(name)s - %(message)s",
|
|
257
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
listener = QueueListener(_QUEUE, console_handler)
|
|
262
|
+
listener.start()
|
|
263
|
+
|
|
264
|
+
# Only register atexit once for cleanup
|
|
265
|
+
def _cleanup_listener():
|
|
266
|
+
"""Safely stop the listener, handling already-stopped case."""
|
|
267
|
+
try:
|
|
268
|
+
if _QUEUE_LISTENER is not None and hasattr(_QUEUE_LISTENER, '_thread'):
|
|
269
|
+
if _QUEUE_LISTENER._thread is not None:
|
|
270
|
+
_QUEUE_LISTENER.stop()
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
|
|
274
|
+
atexit.register(_cleanup_listener)
|
|
275
|
+
_QUEUE_LISTENER = listener
|
|
276
|
+
|
|
277
|
+
return queue_handler
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def setup_django_logging(force: bool = False) -> None:
|
|
281
|
+
"""Setup Django logging configuration with console output.
|
|
282
|
+
|
|
283
|
+
Configures Django's logging system to output to console/terminal.
|
|
284
|
+
Based on Litestar's logging configuration pattern.
|
|
285
|
+
|
|
286
|
+
This should be called once during application startup. Subsequent calls
|
|
287
|
+
are no-ops unless force=True.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
force: If True, reconfigure even if already configured
|
|
291
|
+
"""
|
|
292
|
+
global _LOGGING_CONFIGURED, _QUEUE_LISTENER, _QUEUE
|
|
293
|
+
|
|
294
|
+
# Guard against multiple reconfigurations (Litestar pattern)
|
|
295
|
+
if _LOGGING_CONFIGURED and not force:
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
if force and _QUEUE_LISTENER is not None:
|
|
299
|
+
try:
|
|
300
|
+
_QUEUE_LISTENER.stop()
|
|
301
|
+
except Exception:
|
|
302
|
+
pass
|
|
303
|
+
_QUEUE_LISTENER = None
|
|
304
|
+
|
|
305
|
+
try:
|
|
306
|
+
from django.conf import settings
|
|
307
|
+
|
|
308
|
+
# Check if Django is configured
|
|
309
|
+
if not settings.configured:
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
# Check if LOGGING is explicitly configured in Django settings
|
|
313
|
+
# Note: Django's default settings may have LOGGING, but we want to check
|
|
314
|
+
# if the user explicitly set it in their settings.py
|
|
315
|
+
has_explicit_logging = False
|
|
316
|
+
try:
|
|
317
|
+
# Try to import the actual settings module to check if LOGGING is defined
|
|
318
|
+
import importlib
|
|
319
|
+
settings_module = importlib.import_module(settings.SETTINGS_MODULE)
|
|
320
|
+
has_explicit_logging = hasattr(settings_module, 'LOGGING')
|
|
321
|
+
except (AttributeError, ImportError):
|
|
322
|
+
# Fall back to checking settings object
|
|
323
|
+
has_explicit_logging = hasattr(settings, 'LOGGING') and settings.LOGGING
|
|
324
|
+
|
|
325
|
+
if has_explicit_logging:
|
|
326
|
+
# User has explicitly configured logging, respect it
|
|
327
|
+
_LOGGING_CONFIGURED = True
|
|
328
|
+
return
|
|
329
|
+
|
|
330
|
+
# Get appropriate handlers for Python version
|
|
331
|
+
base_level = "DEBUG" if getattr(settings, "DEBUG", False) else "WARNING"
|
|
332
|
+
|
|
333
|
+
queue_handler = _ensure_queue_logging(base_level)
|
|
334
|
+
|
|
335
|
+
root_logger = logging.getLogger()
|
|
336
|
+
root_logger.handlers.clear()
|
|
337
|
+
root_logger.addHandler(queue_handler)
|
|
338
|
+
root_logger.setLevel(base_level)
|
|
339
|
+
|
|
340
|
+
for logger_name in ("django", "django.server", "django_bolt"):
|
|
341
|
+
logger = logging.getLogger(logger_name)
|
|
342
|
+
logger.handlers.clear()
|
|
343
|
+
logger.addHandler(queue_handler)
|
|
344
|
+
# App-level logging config remains source of truth; use base_level if unset
|
|
345
|
+
current_level = logger.level or logging.getLevelName(base_level)
|
|
346
|
+
logger.setLevel(current_level)
|
|
347
|
+
logger.propagate = logger_name == "django"
|
|
348
|
+
|
|
349
|
+
_LOGGING_CONFIGURED = True
|
|
350
|
+
|
|
351
|
+
except (ImportError, AttributeError, Exception) as e:
|
|
352
|
+
# If Django not available or configuration fails, use basic config
|
|
353
|
+
logging.basicConfig(
|
|
354
|
+
level=logging.INFO,
|
|
355
|
+
format='%(levelname)s - %(asctime)s - %(name)s - %(message)s',
|
|
356
|
+
)
|
|
357
|
+
_LOGGING_CONFIGURED = True
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""Logging middleware for Django-Bolt.
|
|
2
|
+
|
|
3
|
+
Provides request/response logging with support for Django's logging configuration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
import random
|
|
8
|
+
import logging
|
|
9
|
+
from typing import Dict, Any, Optional, Callable, Awaitable
|
|
10
|
+
from .config import LoggingConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class LoggingMiddleware:
|
|
14
|
+
"""Middleware for logging HTTP requests and responses.
|
|
15
|
+
|
|
16
|
+
Integrates with Django's logging system and provides structured logging.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, config: Optional[LoggingConfig] = None):
|
|
20
|
+
"""Initialize logging middleware.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
config: Logging configuration (uses defaults if not provided)
|
|
24
|
+
"""
|
|
25
|
+
if config is None:
|
|
26
|
+
from .config import get_default_logging_config
|
|
27
|
+
config = get_default_logging_config()
|
|
28
|
+
|
|
29
|
+
self.config = config
|
|
30
|
+
self.logger = config.get_logger()
|
|
31
|
+
|
|
32
|
+
def obfuscate_headers(self, headers: Dict[str, str]) -> Dict[str, str]:
|
|
33
|
+
"""Obfuscate sensitive headers.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
headers: Request/response headers
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Headers with sensitive values obfuscated
|
|
40
|
+
"""
|
|
41
|
+
obfuscated = {}
|
|
42
|
+
for key, value in headers.items():
|
|
43
|
+
if key.lower() in self.config.obfuscate_headers:
|
|
44
|
+
obfuscated[key] = "***"
|
|
45
|
+
else:
|
|
46
|
+
obfuscated[key] = value
|
|
47
|
+
return obfuscated
|
|
48
|
+
|
|
49
|
+
def obfuscate_cookies(self, cookies: Dict[str, str]) -> Dict[str, str]:
|
|
50
|
+
"""Obfuscate sensitive cookies.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
cookies: Request cookies
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Cookies with sensitive values obfuscated
|
|
57
|
+
"""
|
|
58
|
+
obfuscated = {}
|
|
59
|
+
for key, value in cookies.items():
|
|
60
|
+
if key in self.config.obfuscate_cookies:
|
|
61
|
+
obfuscated[key] = "***"
|
|
62
|
+
else:
|
|
63
|
+
obfuscated[key] = value
|
|
64
|
+
return obfuscated
|
|
65
|
+
|
|
66
|
+
def extract_request_data(self, request: Dict[str, Any]) -> Dict[str, Any]:
|
|
67
|
+
"""Extract request data for logging.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
request: Request dictionary
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Dictionary of request data to log
|
|
74
|
+
"""
|
|
75
|
+
data = {}
|
|
76
|
+
|
|
77
|
+
if "method" in self.config.request_log_fields:
|
|
78
|
+
data["method"] = request.get("method", "")
|
|
79
|
+
|
|
80
|
+
if "path" in self.config.request_log_fields:
|
|
81
|
+
data["path"] = request.get("path", "")
|
|
82
|
+
|
|
83
|
+
if "query" in self.config.request_log_fields:
|
|
84
|
+
query_params = request.get("query_params", {})
|
|
85
|
+
if query_params:
|
|
86
|
+
data["query"] = query_params
|
|
87
|
+
|
|
88
|
+
if "headers" in self.config.request_log_fields:
|
|
89
|
+
headers = request.get("headers", {})
|
|
90
|
+
if headers:
|
|
91
|
+
data["headers"] = self.obfuscate_headers(headers)
|
|
92
|
+
|
|
93
|
+
if "body" in self.config.request_log_fields and self.config.log_request_body:
|
|
94
|
+
body = request.get("body", b"")
|
|
95
|
+
if body and len(body) <= self.config.max_body_log_size:
|
|
96
|
+
try:
|
|
97
|
+
data["body"] = body.decode("utf-8")
|
|
98
|
+
except UnicodeDecodeError:
|
|
99
|
+
data["body"] = f"<binary data, {len(body)} bytes>"
|
|
100
|
+
|
|
101
|
+
if "client_ip" in self.config.request_log_fields:
|
|
102
|
+
# Try to get client IP from various headers
|
|
103
|
+
headers = request.get("headers", {})
|
|
104
|
+
client_ip = (
|
|
105
|
+
headers.get("x-forwarded-for", "").split(",")[0].strip()
|
|
106
|
+
or headers.get("x-real-ip", "")
|
|
107
|
+
or request.get("client", "")
|
|
108
|
+
)
|
|
109
|
+
if client_ip:
|
|
110
|
+
data["client_ip"] = client_ip
|
|
111
|
+
|
|
112
|
+
if "user_agent" in self.config.request_log_fields:
|
|
113
|
+
headers = request.get("headers", {})
|
|
114
|
+
user_agent = headers.get("user-agent", "")
|
|
115
|
+
if user_agent:
|
|
116
|
+
data["user_agent"] = user_agent
|
|
117
|
+
|
|
118
|
+
return data
|
|
119
|
+
|
|
120
|
+
def log_request(self, request: Dict[str, Any]) -> None:
|
|
121
|
+
"""Log an HTTP request.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
request: Request dictionary
|
|
125
|
+
"""
|
|
126
|
+
path = request.get("path", "")
|
|
127
|
+
if not self.config.should_log_request(path):
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Requests are always DEBUG level. Short-circuit if disabled.
|
|
131
|
+
if not self.logger.isEnabledFor(logging.DEBUG):
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
data = self.extract_request_data(request)
|
|
135
|
+
|
|
136
|
+
# Build log message from configured fields only
|
|
137
|
+
message_parts = []
|
|
138
|
+
if "method" in self.config.request_log_fields and "method" in data:
|
|
139
|
+
message_parts.append(data["method"])
|
|
140
|
+
if "path" in self.config.request_log_fields and "path" in data:
|
|
141
|
+
message_parts.append(data["path"])
|
|
142
|
+
|
|
143
|
+
message = " ".join(message_parts) if message_parts else f"Request: {path}"
|
|
144
|
+
|
|
145
|
+
# Log requests at DEBUG level (less important than responses)
|
|
146
|
+
self.logger.log(logging.DEBUG, message, extra=data)
|
|
147
|
+
|
|
148
|
+
def log_response(
|
|
149
|
+
self,
|
|
150
|
+
request: Dict[str, Any],
|
|
151
|
+
status_code: int,
|
|
152
|
+
duration: float,
|
|
153
|
+
response_size: Optional[int] = None,
|
|
154
|
+
) -> None:
|
|
155
|
+
"""Log an HTTP response.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
request: Request dictionary
|
|
159
|
+
status_code: HTTP status code
|
|
160
|
+
duration: Request duration in seconds
|
|
161
|
+
response_size: Response size in bytes (optional)
|
|
162
|
+
"""
|
|
163
|
+
path = request.get("path", "")
|
|
164
|
+
if not self.config.should_log_request(path, status_code):
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Determine log level early and short-circuit if disabled for success path
|
|
168
|
+
if status_code >= 500:
|
|
169
|
+
log_level = logging.ERROR
|
|
170
|
+
elif status_code >= 400:
|
|
171
|
+
log_level = logging.WARNING
|
|
172
|
+
else:
|
|
173
|
+
log_level = logging.INFO
|
|
174
|
+
|
|
175
|
+
# For successful responses, apply gating: level check, sampling, and slow-only
|
|
176
|
+
if status_code < 400:
|
|
177
|
+
if not self.logger.isEnabledFor(log_level):
|
|
178
|
+
return
|
|
179
|
+
# Sampling gate
|
|
180
|
+
if self.config.sample_rate is not None:
|
|
181
|
+
try:
|
|
182
|
+
if random.random() > float(self.config.sample_rate):
|
|
183
|
+
return
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
# Slow-only gate
|
|
187
|
+
if self.config.min_duration_ms is not None:
|
|
188
|
+
duration_ms_check = (duration * 1000.0)
|
|
189
|
+
if duration_ms_check < float(self.config.min_duration_ms):
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
data = {}
|
|
193
|
+
message_parts = []
|
|
194
|
+
|
|
195
|
+
# Only include method if configured
|
|
196
|
+
if "method" in self.config.request_log_fields:
|
|
197
|
+
method = request.get("method", "")
|
|
198
|
+
data["method"] = method
|
|
199
|
+
message_parts.append(method)
|
|
200
|
+
|
|
201
|
+
# Only include path if configured
|
|
202
|
+
if "path" in self.config.request_log_fields:
|
|
203
|
+
data["path"] = path
|
|
204
|
+
message_parts.append(path)
|
|
205
|
+
|
|
206
|
+
# Only include status_code if configured
|
|
207
|
+
if "status_code" in self.config.response_log_fields:
|
|
208
|
+
data["status_code"] = status_code
|
|
209
|
+
message_parts.append(f"{status_code}")
|
|
210
|
+
|
|
211
|
+
# Only include duration if configured
|
|
212
|
+
if "duration" in self.config.response_log_fields:
|
|
213
|
+
duration_ms = round(duration * 1000, 2)
|
|
214
|
+
data["duration_ms"] = duration_ms
|
|
215
|
+
message_parts.append(f"({duration_ms}ms)")
|
|
216
|
+
|
|
217
|
+
if "size" in self.config.response_log_fields and response_size is not None:
|
|
218
|
+
data["response_size"] = response_size
|
|
219
|
+
|
|
220
|
+
# Build log message from configured fields only
|
|
221
|
+
message = " ".join(message_parts) if message_parts else f"Response: {status_code}"
|
|
222
|
+
|
|
223
|
+
self.logger.log(log_level, message, extra=data)
|
|
224
|
+
|
|
225
|
+
def log_exception(
|
|
226
|
+
self,
|
|
227
|
+
request: Dict[str, Any],
|
|
228
|
+
exc: Exception,
|
|
229
|
+
exc_info: bool = True,
|
|
230
|
+
) -> None:
|
|
231
|
+
"""Log an exception that occurred during request handling.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
request: Request dictionary
|
|
235
|
+
exc: Exception instance
|
|
236
|
+
exc_info: Whether to include exception traceback
|
|
237
|
+
"""
|
|
238
|
+
path = request.get("path", "")
|
|
239
|
+
|
|
240
|
+
data = {
|
|
241
|
+
"method": request.get("method", ""),
|
|
242
|
+
"path": path,
|
|
243
|
+
"exception_type": type(exc).__name__,
|
|
244
|
+
"exception": str(exc),
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
message = f"Exception in {data['method']} {path}: {type(exc).__name__}: {str(exc)}"
|
|
248
|
+
|
|
249
|
+
# Use custom exception handler if provided
|
|
250
|
+
if self.config.exception_logging_handler:
|
|
251
|
+
self.config.exception_logging_handler(
|
|
252
|
+
self.logger, request, exc, exc_info
|
|
253
|
+
)
|
|
254
|
+
else:
|
|
255
|
+
# Default exception logging
|
|
256
|
+
log_level = getattr(logging, self.config.error_log_level.upper(), logging.ERROR)
|
|
257
|
+
self.logger.log(
|
|
258
|
+
log_level,
|
|
259
|
+
message,
|
|
260
|
+
extra=data,
|
|
261
|
+
exc_info=exc_info,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# Convenience function to create logging middleware
|
|
266
|
+
def create_logging_middleware(
|
|
267
|
+
logger_name: Optional[str] = None,
|
|
268
|
+
log_level: Optional[str] = None,
|
|
269
|
+
**kwargs
|
|
270
|
+
) -> LoggingMiddleware:
|
|
271
|
+
"""Create a logging middleware with custom configuration.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
logger_name: Logger name (defaults to 'django.server')
|
|
275
|
+
log_level: Log level (defaults to DEBUG in DEBUG mode, INFO otherwise)
|
|
276
|
+
**kwargs: Additional configuration options
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
LoggingMiddleware instance
|
|
280
|
+
"""
|
|
281
|
+
from .config import get_default_logging_config
|
|
282
|
+
|
|
283
|
+
config = get_default_logging_config()
|
|
284
|
+
|
|
285
|
+
if logger_name:
|
|
286
|
+
config.logger_name = logger_name
|
|
287
|
+
|
|
288
|
+
if log_level:
|
|
289
|
+
config.log_level = log_level
|
|
290
|
+
|
|
291
|
+
# Update config with additional kwargs
|
|
292
|
+
for key, value in kwargs.items():
|
|
293
|
+
if hasattr(config, key):
|
|
294
|
+
setattr(config, key, value)
|
|
295
|
+
|
|
296
|
+
return LoggingMiddleware(config)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Django management commands package
|
|
File without changes
|