karrio-server-core 2025.5rc31__py3-none-any.whl → 2026.1.1__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.
- karrio/server/core/authentication.py +38 -20
- karrio/server/core/config.py +31 -0
- karrio/server/core/datatypes.py +30 -4
- karrio/server/core/dataunits.py +26 -7
- karrio/server/core/exceptions.py +287 -17
- karrio/server/core/filters.py +14 -0
- karrio/server/core/gateway.py +284 -11
- karrio/server/core/logging.py +403 -0
- karrio/server/core/middleware.py +104 -2
- karrio/server/core/models/base.py +34 -1
- karrio/server/core/oauth_validators.py +2 -3
- karrio/server/core/permissions.py +1 -2
- karrio/server/core/serializers.py +154 -7
- karrio/server/core/signals.py +22 -28
- karrio/server/core/telemetry.py +573 -0
- karrio/server/core/tests/__init__.py +27 -0
- karrio/server/core/{tests.py → tests/base.py} +6 -7
- karrio/server/core/tests/test_exception_level.py +159 -0
- karrio/server/core/tests/test_resource_token.py +593 -0
- karrio/server/core/utils.py +688 -38
- karrio/server/core/validators.py +144 -222
- karrio/server/core/views/oauth.py +13 -12
- karrio/server/core/views/references.py +2 -2
- karrio/server/iam/apps.py +1 -4
- karrio/server/iam/migrations/0002_setup_carrier_permission_groups.py +103 -0
- karrio/server/iam/migrations/0003_remove_permission_groups.py +91 -0
- karrio/server/iam/permissions.py +7 -134
- karrio/server/iam/serializers.py +9 -3
- karrio/server/iam/signals.py +2 -4
- karrio/server/providers/admin.py +1 -1
- karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
- karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
- karrio/server/providers/migrations/0087_alter_carrier_capabilities.py +38 -0
- karrio/server/providers/migrations/0088_servicelevel_surcharges.py +24 -0
- karrio/server/providers/migrations/0089_servicelevel_cost_max_volume.py +31 -0
- karrio/server/providers/migrations/0090_ratesheet_surcharges_servicelevel_zone_surcharge_ids.py +47 -0
- karrio/server/providers/migrations/0091_migrate_legacy_zones_surcharges.py +154 -0
- karrio/server/providers/models/carrier.py +101 -29
- karrio/server/providers/models/service.py +182 -125
- karrio/server/providers/models/sheet.py +342 -198
- karrio/server/providers/serializers/base.py +263 -2
- karrio/server/providers/signals.py +2 -4
- karrio/server/providers/templates/providers/oauth_callback.html +105 -0
- karrio/server/providers/tests/__init__.py +5 -0
- karrio/server/providers/tests/test_connections.py +895 -0
- karrio/server/providers/views/carriers.py +1 -3
- karrio/server/providers/views/connections.py +322 -2
- karrio/server/serializers/abstract.py +112 -21
- karrio/server/tracing/utils.py +5 -8
- karrio/server/user/models.py +36 -34
- karrio/server/user/serializers.py +1 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/METADATA +2 -2
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/RECORD +55 -38
- karrio/server/providers/tests.py +0 -3
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/WHEEL +0 -0
- {karrio_server_core-2025.5rc31.dist-info → karrio_server_core-2026.1.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django-integrated Loguru logging configuration for Karrio Server.
|
|
3
|
+
|
|
4
|
+
This module provides seamless integration between Django's logging system
|
|
5
|
+
and Loguru, allowing you to use Loguru's powerful features while maintaining
|
|
6
|
+
compatibility with Django's ecosystem.
|
|
7
|
+
|
|
8
|
+
Usage in Django settings:
|
|
9
|
+
# In settings/base.py, after LOGGING configuration
|
|
10
|
+
from karrio.server.core.logging import setup_django_loguru
|
|
11
|
+
setup_django_loguru()
|
|
12
|
+
|
|
13
|
+
Usage in code:
|
|
14
|
+
from karrio.server.core.logging import logger
|
|
15
|
+
|
|
16
|
+
logger.info("User logged in", user_id=user.id)
|
|
17
|
+
logger.error("Payment failed", error=str(e), order_id=order.id)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from loguru import logger as _logger
|
|
24
|
+
from typing import Optional
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# Remove default handler
|
|
28
|
+
_logger.remove()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DjangoLoguruHandler:
|
|
32
|
+
"""
|
|
33
|
+
Custom handler that integrates Loguru with Django's logging system.
|
|
34
|
+
Preserves Django's context and request information.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.logger = _logger
|
|
39
|
+
|
|
40
|
+
def write(self, message):
|
|
41
|
+
"""Write method for Django compatibility."""
|
|
42
|
+
self.logger.opt(depth=6, colors=True).info(message)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_django_log_config():
|
|
46
|
+
"""
|
|
47
|
+
Get Django-specific log configuration from Django settings.
|
|
48
|
+
Falls back to environment variables if Django settings are not available.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
from django.conf import settings
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
"level": getattr(settings, "LOG_LEVEL", "INFO"),
|
|
55
|
+
"log_file": getattr(settings, "LOG_FILE_NAME", None),
|
|
56
|
+
"log_dir": getattr(settings, "LOG_FILE_DIR", None),
|
|
57
|
+
"debug": getattr(settings, "DEBUG", False),
|
|
58
|
+
}
|
|
59
|
+
except Exception:
|
|
60
|
+
# Fallback to environment variables
|
|
61
|
+
return {
|
|
62
|
+
"level": os.getenv("LOG_LEVEL", "INFO"),
|
|
63
|
+
"log_file": os.getenv("LOG_FILE_NAME"),
|
|
64
|
+
"log_dir": os.getenv("LOG_DIR"),
|
|
65
|
+
"debug": os.getenv("DEBUG_MODE", "False").lower() in ("true", "1", "yes"),
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_log_format(debug: bool = False) -> str:
|
|
70
|
+
"""Get the log format string appropriate for Django."""
|
|
71
|
+
if debug:
|
|
72
|
+
return (
|
|
73
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | "
|
|
74
|
+
"<level>{level: <8}</level> | "
|
|
75
|
+
"<cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> | "
|
|
76
|
+
"<level>{message}</level> | "
|
|
77
|
+
"{extra}"
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
return (
|
|
81
|
+
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
|
|
82
|
+
"<level>{level: <8}</level> | "
|
|
83
|
+
"<cyan>{name}</cyan> | "
|
|
84
|
+
"<level>{message}</level>"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _is_sentry_enabled() -> bool:
|
|
89
|
+
"""Check if Sentry is configured and enabled."""
|
|
90
|
+
try:
|
|
91
|
+
from django.conf import settings
|
|
92
|
+
return bool(getattr(settings, "SENTRY_DSN", None))
|
|
93
|
+
except Exception:
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _sentry_sink(message):
|
|
98
|
+
"""Loguru sink that sends logs to Sentry.
|
|
99
|
+
|
|
100
|
+
- ERROR and CRITICAL level logs are sent as Sentry events
|
|
101
|
+
- WARNING level logs are added as Sentry breadcrumbs
|
|
102
|
+
- INFO and DEBUG logs are ignored (too noisy for Sentry)
|
|
103
|
+
"""
|
|
104
|
+
try:
|
|
105
|
+
import sentry_sdk
|
|
106
|
+
|
|
107
|
+
record = message.record
|
|
108
|
+
level = record["level"].name
|
|
109
|
+
log_message = record["message"]
|
|
110
|
+
|
|
111
|
+
# Build extra context from record
|
|
112
|
+
extra = dict(record.get("extra", {}))
|
|
113
|
+
extra["logger"] = record["name"]
|
|
114
|
+
extra["function"] = record["function"]
|
|
115
|
+
extra["line"] = record["line"]
|
|
116
|
+
extra["file"] = record["file"].name if record["file"] else None
|
|
117
|
+
|
|
118
|
+
if level in ("ERROR", "CRITICAL"):
|
|
119
|
+
# Send as Sentry event
|
|
120
|
+
exception = record.get("exception")
|
|
121
|
+
if exception:
|
|
122
|
+
# If there's an exception, capture it
|
|
123
|
+
exc_type, exc_value, exc_tb = exception.value
|
|
124
|
+
if exc_value:
|
|
125
|
+
with sentry_sdk.push_scope() as scope:
|
|
126
|
+
scope.set_context("loguru", extra)
|
|
127
|
+
scope.set_tag("log_level", level)
|
|
128
|
+
sentry_sdk.capture_exception(exc_value)
|
|
129
|
+
else:
|
|
130
|
+
# No exception, send as message
|
|
131
|
+
with sentry_sdk.push_scope() as scope:
|
|
132
|
+
scope.set_context("loguru", extra)
|
|
133
|
+
scope.set_tag("log_level", level)
|
|
134
|
+
sentry_sdk.capture_message(
|
|
135
|
+
log_message,
|
|
136
|
+
level="error" if level == "ERROR" else "fatal"
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
elif level == "WARNING":
|
|
140
|
+
# Add as breadcrumb for context
|
|
141
|
+
sentry_sdk.add_breadcrumb(
|
|
142
|
+
message=log_message,
|
|
143
|
+
category="loguru",
|
|
144
|
+
level="warning",
|
|
145
|
+
data=extra,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
except ImportError:
|
|
149
|
+
# Sentry not installed
|
|
150
|
+
pass
|
|
151
|
+
except Exception:
|
|
152
|
+
# Fail silently - don't let logging errors break the app
|
|
153
|
+
pass
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def setup_django_loguru(
|
|
157
|
+
level: Optional[str] = None,
|
|
158
|
+
log_file: Optional[str] = None,
|
|
159
|
+
intercept_django: bool = True,
|
|
160
|
+
serialize: bool = False,
|
|
161
|
+
enqueue: bool = True,
|
|
162
|
+
):
|
|
163
|
+
"""
|
|
164
|
+
Set up Loguru for Django with optimal configuration.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
level: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
|
168
|
+
log_file: Path to log file (overrides Django settings)
|
|
169
|
+
intercept_django: Whether to intercept Django's logging (recommended)
|
|
170
|
+
serialize: Whether to serialize logs as JSON
|
|
171
|
+
enqueue: Whether to use async logging (recommended for Django)
|
|
172
|
+
|
|
173
|
+
This function should be called in Django settings after LOGGING configuration.
|
|
174
|
+
"""
|
|
175
|
+
# Remove all existing handlers
|
|
176
|
+
_logger.remove()
|
|
177
|
+
|
|
178
|
+
# Get configuration from Django settings or environment
|
|
179
|
+
config = get_django_log_config()
|
|
180
|
+
log_level = level or config["level"]
|
|
181
|
+
debug_mode = config["debug"]
|
|
182
|
+
log_format = get_log_format(debug_mode)
|
|
183
|
+
|
|
184
|
+
# Add console handler with colors
|
|
185
|
+
_logger.add(
|
|
186
|
+
sys.stderr,
|
|
187
|
+
format=log_format,
|
|
188
|
+
level=log_level,
|
|
189
|
+
colorize=True,
|
|
190
|
+
diagnose=debug_mode,
|
|
191
|
+
backtrace=True,
|
|
192
|
+
enqueue=enqueue,
|
|
193
|
+
serialize=serialize,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
# Add file handler if configured
|
|
197
|
+
log_file_path = log_file or config.get("log_file")
|
|
198
|
+
if log_file_path:
|
|
199
|
+
# Ensure directory exists
|
|
200
|
+
log_dir = Path(log_file_path).parent
|
|
201
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
202
|
+
|
|
203
|
+
_logger.add(
|
|
204
|
+
log_file_path,
|
|
205
|
+
format=log_format,
|
|
206
|
+
level=log_level,
|
|
207
|
+
rotation="500 MB",
|
|
208
|
+
retention="10 days",
|
|
209
|
+
compression="zip",
|
|
210
|
+
diagnose=debug_mode,
|
|
211
|
+
backtrace=True,
|
|
212
|
+
enqueue=enqueue,
|
|
213
|
+
serialize=serialize,
|
|
214
|
+
)
|
|
215
|
+
_logger.info(f"Django file logging enabled: {log_file_path}")
|
|
216
|
+
|
|
217
|
+
# Add Sentry handler if Sentry is configured
|
|
218
|
+
if _is_sentry_enabled():
|
|
219
|
+
_logger.add(
|
|
220
|
+
_sentry_sink,
|
|
221
|
+
level="WARNING", # Only WARNING and above go to Sentry
|
|
222
|
+
format="{message}", # Simple format for Sentry
|
|
223
|
+
enqueue=True, # Async to not block
|
|
224
|
+
backtrace=True,
|
|
225
|
+
diagnose=False, # Don't include verbose diagnostics in Sentry
|
|
226
|
+
)
|
|
227
|
+
_logger.info("Sentry logging handler enabled")
|
|
228
|
+
|
|
229
|
+
# Intercept Django's standard logging
|
|
230
|
+
if intercept_django:
|
|
231
|
+
intercept_standard_logging()
|
|
232
|
+
|
|
233
|
+
# Configure third-party library loggers
|
|
234
|
+
configure_third_party_loggers(debug_mode)
|
|
235
|
+
|
|
236
|
+
_logger.info(f"Loguru configured for Django (level: {log_level})")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def intercept_standard_logging():
|
|
240
|
+
"""
|
|
241
|
+
Intercept all standard library logging and route it through Loguru.
|
|
242
|
+
|
|
243
|
+
This ensures that Django and all third-party libraries using standard
|
|
244
|
+
logging benefit from Loguru's features and consistent formatting.
|
|
245
|
+
"""
|
|
246
|
+
import logging
|
|
247
|
+
|
|
248
|
+
class InterceptHandler(logging.Handler):
|
|
249
|
+
"""
|
|
250
|
+
Handler that intercepts standard logging and forwards to Loguru.
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
254
|
+
# Get corresponding Loguru level
|
|
255
|
+
try:
|
|
256
|
+
level = _logger.level(record.levelname).name
|
|
257
|
+
except ValueError:
|
|
258
|
+
level = record.levelno
|
|
259
|
+
|
|
260
|
+
# Find caller from where the logged message originated
|
|
261
|
+
frame, depth = sys._getframe(6), 6
|
|
262
|
+
while frame and frame.f_code.co_filename == logging.__file__:
|
|
263
|
+
frame = frame.f_back
|
|
264
|
+
depth += 1
|
|
265
|
+
|
|
266
|
+
# Add extra context from Django if available
|
|
267
|
+
extra = {}
|
|
268
|
+
if hasattr(record, "request"):
|
|
269
|
+
extra["request_id"] = getattr(record.request, "id", None)
|
|
270
|
+
extra["user"] = getattr(record.request, "user", None)
|
|
271
|
+
|
|
272
|
+
_logger.opt(depth=depth, exception=record.exc_info).bind(**extra).log(
|
|
273
|
+
level, record.getMessage()
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
# Configure root logger to use our interceptor
|
|
277
|
+
logging.root.handlers = [InterceptHandler()]
|
|
278
|
+
logging.root.setLevel(0)
|
|
279
|
+
|
|
280
|
+
# Update all existing loggers
|
|
281
|
+
for name in logging.root.manager.loggerDict.keys():
|
|
282
|
+
logging.getLogger(name).handlers = []
|
|
283
|
+
logging.getLogger(name).propagate = True
|
|
284
|
+
|
|
285
|
+
_logger.info("Standard logging interception enabled")
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def configure_third_party_loggers(debug_mode: bool = False):
|
|
289
|
+
"""
|
|
290
|
+
Configure logging levels for third-party libraries to reduce noise.
|
|
291
|
+
|
|
292
|
+
In production, we suppress verbose warnings from libraries that don't provide
|
|
293
|
+
useful context. In development, we keep them at WARNING level for debugging.
|
|
294
|
+
"""
|
|
295
|
+
import logging
|
|
296
|
+
|
|
297
|
+
# Configure jstruct logger
|
|
298
|
+
# Note: jstruct logs "unknown arguments" warnings which are handled more verbosely
|
|
299
|
+
# by our own code in karrio.core.utils.dict.DICTPARSE.to_object
|
|
300
|
+
jstruct_logger = logging.getLogger("jstruct.utils")
|
|
301
|
+
|
|
302
|
+
if debug_mode:
|
|
303
|
+
# In debug mode, let our enhanced logging handle unknown arguments
|
|
304
|
+
# Suppress jstruct's basic warnings to avoid duplicates
|
|
305
|
+
jstruct_logger.setLevel(logging.ERROR)
|
|
306
|
+
else:
|
|
307
|
+
# In production, completely silence jstruct warnings
|
|
308
|
+
# (they're typically not actionable in production)
|
|
309
|
+
jstruct_logger.setLevel(logging.ERROR)
|
|
310
|
+
|
|
311
|
+
# Configure WeasyPrint/CSS parsing loggers to suppress CSS warnings
|
|
312
|
+
# WeasyPrint uses cssutils/tinycss2 which emit verbose CSS parsing warnings
|
|
313
|
+
# These warnings are typically not actionable and clutter the logs
|
|
314
|
+
css_loggers = [
|
|
315
|
+
"weasyprint",
|
|
316
|
+
"weasyprint.css",
|
|
317
|
+
"weasyprint.css.validation",
|
|
318
|
+
"weasyprint.html",
|
|
319
|
+
"cssutils",
|
|
320
|
+
"cssutils.css",
|
|
321
|
+
"tinycss2",
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
for logger_name in css_loggers:
|
|
325
|
+
css_logger = logging.getLogger(logger_name)
|
|
326
|
+
# Suppress CSS parsing warnings - they're typically not useful
|
|
327
|
+
# Only show ERROR level and above
|
|
328
|
+
css_logger.setLevel(logging.ERROR)
|
|
329
|
+
# Disable propagation to prevent warnings from bubbling up to parent loggers
|
|
330
|
+
css_logger.propagate = False
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def get_request_context_logger(request):
|
|
334
|
+
"""
|
|
335
|
+
Get a logger bound with request context for structured logging.
|
|
336
|
+
|
|
337
|
+
Usage in Django views:
|
|
338
|
+
from karrio.server.core.logging import get_request_context_logger
|
|
339
|
+
|
|
340
|
+
def my_view(request):
|
|
341
|
+
logger = get_request_context_logger(request)
|
|
342
|
+
logger.info("Processing request")
|
|
343
|
+
"""
|
|
344
|
+
return _logger.bind(
|
|
345
|
+
request_id=getattr(request, "id", None),
|
|
346
|
+
user_id=getattr(request.user, "id", None) if hasattr(request, "user") else None,
|
|
347
|
+
path=request.path if hasattr(request, "path") else None,
|
|
348
|
+
method=request.method if hasattr(request, "method") else None,
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
# Create middleware for automatic request logging
|
|
353
|
+
class LoguruRequestLoggingMiddleware:
|
|
354
|
+
"""
|
|
355
|
+
Django middleware that adds request/response logging with Loguru.
|
|
356
|
+
|
|
357
|
+
Add to MIDDLEWARE in Django settings:
|
|
358
|
+
'karrio.server.core.logging.LoguruRequestLoggingMiddleware',
|
|
359
|
+
"""
|
|
360
|
+
|
|
361
|
+
def __init__(self, get_response):
|
|
362
|
+
self.get_response = get_response
|
|
363
|
+
|
|
364
|
+
def __call__(self, request):
|
|
365
|
+
# Bind request context
|
|
366
|
+
request_logger = get_request_context_logger(request)
|
|
367
|
+
|
|
368
|
+
# Log request
|
|
369
|
+
request_logger.info(
|
|
370
|
+
f"Request started: {request.method} {request.path}",
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
# Process request
|
|
374
|
+
response = self.get_response(request)
|
|
375
|
+
|
|
376
|
+
# Log response
|
|
377
|
+
request_logger.info(
|
|
378
|
+
f"Request finished: {request.method} {request.path} - Status: {response.status_code}",
|
|
379
|
+
status_code=response.status_code,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
return response
|
|
383
|
+
|
|
384
|
+
def process_exception(self, request, exception):
|
|
385
|
+
"""Log exceptions with full context."""
|
|
386
|
+
request_logger = get_request_context_logger(request)
|
|
387
|
+
request_logger.exception(
|
|
388
|
+
f"Request exception: {request.method} {request.path}",
|
|
389
|
+
exception_type=type(exception).__name__,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
# Export the configured logger
|
|
394
|
+
logger = _logger
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
__all__ = [
|
|
398
|
+
"logger",
|
|
399
|
+
"setup_django_loguru",
|
|
400
|
+
"intercept_standard_logging",
|
|
401
|
+
"get_request_context_logger",
|
|
402
|
+
"LoguruRequestLoggingMiddleware",
|
|
403
|
+
]
|
karrio/server/core/middleware.py
CHANGED
|
@@ -25,6 +25,16 @@ class UserToken:
|
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class SessionContext:
|
|
28
|
+
"""Middleware that manages request context, tracing, and telemetry.
|
|
29
|
+
|
|
30
|
+
This middleware:
|
|
31
|
+
1. Creates a Tracer instance for each request
|
|
32
|
+
2. Injects telemetry (Sentry) if configured
|
|
33
|
+
3. Stores the request in thread-local storage for access throughout the request lifecycle
|
|
34
|
+
4. Saves tracing records after the response is generated
|
|
35
|
+
5. Sets up user context for telemetry
|
|
36
|
+
"""
|
|
37
|
+
|
|
28
38
|
_threadmap: dict = {}
|
|
29
39
|
|
|
30
40
|
def __init__(self, get_response):
|
|
@@ -32,13 +42,26 @@ class SessionContext:
|
|
|
32
42
|
# One-time configuration and initialization.
|
|
33
43
|
|
|
34
44
|
def __call__(self, request):
|
|
45
|
+
import time
|
|
46
|
+
|
|
35
47
|
# Code to be executed for each request before
|
|
36
48
|
# the view (and later middleware) are called.
|
|
37
|
-
|
|
49
|
+
|
|
50
|
+
# Create tracer with telemetry injection
|
|
51
|
+
tracer = Tracer()
|
|
52
|
+
self._inject_telemetry(tracer, request)
|
|
53
|
+
request.tracer = tracer
|
|
54
|
+
|
|
38
55
|
self._threadmap[threading.get_ident()] = request
|
|
39
56
|
|
|
57
|
+
# Track request timing
|
|
58
|
+
start_time = time.time()
|
|
59
|
+
|
|
40
60
|
response = self.get_response(request)
|
|
41
61
|
|
|
62
|
+
# Record request metrics
|
|
63
|
+
self._record_request_metrics(request, response, start_time)
|
|
64
|
+
|
|
42
65
|
# Code to be executed for each request/response after
|
|
43
66
|
# the view is called.
|
|
44
67
|
try:
|
|
@@ -49,6 +72,85 @@ class SessionContext:
|
|
|
49
72
|
|
|
50
73
|
return response
|
|
51
74
|
|
|
75
|
+
def _inject_telemetry(self, tracer: Tracer, request):
|
|
76
|
+
"""Inject telemetry into tracer if Sentry is configured.
|
|
77
|
+
|
|
78
|
+
This method conditionally imports and sets up SentryTelemetry
|
|
79
|
+
only when SENTRY_DSN is configured, ensuring zero overhead
|
|
80
|
+
when Sentry is not in use.
|
|
81
|
+
"""
|
|
82
|
+
try:
|
|
83
|
+
from karrio.server.core.telemetry import get_telemetry_for_request
|
|
84
|
+
|
|
85
|
+
telemetry = get_telemetry_for_request()
|
|
86
|
+
tracer.set_telemetry(telemetry)
|
|
87
|
+
|
|
88
|
+
# Set user context if authenticated
|
|
89
|
+
user = getattr(request, "user", None)
|
|
90
|
+
if user and getattr(user, "is_authenticated", False):
|
|
91
|
+
tracer.set_user(
|
|
92
|
+
user_id=str(user.id) if hasattr(user, "id") else None,
|
|
93
|
+
email=getattr(user, "email", None),
|
|
94
|
+
username=getattr(user, "username", None),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Set request context tags
|
|
98
|
+
tracer.set_tag("http.method", request.method)
|
|
99
|
+
tracer.set_tag("http.path", request.path)
|
|
100
|
+
|
|
101
|
+
# Set test_mode tag if available
|
|
102
|
+
test_mode = getattr(request, "test_mode", None)
|
|
103
|
+
if test_mode is not None:
|
|
104
|
+
tracer.set_tag("test_mode", str(test_mode).lower())
|
|
105
|
+
|
|
106
|
+
# Set org context for multi-tenant deployments
|
|
107
|
+
org = getattr(request, "org", None)
|
|
108
|
+
if org:
|
|
109
|
+
tracer.set_tag("org_id", str(org.id) if hasattr(org, "id") else str(org))
|
|
110
|
+
|
|
111
|
+
except ImportError:
|
|
112
|
+
# Telemetry module not available, continue with NoOpTelemetry
|
|
113
|
+
pass
|
|
114
|
+
except Exception:
|
|
115
|
+
# Any other error, continue with NoOpTelemetry
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
def _record_request_metrics(self, request, response, start_time):
|
|
119
|
+
"""Record HTTP request metrics to telemetry."""
|
|
120
|
+
import time
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
from karrio.server.core.telemetry import get_telemetry_for_request
|
|
124
|
+
|
|
125
|
+
telemetry = get_telemetry_for_request()
|
|
126
|
+
duration_ms = (time.time() - start_time) * 1000
|
|
127
|
+
|
|
128
|
+
# Common tags for all metrics
|
|
129
|
+
tags = {
|
|
130
|
+
"method": request.method,
|
|
131
|
+
"path": request.path,
|
|
132
|
+
"status_code": str(response.status_code),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
# Add test_mode tag if available
|
|
136
|
+
test_mode = getattr(request, "test_mode", None)
|
|
137
|
+
if test_mode is not None:
|
|
138
|
+
tags["test_mode"] = str(test_mode).lower()
|
|
139
|
+
|
|
140
|
+
# Record request count
|
|
141
|
+
telemetry.record_metric("karrio.http.request", 1, tags=tags, metric_type="counter")
|
|
142
|
+
|
|
143
|
+
# Record response time distribution
|
|
144
|
+
telemetry.record_metric("karrio.http.duration", duration_ms, unit="millisecond", tags=tags, metric_type="distribution")
|
|
145
|
+
|
|
146
|
+
# Record error count for 4xx/5xx responses
|
|
147
|
+
if response.status_code >= 400:
|
|
148
|
+
error_tags = {**tags, "error_class": "client" if response.status_code < 500 else "server"}
|
|
149
|
+
telemetry.record_metric("karrio.http.error", 1, tags=error_tags, metric_type="counter")
|
|
150
|
+
|
|
151
|
+
except Exception:
|
|
152
|
+
pass # Don't let metrics recording break the request
|
|
153
|
+
|
|
52
154
|
def _save_tracing_records(self, request, schema: str = None):
|
|
53
155
|
from karrio.server.tracing.utils import save_tracing_records
|
|
54
156
|
|
|
@@ -63,7 +165,7 @@ class NonHtmlDebugToolbarMiddleware:
|
|
|
63
165
|
"""
|
|
64
166
|
The Django Debug Toolbar usually only works for views that return HTML.
|
|
65
167
|
This middleware wraps any non-HTML response in HTML if the request
|
|
66
|
-
has a 'debug' query parameter (e.g.
|
|
168
|
+
has a 'debug' query parameter (e.g. https://api.karrio.io/foo?debug)
|
|
67
169
|
Special handling for json (pretty printing) and
|
|
68
170
|
binary data (only show data length)
|
|
69
171
|
"""
|
|
@@ -41,13 +41,46 @@ class ControlledAccessModel:
|
|
|
41
41
|
|
|
42
42
|
if hasattr(cls, "test_mode") and test_mode is not None:
|
|
43
43
|
query = query & models.Q(
|
|
44
|
-
models.Q(test_mode=
|
|
44
|
+
models.Q(test_mode=test_mode) | models.Q(test_mode__isnull=True)
|
|
45
45
|
)
|
|
46
46
|
|
|
47
47
|
queryset = getattr(cls, manager, cls.objects)
|
|
48
48
|
|
|
49
|
+
if hasattr(cls, "resolve_context_data"):
|
|
50
|
+
queryset = cls.resolve_context_data(queryset, context)
|
|
51
|
+
|
|
49
52
|
return queryset.filter(query)
|
|
50
53
|
|
|
54
|
+
@classmethod
|
|
55
|
+
def resolve_context_data(cls, queryset, context):
|
|
56
|
+
# 1. Self-optimization (e.g., Carrier resolving its own configs)
|
|
57
|
+
if hasattr(queryset, 'resolve_config_for'):
|
|
58
|
+
queryset = queryset.resolve_config_for(context)
|
|
59
|
+
|
|
60
|
+
# 2. Relation optimization
|
|
61
|
+
relations = getattr(cls, "CONTEXT_RELATIONS", [])
|
|
62
|
+
if relations:
|
|
63
|
+
from django.db.models import Prefetch
|
|
64
|
+
prefetches = []
|
|
65
|
+
|
|
66
|
+
for field_name in relations:
|
|
67
|
+
field = cls._meta.get_field(field_name)
|
|
68
|
+
related_model = field.related_model
|
|
69
|
+
|
|
70
|
+
# Check if related model is capable of context resolution
|
|
71
|
+
if hasattr(related_model.objects, 'resolve_config_for'):
|
|
72
|
+
qs = related_model.objects.resolve_config_for(context)
|
|
73
|
+
prefetches.append(Prefetch(field_name, queryset=qs))
|
|
74
|
+
elif hasattr(related_model, 'access_by'):
|
|
75
|
+
# Recurse into related model's access_by (which calls its resolve_context_data)
|
|
76
|
+
qs = related_model.access_by(context)
|
|
77
|
+
prefetches.append(Prefetch(field_name, queryset=qs))
|
|
78
|
+
|
|
79
|
+
if prefetches:
|
|
80
|
+
queryset = queryset.prefetch_related(*prefetches)
|
|
81
|
+
|
|
82
|
+
return queryset
|
|
83
|
+
|
|
51
84
|
|
|
52
85
|
def register_model(model: T) -> T:
|
|
53
86
|
transform = lambda model, transformer: (
|
|
@@ -154,9 +154,8 @@ class CustomOAuth2Validator(OAuth2Validator):
|
|
|
154
154
|
super().save_authorization_code(client_id, code, request, *args, **kwargs)
|
|
155
155
|
|
|
156
156
|
# Log OAuth events for audit purposes
|
|
157
|
-
import
|
|
158
|
-
logger =
|
|
159
|
-
logger.info(f"Authorization code granted for client {client_id}")
|
|
157
|
+
from karrio.server.core.logging import logger
|
|
158
|
+
logger.info("Authorization code granted", client_id=client_id)
|
|
160
159
|
|
|
161
160
|
def authenticate_user(self, request):
|
|
162
161
|
"""
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
import pydoc
|
|
3
2
|
import typing
|
|
4
3
|
from rest_framework import permissions, exceptions
|
|
4
|
+
from karrio.server.core.logging import logger
|
|
5
5
|
|
|
6
6
|
import karrio.server.conf as conf
|
|
7
7
|
|
|
8
|
-
logger = logging.getLogger(__name__)
|
|
9
8
|
PERMISSION_CHECKS = getattr(
|
|
10
9
|
conf.settings, "PERMISSION_CHECKS", ["karrio.server.core.permissions.check_feature_flags"]
|
|
11
10
|
)
|