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.

Files changed (128) hide show
  1. django_bolt/__init__.py +147 -0
  2. django_bolt/_core.pyd +0 -0
  3. django_bolt/admin/__init__.py +25 -0
  4. django_bolt/admin/admin_detection.py +179 -0
  5. django_bolt/admin/asgi_bridge.py +267 -0
  6. django_bolt/admin/routes.py +91 -0
  7. django_bolt/admin/static.py +155 -0
  8. django_bolt/admin/static_routes.py +111 -0
  9. django_bolt/api.py +1011 -0
  10. django_bolt/apps.py +7 -0
  11. django_bolt/async_collector.py +228 -0
  12. django_bolt/auth/README.md +464 -0
  13. django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
  14. django_bolt/auth/__init__.py +84 -0
  15. django_bolt/auth/backends.py +236 -0
  16. django_bolt/auth/guards.py +224 -0
  17. django_bolt/auth/jwt_utils.py +212 -0
  18. django_bolt/auth/revocation.py +286 -0
  19. django_bolt/auth/token.py +335 -0
  20. django_bolt/binding.py +363 -0
  21. django_bolt/bootstrap.py +77 -0
  22. django_bolt/cli.py +133 -0
  23. django_bolt/compression.py +104 -0
  24. django_bolt/decorators.py +159 -0
  25. django_bolt/dependencies.py +128 -0
  26. django_bolt/error_handlers.py +305 -0
  27. django_bolt/exceptions.py +294 -0
  28. django_bolt/health.py +129 -0
  29. django_bolt/logging/__init__.py +6 -0
  30. django_bolt/logging/config.py +357 -0
  31. django_bolt/logging/middleware.py +296 -0
  32. django_bolt/management/__init__.py +1 -0
  33. django_bolt/management/commands/__init__.py +0 -0
  34. django_bolt/management/commands/runbolt.py +427 -0
  35. django_bolt/middleware/__init__.py +32 -0
  36. django_bolt/middleware/compiler.py +131 -0
  37. django_bolt/middleware/middleware.py +247 -0
  38. django_bolt/openapi/__init__.py +23 -0
  39. django_bolt/openapi/config.py +196 -0
  40. django_bolt/openapi/plugins.py +439 -0
  41. django_bolt/openapi/routes.py +152 -0
  42. django_bolt/openapi/schema_generator.py +581 -0
  43. django_bolt/openapi/spec/__init__.py +68 -0
  44. django_bolt/openapi/spec/base.py +74 -0
  45. django_bolt/openapi/spec/callback.py +24 -0
  46. django_bolt/openapi/spec/components.py +72 -0
  47. django_bolt/openapi/spec/contact.py +21 -0
  48. django_bolt/openapi/spec/discriminator.py +25 -0
  49. django_bolt/openapi/spec/encoding.py +67 -0
  50. django_bolt/openapi/spec/enums.py +41 -0
  51. django_bolt/openapi/spec/example.py +36 -0
  52. django_bolt/openapi/spec/external_documentation.py +21 -0
  53. django_bolt/openapi/spec/header.py +132 -0
  54. django_bolt/openapi/spec/info.py +50 -0
  55. django_bolt/openapi/spec/license.py +28 -0
  56. django_bolt/openapi/spec/link.py +66 -0
  57. django_bolt/openapi/spec/media_type.py +51 -0
  58. django_bolt/openapi/spec/oauth_flow.py +36 -0
  59. django_bolt/openapi/spec/oauth_flows.py +28 -0
  60. django_bolt/openapi/spec/open_api.py +87 -0
  61. django_bolt/openapi/spec/operation.py +105 -0
  62. django_bolt/openapi/spec/parameter.py +147 -0
  63. django_bolt/openapi/spec/path_item.py +78 -0
  64. django_bolt/openapi/spec/paths.py +27 -0
  65. django_bolt/openapi/spec/reference.py +38 -0
  66. django_bolt/openapi/spec/request_body.py +38 -0
  67. django_bolt/openapi/spec/response.py +48 -0
  68. django_bolt/openapi/spec/responses.py +44 -0
  69. django_bolt/openapi/spec/schema.py +678 -0
  70. django_bolt/openapi/spec/security_requirement.py +28 -0
  71. django_bolt/openapi/spec/security_scheme.py +69 -0
  72. django_bolt/openapi/spec/server.py +34 -0
  73. django_bolt/openapi/spec/server_variable.py +32 -0
  74. django_bolt/openapi/spec/tag.py +32 -0
  75. django_bolt/openapi/spec/xml.py +44 -0
  76. django_bolt/pagination.py +669 -0
  77. django_bolt/param_functions.py +49 -0
  78. django_bolt/params.py +337 -0
  79. django_bolt/request_parsing.py +128 -0
  80. django_bolt/responses.py +214 -0
  81. django_bolt/router.py +48 -0
  82. django_bolt/serialization.py +193 -0
  83. django_bolt/status_codes.py +321 -0
  84. django_bolt/testing/__init__.py +10 -0
  85. django_bolt/testing/client.py +274 -0
  86. django_bolt/testing/helpers.py +93 -0
  87. django_bolt/tests/__init__.py +0 -0
  88. django_bolt/tests/admin_tests/__init__.py +1 -0
  89. django_bolt/tests/admin_tests/conftest.py +6 -0
  90. django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
  91. django_bolt/tests/admin_tests/urls.py +9 -0
  92. django_bolt/tests/cbv/__init__.py +0 -0
  93. django_bolt/tests/cbv/test_class_views.py +570 -0
  94. django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
  95. django_bolt/tests/cbv/test_class_views_features.py +1173 -0
  96. django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
  97. django_bolt/tests/conftest.py +165 -0
  98. django_bolt/tests/test_action_decorator.py +399 -0
  99. django_bolt/tests/test_auth_secret_key.py +83 -0
  100. django_bolt/tests/test_decorator_syntax.py +159 -0
  101. django_bolt/tests/test_error_handling.py +481 -0
  102. django_bolt/tests/test_file_response.py +192 -0
  103. django_bolt/tests/test_global_cors.py +172 -0
  104. django_bolt/tests/test_guards_auth.py +441 -0
  105. django_bolt/tests/test_guards_integration.py +303 -0
  106. django_bolt/tests/test_health.py +283 -0
  107. django_bolt/tests/test_integration_validation.py +400 -0
  108. django_bolt/tests/test_json_validation.py +536 -0
  109. django_bolt/tests/test_jwt_auth.py +327 -0
  110. django_bolt/tests/test_jwt_token.py +458 -0
  111. django_bolt/tests/test_logging.py +837 -0
  112. django_bolt/tests/test_logging_merge.py +419 -0
  113. django_bolt/tests/test_middleware.py +492 -0
  114. django_bolt/tests/test_middleware_server.py +230 -0
  115. django_bolt/tests/test_model_viewset.py +323 -0
  116. django_bolt/tests/test_models.py +24 -0
  117. django_bolt/tests/test_pagination.py +1258 -0
  118. django_bolt/tests/test_parameter_validation.py +178 -0
  119. django_bolt/tests/test_syntax.py +626 -0
  120. django_bolt/tests/test_testing_utilities.py +163 -0
  121. django_bolt/tests/test_testing_utilities_simple.py +123 -0
  122. django_bolt/tests/test_viewset_unified.py +346 -0
  123. django_bolt/typing.py +273 -0
  124. django_bolt/views.py +1110 -0
  125. django_bolt-0.1.0.dist-info/METADATA +629 -0
  126. django_bolt-0.1.0.dist-info/RECORD +128 -0
  127. django_bolt-0.1.0.dist-info/WHEEL +4 -0
  128. 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