karrio-server 2025.5rc35__py3-none-any.whl → 2025.5.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/VERSION CHANGED
@@ -1 +1 @@
1
- 2025.5rc35
1
+ 2025.5.1
@@ -35,33 +35,192 @@ if POSTHOG_KEY:
35
35
  posthog.host = POSTHOG_HOST
36
36
 
37
37
 
38
- # Sentry
38
+ # =============================================================================
39
+ # Sentry Configuration
40
+ # =============================================================================
41
+ #
42
+ # Sentry provides error tracking, performance monitoring, and distributed
43
+ # tracing for Karrio. When SENTRY_DSN is configured, the following features
44
+ # are enabled:
45
+ #
46
+ # 1. Error Tracking: All unhandled exceptions are captured with full context
47
+ # 2. Performance Monitoring: API endpoints and gateway operations are traced
48
+ # 3. Distributed Tracing: Carrier API calls are linked to parent transactions
49
+ # 4. Metrics: Custom metrics for carrier operations (rates, shipments, etc.)
50
+ # 5. Logging Integration: ERROR/CRITICAL logs are sent to Sentry
51
+ #
52
+ # Environment Variables:
53
+ # SENTRY_DSN - Sentry Data Source Name (required to enable)
54
+ # SENTRY_ENVIRONMENT - Environment name (default: from ENV or "production")
55
+ # SENTRY_RELEASE - Release version (default: VERSION)
56
+ # SENTRY_TRACES_SAMPLE_RATE - Transaction sampling rate 0.0-1.0 (default: 1.0)
57
+ # SENTRY_PROFILES_SAMPLE_RATE - Profile sampling rate 0.0-1.0 (default: 1.0)
58
+ # SENTRY_SEND_PII - Send personally identifiable information (default: true)
59
+ # SENTRY_DEBUG - Enable Sentry debug mode (default: false)
60
+ #
61
+ # =============================================================================
62
+
39
63
  sentry_sdk.utils.MAX_STRING_LENGTH = 4096
40
64
  SENTRY_DSN = config("SENTRY_DSN", default=None)
65
+ SENTRY_ENVIRONMENT = config("SENTRY_ENVIRONMENT", default=config("ENV", default="production"))
66
+ SENTRY_RELEASE = config("SENTRY_RELEASE", default=config("VERSION", default=None))
67
+ SENTRY_TRACES_SAMPLE_RATE = config("SENTRY_TRACES_SAMPLE_RATE", default=1.0, cast=float)
68
+ SENTRY_PROFILES_SAMPLE_RATE = config("SENTRY_PROFILES_SAMPLE_RATE", default=1.0, cast=float)
69
+ SENTRY_SEND_PII = config("SENTRY_SEND_PII", default=True, cast=bool)
70
+ SENTRY_DEBUG = config("SENTRY_DEBUG", default=False, cast=bool)
71
+
72
+
73
+ def _sentry_before_send(event, hint):
74
+ """Pre-process events before sending to Sentry.
75
+
76
+ This hook allows us to:
77
+ - Scrub sensitive data (API keys, tokens, passwords)
78
+ - Add custom tags
79
+ - Filter out certain events
80
+ """
81
+ # Scrub sensitive data from request bodies
82
+ if "request" in event:
83
+ request_data = event["request"]
84
+
85
+ # Scrub headers
86
+ if "headers" in request_data:
87
+ sensitive_headers = ["authorization", "x-api-key", "cookie", "x-csrf-token"]
88
+ for header in sensitive_headers:
89
+ if header in request_data["headers"]:
90
+ request_data["headers"][header] = "[Filtered]"
91
+
92
+ # Scrub POST data
93
+ if "data" in request_data and isinstance(request_data["data"], dict):
94
+ sensitive_fields = [
95
+ "password", "secret", "token", "api_key", "apikey",
96
+ "access_token", "refresh_token", "client_secret",
97
+ "account_number", "meter_number", "license_key",
98
+ ]
99
+ for field in sensitive_fields:
100
+ for key in list(request_data["data"].keys()):
101
+ if field.lower() in key.lower():
102
+ request_data["data"][key] = "[Filtered]"
103
+
104
+ return event
105
+
106
+
107
+ def _sentry_before_send_transaction(event, hint):
108
+ """Pre-process transactions before sending to Sentry.
109
+
110
+ This hook allows us to:
111
+ - Filter out noisy transactions (health checks, static files)
112
+ - Add custom tags
113
+ """
114
+ transaction_name = event.get("transaction", "")
115
+
116
+ # Filter out health check and monitoring endpoints
117
+ noisy_endpoints = [
118
+ "/health",
119
+ "/ready",
120
+ "/live",
121
+ "/_health",
122
+ "/favicon.ico",
123
+ "/static/",
124
+ "/robots.txt",
125
+ ]
126
+
127
+ for endpoint in noisy_endpoints:
128
+ if transaction_name.startswith(endpoint):
129
+ return None # Drop this transaction
130
+
131
+ return event
132
+
41
133
 
42
134
  if SENTRY_DSN:
43
135
  # Build integrations list
44
- integrations = [DjangoIntegration()]
136
+ integrations = [
137
+ DjangoIntegration(
138
+ transaction_style="url", # Use URL patterns for transaction names
139
+ middleware_spans=True, # Create spans for middleware
140
+ signals_spans=True, # Create spans for Django signals
141
+ ),
142
+ ]
143
+
144
+ # Add PostHog integration if available
45
145
  if POSTHOG_KEY and PostHogIntegration is not None:
46
146
  integrations.append(PostHogIntegration())
47
147
 
148
+ # Try to add Redis integration if Redis is configured
149
+ try:
150
+ from sentry_sdk.integrations.redis import RedisIntegration
151
+ if config("REDIS_URL", default=None) or config("REDIS_HOST", default=None):
152
+ integrations.append(RedisIntegration())
153
+ except ImportError:
154
+ pass
155
+
156
+ # Try to add Huey integration for background tasks
157
+ try:
158
+ from sentry_sdk.integrations.huey import HueyIntegration
159
+ integrations.append(HueyIntegration())
160
+ except ImportError:
161
+ pass
162
+
163
+ # Try to add httpx integration for async HTTP clients
164
+ try:
165
+ from sentry_sdk.integrations.httpx import HttpxIntegration
166
+ integrations.append(HttpxIntegration())
167
+ except Exception:
168
+ pass # httpx may not be installed
169
+
170
+ # Try to add Strawberry GraphQL integration
171
+ try:
172
+ from sentry_sdk.integrations.strawberry import StrawberryIntegration
173
+ integrations.append(StrawberryIntegration(async_execution=False))
174
+ except Exception:
175
+ pass # strawberry integration may not be available
176
+
48
177
  sentry_sdk.init(
49
178
  dsn=SENTRY_DSN,
50
179
  integrations=integrations,
51
- # Set traces_sample_rate to 1.0 to capture 100%
52
- # of transactions for tracing.
53
- traces_sample_rate=1.0,
54
- # Set profile_session_sample_rate to 1.0 to profile 100%
55
- # of profile sessions.
56
- profile_session_sample_rate=1.0,
57
- # Set profile_lifecycle to "trace" to automatically
58
- # run the profiler on when there is an active transaction
180
+
181
+ # Environment and release tracking
182
+ environment=SENTRY_ENVIRONMENT,
183
+ release=SENTRY_RELEASE,
184
+
185
+ # Performance monitoring
186
+ traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
187
+ profile_session_sample_rate=SENTRY_PROFILES_SAMPLE_RATE,
59
188
  profile_lifecycle="trace",
60
- # If you wish to associate users to errors (assuming you are using
61
- # django.contrib.auth) you may enable sending PII data.
62
- send_default_pii=True,
63
- # Enable sending logs to Sentry
189
+
190
+ # Privacy settings
191
+ send_default_pii=SENTRY_SEND_PII,
192
+
193
+ # Logging integration
64
194
  enable_logs=True,
195
+
196
+ # Debug mode
197
+ debug=SENTRY_DEBUG,
198
+
199
+ # Event processing hooks
200
+ before_send=_sentry_before_send,
201
+ before_send_transaction=_sentry_before_send_transaction,
202
+
203
+ # Additional options
204
+ max_breadcrumbs=50, # Keep last 50 breadcrumbs for context
205
+ attach_stacktrace=True, # Attach stack traces to messages
206
+ include_source_context=True, # Include source code in stack traces
207
+ include_local_variables=True, # Include local variables in stack traces
208
+
209
+ # Set custom tags
210
+ _experiments={
211
+ "record_sql_params": True, # Record SQL query parameters
212
+ },
213
+ )
214
+
215
+ # Set default tags that will be applied to all events
216
+ sentry_sdk.set_tag("service", "karrio-api")
217
+ sentry_sdk.set_tag("framework", "django")
218
+
219
+ import logging
220
+ logger = logging.getLogger(__name__)
221
+ logger.info(
222
+ f"Sentry initialized: env={SENTRY_ENVIRONMENT}, "
223
+ f"traces_sample_rate={SENTRY_TRACES_SAMPLE_RATE}"
65
224
  )
66
225
 
67
226
 
@@ -191,3 +350,141 @@ if OTEL_ENABLED and OTEL_EXPORTER_OTLP_ENDPOINT:
191
350
  # Log that OpenTelemetry is enabled
192
351
  logger = logging.getLogger(__name__)
193
352
  logger.info(f"OpenTelemetry enabled: Service={OTEL_SERVICE_NAME}, Endpoint={OTEL_EXPORTER_OTLP_ENDPOINT}")
353
+
354
+
355
+ # =============================================================================
356
+ # Datadog Configuration
357
+ # =============================================================================
358
+ #
359
+ # Datadog provides full-stack observability including APM, infrastructure
360
+ # monitoring, logs, and more. When DD_TRACE_ENABLED is set, the following
361
+ # features are enabled:
362
+ #
363
+ # 1. APM Tracing: Distributed tracing for all requests and background tasks
364
+ # 2. Metrics: Custom metrics via DogStatsD
365
+ # 3. Logs: Automatic trace ID injection into logs
366
+ # 4. Profiling: Continuous profiling (optional)
367
+ #
368
+ # Environment Variables:
369
+ # DD_TRACE_ENABLED - Enable Datadog tracing (default: false)
370
+ # DD_SERVICE - Service name (default: karrio-api)
371
+ # DD_ENV - Environment name (default: from ENV or "production")
372
+ # DD_VERSION - Application version (default: VERSION)
373
+ # DD_AGENT_HOST - Datadog agent host (default: localhost)
374
+ # DD_TRACE_AGENT_PORT - Datadog agent port (default: 8126)
375
+ # DD_DOGSTATSD_PORT - DogStatsD port for metrics (default: 8125)
376
+ # DD_TRACE_SAMPLE_RATE - Sampling rate 0.0-1.0 (default: 1.0)
377
+ # DD_PROFILING_ENABLED - Enable continuous profiling (default: false)
378
+ # DD_LOGS_INJECTION - Inject trace IDs into logs (default: true)
379
+ #
380
+ # =============================================================================
381
+
382
+ DD_TRACE_ENABLED = config("DD_TRACE_ENABLED", default=False, cast=bool)
383
+ DATADOG_ENABLED = config("DATADOG_ENABLED", default=False, cast=bool) # Alias
384
+
385
+ # Use either DD_TRACE_ENABLED or DATADOG_ENABLED
386
+ _datadog_enabled = DD_TRACE_ENABLED or DATADOG_ENABLED
387
+
388
+ if _datadog_enabled:
389
+ # Datadog configuration
390
+ DD_SERVICE = config("DD_SERVICE", default="karrio-api")
391
+ DD_ENV = config("DD_ENV", default=config("ENV", default="production"))
392
+ DD_VERSION = config("DD_VERSION", default=config("VERSION", default="unknown"))
393
+ DD_AGENT_HOST = config("DD_AGENT_HOST", default="localhost")
394
+ DD_TRACE_AGENT_PORT = config("DD_TRACE_AGENT_PORT", default=8126, cast=int)
395
+ DD_DOGSTATSD_PORT = config("DD_DOGSTATSD_PORT", default=8125, cast=int)
396
+ DD_TRACE_SAMPLE_RATE = config("DD_TRACE_SAMPLE_RATE", default=1.0, cast=float)
397
+ DD_PROFILING_ENABLED = config("DD_PROFILING_ENABLED", default=False, cast=bool)
398
+ DD_LOGS_INJECTION = config("DD_LOGS_INJECTION", default=True, cast=bool)
399
+
400
+ try:
401
+ import ddtrace
402
+ from ddtrace import config as dd_config, tracer, patch_all
403
+
404
+ # Configure tracer
405
+ ddtrace.config.service = DD_SERVICE
406
+ ddtrace.config.env = DD_ENV
407
+ ddtrace.config.version = DD_VERSION
408
+
409
+ # Configure Django integration
410
+ dd_config.django["service_name"] = DD_SERVICE
411
+ dd_config.django["cache_service_name"] = f"{DD_SERVICE}-cache"
412
+ dd_config.django["database_service_name"] = f"{DD_SERVICE}-db"
413
+ dd_config.django["trace_query_string"] = True
414
+ dd_config.django["analytics_enabled"] = True
415
+
416
+ # Configure trace sampling
417
+ tracer.configure(
418
+ hostname=DD_AGENT_HOST,
419
+ port=DD_TRACE_AGENT_PORT,
420
+ )
421
+
422
+ # Set global sample rate
423
+ from ddtrace.sampler import DatadogSampler
424
+ tracer.configure(sampler=DatadogSampler(default_sample_rate=DD_TRACE_SAMPLE_RATE))
425
+
426
+ # Enable log injection
427
+ if DD_LOGS_INJECTION:
428
+ ddtrace.patch(logging=True)
429
+
430
+ # Patch all supported libraries
431
+ patch_all(
432
+ django=True,
433
+ redis=True,
434
+ psycopg=True,
435
+ requests=True,
436
+ httpx=True,
437
+ logging=DD_LOGS_INJECTION,
438
+ )
439
+
440
+ # Patch Huey for background task tracing
441
+ try:
442
+ from ddtrace import patch
443
+ patch(huey=True)
444
+ except Exception:
445
+ pass # Huey integration may not be available in all ddtrace versions
446
+
447
+ # Enable profiling if configured
448
+ if DD_PROFILING_ENABLED:
449
+ try:
450
+ import ddtrace.profiling.auto # noqa: F401
451
+ except ImportError:
452
+ pass
453
+
454
+ # Configure DogStatsD for metrics
455
+ try:
456
+ from datadog import initialize, statsd
457
+
458
+ initialize(
459
+ statsd_host=DD_AGENT_HOST,
460
+ statsd_port=DD_DOGSTATSD_PORT,
461
+ )
462
+
463
+ # Set default tags for all metrics
464
+ statsd.constant_tags = [
465
+ f"service:{DD_SERVICE}",
466
+ f"env:{DD_ENV}",
467
+ f"version:{DD_VERSION}",
468
+ ]
469
+
470
+ except ImportError:
471
+ pass # datadog package not installed, metrics won't work
472
+
473
+ import logging
474
+ logger = logging.getLogger(__name__)
475
+ logger.info(
476
+ f"Datadog APM initialized: service={DD_SERVICE}, env={DD_ENV}, "
477
+ f"agent={DD_AGENT_HOST}:{DD_TRACE_AGENT_PORT}"
478
+ )
479
+
480
+ except ImportError:
481
+ import logging
482
+ logger = logging.getLogger(__name__)
483
+ logger.warning(
484
+ "Datadog tracing enabled but ddtrace package not installed. "
485
+ "Install with: pip install ddtrace"
486
+ )
487
+ except Exception as e:
488
+ import logging
489
+ logger = logging.getLogger(__name__)
490
+ logger.warning(f"Failed to initialize Datadog APM: {e}")
@@ -1,4 +1,4 @@
1
- """ Dynamic configuration editable on runtime powered by django-constance."""
1
+ """Dynamic configuration editable on runtime powered by django-constance."""
2
2
 
3
3
  from decouple import config
4
4
  import karrio.references as ref
@@ -22,6 +22,7 @@ GOOGLE_CLOUD_API_KEY = config("GOOGLE_CLOUD_API_KEY", default="")
22
22
  CANADAPOST_ADDRESS_COMPLETE_API_KEY = config(
23
23
  "CANADAPOST_ADDRESS_COMPLETE_API_KEY", default=""
24
24
  )
25
+
25
26
  # data retention env in days
26
27
  ORDER_DATA_RETENTION = config("ORDER_DATA_RETENTION", default=183, cast=int)
27
28
  TRACKER_DATA_RETENTION = config("TRACKER_DATA_RETENTION", default=183, cast=int)
@@ -29,7 +30,9 @@ SHIPMENT_DATA_RETENTION = config("SHIPMENT_DATA_RETENTION", default=183, cast=in
29
30
  API_LOGS_DATA_RETENTION = config("API_LOGS_DATA_RETENTION", default=92, cast=int)
30
31
 
31
32
  # registry config
32
- ENABLE_ALL_PLUGINS_BY_DEFAULT = config("ENABLE_ALL_PLUGINS_BY_DEFAULT", default=True if base.DEBUG else False, cast=bool)
33
+ ENABLE_ALL_PLUGINS_BY_DEFAULT = config(
34
+ "ENABLE_ALL_PLUGINS_BY_DEFAULT", default=True if base.DEBUG else False, cast=bool
35
+ )
33
36
 
34
37
  # Create feature flags config only for modules that exist
35
38
  FEATURE_FLAGS_CONFIG = {
@@ -161,8 +164,21 @@ PLUGIN_REGISTRY = {
161
164
  config(f"{ext.upper()}_ENABLED", default=True, cast=bool),
162
165
  f"{metadata.get('label')} plugin",
163
166
  bool,
164
- ) for ext, metadata in ref.PLUGIN_METADATA.items()
165
- }
167
+ )
168
+ for ext, metadata in ref.PLUGIN_METADATA.items()
169
+ },
170
+ }
171
+
172
+ # Collect plugin system configs from ref.SYSTEM_CONFIGS
173
+ # Format: Dict[str, Tuple[default_value, description, type]]
174
+ PLUGIN_SYSTEM_CONFIG = {
175
+ key: (config(key, default=default_value, cast=value_type), description, value_type)
176
+ for key, (default_value, description, value_type) in ref.SYSTEM_CONFIGS.items()
177
+ }
178
+ PLUGIN_SYSTEM_CONFIG_FIELDSETS = {
179
+ f"{metadata.get('label')} Config": tuple(metadata.get("system_config", {}).keys())
180
+ for _, metadata in ref.PLUGIN_METADATA.items()
181
+ if metadata.get("system_config")
166
182
  }
167
183
 
168
184
 
@@ -218,6 +234,7 @@ CONSTANCE_CONFIG = {
218
234
  ),
219
235
  **{k: v for k, v in FEATURE_FLAGS_CONFIG.items() if v is not None},
220
236
  **PLUGIN_REGISTRY,
237
+ **PLUGIN_SYSTEM_CONFIG,
221
238
  }
222
239
 
223
240
  CONSTANCE_CONFIG_FIELDSETS = {
@@ -241,5 +258,12 @@ CONSTANCE_CONFIG_FIELDSETS = {
241
258
  ),
242
259
  "Feature Flags": tuple(FEATURE_FLAGS_FIELDSET),
243
260
  "Registry Config": ("ENABLE_ALL_PLUGINS_BY_DEFAULT",),
244
- "Registry Plugins": tuple([k for k in PLUGIN_REGISTRY.keys() if not k in ("ENABLE_ALL_PLUGINS_BY_DEFAULT",)]),
261
+ "Registry Plugins": tuple(
262
+ [
263
+ k
264
+ for k in PLUGIN_REGISTRY.keys()
265
+ if not k in ("ENABLE_ALL_PLUGINS_BY_DEFAULT",)
266
+ ]
267
+ ),
268
+ **PLUGIN_SYSTEM_CONFIG_FIELDSETS,
245
269
  }