karrio-server-core 2025.5__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.
Files changed (213) hide show
  1. karrio/server/conf.py +54 -0
  2. karrio/server/core/__init__.py +3 -0
  3. karrio/server/core/admin.py +1 -0
  4. karrio/server/core/apps.py +10 -0
  5. karrio/server/core/authentication.py +347 -0
  6. karrio/server/core/config.py +31 -0
  7. karrio/server/core/context_processors.py +12 -0
  8. karrio/server/core/datatypes.py +394 -0
  9. karrio/server/core/dataunits.py +187 -0
  10. karrio/server/core/exceptions.py +404 -0
  11. karrio/server/core/fields.py +12 -0
  12. karrio/server/core/filters.py +837 -0
  13. karrio/server/core/gateway.py +1011 -0
  14. karrio/server/core/logging.py +403 -0
  15. karrio/server/core/management/commands/cli.py +19 -0
  16. karrio/server/core/management/commands/create_oauth_client.py +41 -0
  17. karrio/server/core/management/commands/runserver.py +5 -0
  18. karrio/server/core/middleware.py +197 -0
  19. karrio/server/core/migrations/0001_initial.py +28 -0
  20. karrio/server/core/migrations/0002_apilogindex.py +69 -0
  21. karrio/server/core/migrations/0003_apilogindex_test_mode.py +62 -0
  22. karrio/server/core/migrations/0004_metafield.py +74 -0
  23. karrio/server/core/migrations/0005_alter_metafield_type_alter_metafield_value.py +23 -0
  24. karrio/server/core/migrations/0006_add_api_log_requested_at_index.py +22 -0
  25. karrio/server/core/migrations/__init__.py +0 -0
  26. karrio/server/core/models/__init__.py +48 -0
  27. karrio/server/core/models/base.py +103 -0
  28. karrio/server/core/models/entity.py +24 -0
  29. karrio/server/core/models/metafield.py +144 -0
  30. karrio/server/core/models/third_party.py +21 -0
  31. karrio/server/core/oauth_validators.py +170 -0
  32. karrio/server/core/permissions.py +36 -0
  33. karrio/server/core/renderers.py +11 -0
  34. karrio/server/core/router.py +3 -0
  35. karrio/server/core/serializers.py +1971 -0
  36. karrio/server/core/signals.py +55 -0
  37. karrio/server/core/telemetry.py +573 -0
  38. karrio/server/core/tests.py +99 -0
  39. karrio/server/core/tests_resource_token.py +411 -0
  40. karrio/server/core/urls.py +12 -0
  41. karrio/server/core/utils.py +1025 -0
  42. karrio/server/core/validators.py +264 -0
  43. karrio/server/core/views/__init__.py +2 -0
  44. karrio/server/core/views/api.py +133 -0
  45. karrio/server/core/views/metadata.py +44 -0
  46. karrio/server/core/views/oauth.py +75 -0
  47. karrio/server/core/views/references.py +82 -0
  48. karrio/server/core/views/schema.py +310 -0
  49. karrio/server/filters/__init__.py +2 -0
  50. karrio/server/filters/abstract.py +26 -0
  51. karrio/server/iam/__init__.py +0 -0
  52. karrio/server/iam/admin.py +3 -0
  53. karrio/server/iam/apps.py +21 -0
  54. karrio/server/iam/migrations/0001_initial.py +33 -0
  55. karrio/server/iam/migrations/__init__.py +0 -0
  56. karrio/server/iam/models.py +48 -0
  57. karrio/server/iam/permissions.py +155 -0
  58. karrio/server/iam/serializers.py +54 -0
  59. karrio/server/iam/signals.py +18 -0
  60. karrio/server/iam/tests.py +3 -0
  61. karrio/server/iam/views.py +3 -0
  62. karrio/server/openapi.py +75 -0
  63. karrio/server/providers/__init__.py +1 -0
  64. karrio/server/providers/admin.py +364 -0
  65. karrio/server/providers/apps.py +10 -0
  66. karrio/server/providers/management/commands/migrate_rate_sheets.py +101 -0
  67. karrio/server/providers/migrations/0001_initial.py +140 -0
  68. karrio/server/providers/migrations/0002_carrier_active.py +18 -0
  69. karrio/server/providers/migrations/0003_auto_20201230_0820.py +24 -0
  70. karrio/server/providers/migrations/0004_auto_20210212_0554.py +178 -0
  71. karrio/server/providers/migrations/0005_auto_20210212_0555.py +18 -0
  72. karrio/server/providers/migrations/0006_australiapostsettings.py +29 -0
  73. karrio/server/providers/migrations/0007_auto_20210213_0206.py +21 -0
  74. karrio/server/providers/migrations/0008_auto_20210214_0409.py +30 -0
  75. karrio/server/providers/migrations/0009_auto_20210308_0302.py +18 -0
  76. karrio/server/providers/migrations/0010_auto_20210409_0852.py +32 -0
  77. karrio/server/providers/migrations/0011_auto_20210409_0853.py +21 -0
  78. karrio/server/providers/migrations/0012_alter_carrier_options.py +17 -0
  79. karrio/server/providers/migrations/0013_tntsettings.py +30 -0
  80. karrio/server/providers/migrations/0014_auto_20210612_1608.py +46 -0
  81. karrio/server/providers/migrations/0015_auto_20210615_1601.py +28 -0
  82. karrio/server/providers/migrations/0016_alter_purolatorsettings_user_token.py +18 -0
  83. karrio/server/providers/migrations/0017_auto_20210805_0359.py +1293 -0
  84. karrio/server/providers/migrations/0018_alter_fedexsettings_user_key.py +18 -0
  85. karrio/server/providers/migrations/0019_dhlpolandsettings_servicelevel.py +65 -0
  86. karrio/server/providers/migrations/0020_genericsettings_labeltemplate.py +52 -0
  87. karrio/server/providers/migrations/0021_auto_20211231_2353.py +40 -0
  88. karrio/server/providers/migrations/0022_carrier_metadata.py +18 -0
  89. karrio/server/providers/migrations/0023_auto_20220124_1916.py +27 -0
  90. karrio/server/providers/migrations/0024_alter_genericsettings_custom_carrier_name.py +19 -0
  91. karrio/server/providers/migrations/0025_alter_servicelevel_service_code.py +19 -0
  92. karrio/server/providers/migrations/0026_auto_20220208_0132.py +59 -0
  93. karrio/server/providers/migrations/0027_auto_20220304_1340.py +29 -0
  94. karrio/server/providers/migrations/0028_auto_20220323_1500.py +33 -0
  95. karrio/server/providers/migrations/0029_easypostsettings.py +27 -0
  96. karrio/server/providers/migrations/0030_amazonmwssettings.py +29 -0
  97. karrio/server/providers/migrations/0031_delete_amazonmwssettings.py +18 -0
  98. karrio/server/providers/migrations/0032_alter_carrier_test.py +18 -0
  99. karrio/server/providers/migrations/0033_auto_20220708_1350.py +22 -0
  100. karrio/server/providers/migrations/0034_amazonmwssettings_dpdhlsettings.py +47 -0
  101. karrio/server/providers/migrations/0035_alter_carrier_capabilities.py +43 -0
  102. karrio/server/providers/migrations/0036_upsfreightsettings.py +31 -0
  103. karrio/server/providers/migrations/0037_chronopostsettings.py +29 -0
  104. karrio/server/providers/migrations/0038_alter_genericsettings_label_template.py +19 -0
  105. karrio/server/providers/migrations/0039_auto_20220906_0612.py +23 -0
  106. karrio/server/providers/migrations/0040_dpdhlsettings_services.py +18 -0
  107. karrio/server/providers/migrations/0041_auto_20221105_0705.py +38 -0
  108. karrio/server/providers/migrations/0042_auto_20221215_1642.py +23 -0
  109. karrio/server/providers/migrations/0043_alter_genericsettings_account_number_and_more.py +39 -0
  110. karrio/server/providers/migrations/0044_carrier_carrier_capabilities.py +64 -0
  111. karrio/server/providers/migrations/0045_alter_carrier_active_alter_carrier_carrier_id.py +31 -0
  112. karrio/server/providers/migrations/0046_remove_dpdhlsettings_signature_and_more.py +41 -0
  113. karrio/server/providers/migrations/0047_dpdsettings.py +286 -0
  114. karrio/server/providers/migrations/0048_servicelevel_min_weight_servicelevel_transit_days_and_more.py +64 -0
  115. karrio/server/providers/migrations/0049_boxknightsettings_geodissettings_lapostesettings_and_more.py +156 -0
  116. karrio/server/providers/migrations/0050_carrier_is_system_alter_carrier_metadata_and_more.py +106 -0
  117. karrio/server/providers/migrations/0051_rename_username_upssettings_client_id_and_more.py +31 -0
  118. karrio/server/providers/migrations/0052_alter_upssettings_account_number_and_more.py +20 -0
  119. karrio/server/providers/migrations/0053_locate2usettings.py +281 -0
  120. karrio/server/providers/migrations/0054_zoom2usettings.py +280 -0
  121. karrio/server/providers/migrations/0055_rename_amazonmwssettings_amazonshippingsettings_and_more.py +44 -0
  122. karrio/server/providers/migrations/0056_asendiaussettings_geodissettings_code_client_and_more.py +75 -0
  123. karrio/server/providers/migrations/0057_alter_servicelevel_weight_unit_belgianpostsettings.py +51 -0
  124. karrio/server/providers/migrations/0058_alliedexpresssettings.py +38 -0
  125. karrio/server/providers/migrations/0059_ratesheet.py +81 -0
  126. karrio/server/providers/migrations/0060_belgianpostsettings_rate_sheet_and_more.py +73 -0
  127. karrio/server/providers/migrations/0061_alliedexpresssettings_service_type.py +17 -0
  128. karrio/server/providers/migrations/0062_sendlesettings_account_country_code.py +257 -0
  129. karrio/server/providers/migrations/0063_servicelevel_metadata.py +25 -0
  130. karrio/server/providers/migrations/0064_alliedexpresslocalsettings.py +43 -0
  131. karrio/server/providers/migrations/0065_servicelevel_carrier_service_code_and_more.py +66 -0
  132. karrio/server/providers/migrations/0066_rename_fedexsettings_fedexwssettings_and_more.py +28 -0
  133. karrio/server/providers/migrations/0067_fedexsettings.py +283 -0
  134. karrio/server/providers/migrations/0068_fedexsettings_track_api_key_and_more.py +38 -0
  135. karrio/server/providers/migrations/0069_alter_canadapostsettings_contract_id_and_more.py +23 -0
  136. karrio/server/providers/migrations/0070_tgesettings_alter_carrier_capabilities.py +65 -0
  137. karrio/server/providers/migrations/0071_alter_tgesettings_my_toll_token.py +18 -0
  138. karrio/server/providers/migrations/0072_rename_eshippersettings_eshipperxmlsettings_and_more.py +28 -0
  139. karrio/server/providers/migrations/0073_delete_eshipperxmlsettings.py +41 -0
  140. karrio/server/providers/migrations/0074_eshippersettings.py +38 -0
  141. karrio/server/providers/migrations/0075_haypostsettings.py +40 -0
  142. karrio/server/providers/migrations/0076_rename_customer_registration_id_uspsinternationalsettings_account_number_and_more.py +125 -0
  143. karrio/server/providers/migrations/0077_uspswtinternationalsettings_uspswtsettings_and_more.py +165 -0
  144. karrio/server/providers/migrations/0078_auto_20240813_1552.py +120 -0
  145. karrio/server/providers/migrations/0079_alter_carrier_options_alter_ratesheet_created_by.py +31 -0
  146. karrio/server/providers/migrations/0080_alter_aramexsettings_account_country_code_and_more.py +3025 -0
  147. karrio/server/providers/migrations/0081_remove_alliedexpresssettings_carrier_ptr_and_more.py +338 -0
  148. karrio/server/providers/migrations/0082_add_zone_identifiers.py +50 -0
  149. karrio/server/providers/migrations/0083_add_optimized_rate_sheet_structure.py +33 -0
  150. karrio/server/providers/migrations/0084_alter_servicelevel_currency.py +168 -0
  151. karrio/server/providers/migrations/0085_populate_dhl_parcel_de_oauth_credentials.py +82 -0
  152. karrio/server/providers/migrations/0086_rename_dhl_parcel_de_customer_number_to_billing_number.py +71 -0
  153. karrio/server/providers/migrations/__init__.py +0 -0
  154. karrio/server/providers/models/__init__.py +16 -0
  155. karrio/server/providers/models/carrier.py +387 -0
  156. karrio/server/providers/models/config.py +30 -0
  157. karrio/server/providers/models/service.py +192 -0
  158. karrio/server/providers/models/sheet.py +287 -0
  159. karrio/server/providers/models/template.py +39 -0
  160. karrio/server/providers/models/utils.py +58 -0
  161. karrio/server/providers/router.py +3 -0
  162. karrio/server/providers/serializers/__init__.py +3 -0
  163. karrio/server/providers/serializers/base.py +538 -0
  164. karrio/server/providers/signals.py +25 -0
  165. karrio/server/providers/templates/providers/oauth_callback.html +105 -0
  166. karrio/server/providers/tests/__init__.py +5 -0
  167. karrio/server/providers/tests/test_connections.py +895 -0
  168. karrio/server/providers/urls.py +11 -0
  169. karrio/server/providers/views/__init__.py +0 -0
  170. karrio/server/providers/views/carriers.py +267 -0
  171. karrio/server/providers/views/connections.py +496 -0
  172. karrio/server/samples.py +352 -0
  173. karrio/server/serializers/__init__.py +2 -0
  174. karrio/server/serializers/abstract.py +602 -0
  175. karrio/server/tracing/__init__.py +0 -0
  176. karrio/server/tracing/admin.py +63 -0
  177. karrio/server/tracing/apps.py +8 -0
  178. karrio/server/tracing/migrations/0001_initial.py +41 -0
  179. karrio/server/tracing/migrations/0002_auto_20220710_1307.py +22 -0
  180. karrio/server/tracing/migrations/0003_auto_20221105_0317.py +43 -0
  181. karrio/server/tracing/migrations/0004_tracingrecord_carrier_account_idx.py +24 -0
  182. karrio/server/tracing/migrations/0005_optimise_tracingrecord_request_log_idx.py +25 -0
  183. karrio/server/tracing/migrations/0006_alter_tracingrecord_options_and_more.py +49 -0
  184. karrio/server/tracing/migrations/0007_tracingrecord_tracing_created_at_idx.py +19 -0
  185. karrio/server/tracing/migrations/__init__.py +0 -0
  186. karrio/server/tracing/models.py +82 -0
  187. karrio/server/tracing/tests.py +3 -0
  188. karrio/server/tracing/utils.py +109 -0
  189. karrio/server/user/__init__.py +0 -0
  190. karrio/server/user/admin.py +96 -0
  191. karrio/server/user/apps.py +7 -0
  192. karrio/server/user/forms.py +35 -0
  193. karrio/server/user/migrations/0001_initial.py +41 -0
  194. karrio/server/user/migrations/0002_token.py +29 -0
  195. karrio/server/user/migrations/0003_token_test_mode.py +20 -0
  196. karrio/server/user/migrations/0004_group.py +26 -0
  197. karrio/server/user/migrations/0005_token_label.py +21 -0
  198. karrio/server/user/migrations/0006_workspaceconfig.py +63 -0
  199. karrio/server/user/migrations/0007_user_metadata.py +25 -0
  200. karrio/server/user/migrations/__init__.py +0 -0
  201. karrio/server/user/models.py +218 -0
  202. karrio/server/user/serializers.py +47 -0
  203. karrio/server/user/templates/registration/login.html +108 -0
  204. karrio/server/user/templates/registration/registration_confirm_email.html +10 -0
  205. karrio/server/user/templates/registration/registration_confirm_email.txt +3 -0
  206. karrio/server/user/tests.py +3 -0
  207. karrio/server/user/urls.py +10 -0
  208. karrio/server/user/utils.py +60 -0
  209. karrio/server/user/views.py +9 -0
  210. karrio_server_core-2025.5.dist-info/METADATA +32 -0
  211. karrio_server_core-2025.5.dist-info/RECORD +213 -0
  212. karrio_server_core-2025.5.dist-info/WHEEL +5 -0
  213. karrio_server_core-2025.5.dist-info/top_level.txt +2 -0
@@ -0,0 +1,55 @@
1
+ from django.conf import settings
2
+ from django.dispatch import receiver
3
+ from constance import config
4
+ from constance.signals import config_updated
5
+ from django.core.signals import request_started
6
+
7
+ from karrio.server.core.logging import logger
8
+
9
+
10
+ def register_signals():
11
+ config_updated.connect(constance_updated)
12
+ # Defer config initialization until after Django is fully loaded
13
+ request_started.connect(initialize_settings)
14
+
15
+ logger.info("Signal registration complete", module="karrio.core")
16
+
17
+
18
+ def initialize_settings(sender=None, **kwargs):
19
+ # Only run once
20
+ if not getattr(initialize_settings, "has_run", False):
21
+ try:
22
+ update_settings(config)
23
+ initialize_settings.has_run = True
24
+ except Exception as e:
25
+ logger.error("Failed to initialize settings", error=str(e))
26
+
27
+
28
+ @receiver(config_updated)
29
+ def constance_updated(sender, key, old_value, new_value, **kwargs):
30
+ update_settings(sender)
31
+
32
+
33
+ def update_settings(current):
34
+ CONSTANCE_CONFIG_KEYS = [
35
+ key for key in settings.CONSTANCE_CONFIG.keys() if hasattr(settings, key)
36
+ ]
37
+
38
+ for key in CONSTANCE_CONFIG_KEYS:
39
+ try:
40
+ setattr(settings, key, getattr(current, key))
41
+ except Exception as e:
42
+ logger.error("Failed to update setting", setting_key=key, error=str(e))
43
+
44
+ # Check EMAIL_ENABLED after all settings are updated
45
+ try:
46
+ settings.EMAIL_ENABLED = all(
47
+ cfg is not None and cfg != ""
48
+ for cfg in [
49
+ current.EMAIL_HOST,
50
+ current.EMAIL_HOST_USER,
51
+ ]
52
+ )
53
+ except Exception as e:
54
+ logger.error("Failed to set EMAIL_ENABLED", error=str(e))
55
+ settings.EMAIL_ENABLED = False
@@ -0,0 +1,573 @@
1
+ """
2
+ Telemetry implementations for Karrio Server.
3
+
4
+ This module provides telemetry implementations for multiple APM providers:
5
+ - Sentry: Error tracking and performance monitoring
6
+ - OpenTelemetry (OTEL): Vendor-neutral observability standard
7
+ - Datadog: Full-stack observability platform
8
+
9
+ Provider Priority (first enabled wins):
10
+ 1. SENTRY_DSN -> SentryTelemetry
11
+ 2. OTEL_ENABLED=true -> OpenTelemetryTelemetry
12
+ 3. DD_TRACE_ENABLED=true -> DatadogTelemetry
13
+ 4. None -> NoOpTelemetry (zero overhead)
14
+ """
15
+
16
+ import typing
17
+ import functools
18
+ from functools import lru_cache
19
+ from enum import Enum
20
+
21
+ import karrio.lib as lib
22
+
23
+
24
+ T = typing.TypeVar("T")
25
+
26
+
27
+ def failsafe(callable: typing.Callable[[], T]) -> T:
28
+ """Execute callable and return None on any exception."""
29
+ try:
30
+ return callable()
31
+ except Exception:
32
+ return None
33
+
34
+
35
+ class TelemetryProvider(Enum):
36
+ NONE = "none"
37
+ SENTRY = "sentry"
38
+ OPENTELEMETRY = "opentelemetry"
39
+ DATADOG = "datadog"
40
+
41
+
42
+ # =============================================================================
43
+ # Provider Detection
44
+ # =============================================================================
45
+
46
+
47
+ def is_sentry_enabled() -> bool:
48
+ return failsafe(lambda: bool(__import__("django.conf", fromlist=["settings"]).settings.SENTRY_DSN)) or False
49
+
50
+
51
+ def is_otel_enabled() -> bool:
52
+ return failsafe(lambda: bool(getattr(__import__("django.conf", fromlist=["settings"]).settings, "OTEL_ENABLED", False))) or False
53
+
54
+
55
+ def is_datadog_enabled() -> bool:
56
+ def _check():
57
+ settings = __import__("django.conf", fromlist=["settings"]).settings
58
+ return bool(getattr(settings, "DD_TRACE_ENABLED", False) or getattr(settings, "DATADOG_ENABLED", False))
59
+ return failsafe(_check) or False
60
+
61
+
62
+ def get_active_provider() -> TelemetryProvider:
63
+ if is_sentry_enabled():
64
+ return TelemetryProvider.SENTRY
65
+ if is_otel_enabled():
66
+ return TelemetryProvider.OPENTELEMETRY
67
+ if is_datadog_enabled():
68
+ return TelemetryProvider.DATADOG
69
+ return TelemetryProvider.NONE
70
+
71
+
72
+ # =============================================================================
73
+ # Telemetry Factory
74
+ # =============================================================================
75
+
76
+
77
+ @lru_cache(maxsize=1)
78
+ def get_telemetry_instance() -> lib.Telemetry:
79
+ provider = get_active_provider()
80
+ return {
81
+ TelemetryProvider.SENTRY: lambda: SentryTelemetry(),
82
+ TelemetryProvider.OPENTELEMETRY: lambda: OpenTelemetryTelemetry(),
83
+ TelemetryProvider.DATADOG: lambda: DatadogTelemetry(),
84
+ }.get(provider, lambda: lib.NoOpTelemetry())()
85
+
86
+
87
+ def get_telemetry_for_request() -> lib.Telemetry:
88
+ return get_telemetry_instance()
89
+
90
+
91
+ # =============================================================================
92
+ # Sentry Implementation
93
+ # =============================================================================
94
+
95
+
96
+ class SentrySpanContext(lib.SpanContext):
97
+ def __init__(self, span):
98
+ self._span = span
99
+
100
+ def __enter__(self) -> "SentrySpanContext":
101
+ self._span and self._span.__enter__()
102
+ return self
103
+
104
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
105
+ if not self._span:
106
+ return
107
+ if exc_val:
108
+ self._span.set_status("error")
109
+ self._span.set_data("error.message", str(exc_val))
110
+ self._span.__exit__(exc_type, exc_val, exc_tb)
111
+
112
+ def set_attribute(self, key: str, value: typing.Any) -> None:
113
+ self._span and key and self._span.set_data(key, value)
114
+
115
+ def set_status(self, status: str, message: str = None) -> None:
116
+ if not self._span:
117
+ return
118
+ self._span.set_status(status or "ok")
119
+ message and self._span.set_data("status.message", message)
120
+
121
+ def record_exception(self, exception: Exception) -> None:
122
+ if self._span and exception:
123
+ self._span.set_data("exception.type", type(exception).__name__)
124
+ self._span.set_data("exception.message", str(exception))
125
+
126
+ def add_event(self, name: str, attributes: typing.Dict[str, typing.Any] = None) -> None:
127
+ self._span and name and self._span.set_data(f"event.{name}", attributes or {})
128
+
129
+
130
+ class SentryTelemetry(lib.Telemetry):
131
+ """Sentry-backed telemetry. Requires: sentry-sdk >= 2.0.0"""
132
+
133
+ def start_span(self, name: str, attributes: typing.Dict[str, typing.Any] = None, kind: str = None) -> lib.SpanContext:
134
+ try:
135
+ import sentry_sdk
136
+ current = sentry_sdk.get_current_span()
137
+ span = (
138
+ current.start_child(op=kind or "function", name=name)
139
+ if current else
140
+ sentry_sdk.start_span(op=kind or "function", name=name)
141
+ )
142
+ [span.set_data(k, v) for k, v in (attributes or {}).items()]
143
+ return SentrySpanContext(span)
144
+ except Exception:
145
+ return lib.NoOpSpanContext()
146
+
147
+ def add_breadcrumb(self, message: str, category: str, data: typing.Dict[str, typing.Any] = None, level: str = "info") -> None:
148
+ failsafe(lambda: __import__("sentry_sdk").add_breadcrumb(
149
+ message=message, category=category or "default", data=data or {}, level=level or "info"
150
+ ))
151
+
152
+ def record_metric(self, name: str, value: float, unit: str = None, tags: typing.Dict[str, str] = None, metric_type: str = "counter") -> None:
153
+ def _record():
154
+ from sentry_sdk import metrics
155
+ metric_tags = tags or {}
156
+ # Use metrics.count for counters (newer API), metrics.gauge for gauges, metrics.distribution for histograms
157
+ if metric_type == "counter":
158
+ metrics.count(name, value, tags=metric_tags, unit=unit)
159
+ elif metric_type == "gauge":
160
+ metrics.gauge(name, value, tags=metric_tags, unit=unit)
161
+ elif metric_type == "distribution":
162
+ metrics.distribution(name, value, tags=metric_tags, unit=unit)
163
+ else:
164
+ metrics.count(name, value, tags=metric_tags, unit=unit)
165
+ failsafe(_record)
166
+
167
+ def capture_exception(self, exception: Exception, context: typing.Dict[str, typing.Any] = None, tags: typing.Dict[str, str] = None) -> None:
168
+ def _capture():
169
+ import sentry_sdk
170
+ with sentry_sdk.push_scope() as scope:
171
+ context and scope.set_context("karrio", context)
172
+ [scope.set_tag(k, v) for k, v in (tags or {}).items()]
173
+ sentry_sdk.capture_exception(exception)
174
+ failsafe(_capture)
175
+
176
+ def set_context(self, name: str, data: typing.Dict[str, typing.Any]) -> None:
177
+ failsafe(lambda: __import__("sentry_sdk").set_context(name, data or {}))
178
+
179
+ def set_tag(self, key: str, value: str) -> None:
180
+ failsafe(lambda: __import__("sentry_sdk").set_tag(key, str(value) if value is not None else ""))
181
+
182
+ def set_user(self, user_id: str = None, email: str = None, username: str = None, ip_address: str = None, data: typing.Dict[str, typing.Any] = None) -> None:
183
+ def _set_user():
184
+ import sentry_sdk
185
+ user_data = {
186
+ **({"id": user_id} if user_id else {}),
187
+ **({"email": email} if email else {}),
188
+ **({"username": username} if username else {}),
189
+ **({"ip_address": ip_address} if ip_address else {}),
190
+ **(data or {}),
191
+ }
192
+ user_data and sentry_sdk.set_user(user_data)
193
+ failsafe(_set_user)
194
+
195
+ def start_transaction(self, name: str, op: str = None, attributes: typing.Dict[str, typing.Any] = None) -> lib.SpanContext:
196
+ try:
197
+ import sentry_sdk
198
+ transaction = sentry_sdk.start_transaction(name=name, op=op or "http.server")
199
+ [transaction.set_data(k, v) for k, v in (attributes or {}).items()]
200
+ return SentrySpanContext(transaction)
201
+ except Exception:
202
+ return lib.NoOpSpanContext()
203
+
204
+
205
+ # =============================================================================
206
+ # OpenTelemetry Implementation
207
+ # =============================================================================
208
+
209
+
210
+ class OTELSpanContext(lib.SpanContext):
211
+ def __init__(self, span, token=None):
212
+ self._span = span
213
+ self._token = token
214
+
215
+ def __enter__(self) -> "OTELSpanContext":
216
+ return self
217
+
218
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
219
+ if not self._span:
220
+ return
221
+ try:
222
+ from opentelemetry.trace import StatusCode
223
+ if exc_val:
224
+ self._span.set_status(StatusCode.ERROR, str(exc_val))
225
+ self._span.record_exception(exc_val)
226
+ else:
227
+ self._span.set_status(StatusCode.OK)
228
+ self._span.end()
229
+ except Exception:
230
+ pass
231
+ finally:
232
+ self._token and failsafe(lambda: __import__("opentelemetry").context.detach(self._token))
233
+
234
+ def _safe_value(self, value: typing.Any) -> typing.Any:
235
+ if value is None:
236
+ return ""
237
+ if isinstance(value, (str, int, float, bool)):
238
+ return value
239
+ if isinstance(value, (list, tuple)):
240
+ return [str(v) for v in value]
241
+ return str(value)
242
+
243
+ def set_attribute(self, key: str, value: typing.Any) -> None:
244
+ self._span and key and self._span.set_attribute(key, self._safe_value(value))
245
+
246
+ def set_status(self, status: str, message: str = None) -> None:
247
+ def _set():
248
+ from opentelemetry.trace import StatusCode
249
+ code = {"ok": StatusCode.OK, "error": StatusCode.ERROR}.get((status or "ok").lower(), StatusCode.UNSET)
250
+ self._span.set_status(code, message)
251
+ self._span and failsafe(_set)
252
+
253
+ def record_exception(self, exception: Exception) -> None:
254
+ self._span and exception and self._span.record_exception(exception)
255
+
256
+ def add_event(self, name: str, attributes: typing.Dict[str, typing.Any] = None) -> None:
257
+ if self._span and name:
258
+ safe_attrs = {k: self._safe_value(v) for k, v in (attributes or {}).items()}
259
+ self._span.add_event(name, attributes=safe_attrs or None)
260
+
261
+
262
+ class OpenTelemetryTelemetry(lib.Telemetry):
263
+ """OpenTelemetry-backed telemetry. Requires: opentelemetry-api >= 1.20.0"""
264
+
265
+ def __init__(self):
266
+ self._tracer = None
267
+ self._meter = None
268
+
269
+ def _get_tracer(self):
270
+ if not self._tracer:
271
+ self._tracer = failsafe(lambda: __import__("opentelemetry").trace.get_tracer("karrio"))
272
+ return self._tracer
273
+
274
+ def _get_meter(self):
275
+ if not self._meter:
276
+ self._meter = failsafe(lambda: __import__("opentelemetry").metrics.get_meter("karrio"))
277
+ return self._meter
278
+
279
+ def _safe_value(self, value: typing.Any) -> typing.Any:
280
+ if value is None:
281
+ return ""
282
+ if isinstance(value, (str, int, float, bool)):
283
+ return value
284
+ if isinstance(value, (list, tuple)):
285
+ return [str(v) for v in value]
286
+ return str(value)
287
+
288
+ def start_span(self, name: str, attributes: typing.Dict[str, typing.Any] = None, kind: str = None) -> lib.SpanContext:
289
+ try:
290
+ from opentelemetry import trace, context
291
+ from opentelemetry.trace import SpanKind
292
+
293
+ tracer = self._get_tracer()
294
+ if not tracer:
295
+ return lib.NoOpSpanContext()
296
+
297
+ kind_map = {"client": SpanKind.CLIENT, "server": SpanKind.SERVER, "producer": SpanKind.PRODUCER, "consumer": SpanKind.CONSUMER}
298
+ span_kind = kind_map.get((kind or "").lower(), SpanKind.INTERNAL)
299
+ safe_attrs = {k: self._safe_value(v) for k, v in (attributes or {}).items()} or None
300
+
301
+ span = tracer.start_span(name, kind=span_kind, attributes=safe_attrs)
302
+ ctx = trace.set_span_in_context(span)
303
+ token = context.attach(ctx)
304
+
305
+ return OTELSpanContext(span, token)
306
+ except Exception:
307
+ return lib.NoOpSpanContext()
308
+
309
+ def add_breadcrumb(self, message: str, category: str, data: typing.Dict[str, typing.Any] = None, level: str = "info") -> None:
310
+ def _add():
311
+ from opentelemetry import trace
312
+ span = trace.get_current_span()
313
+ if span and span.is_recording():
314
+ attrs = {"category": category or "default", "level": level or "info", **{k: self._safe_value(v) for k, v in (data or {}).items()}}
315
+ span.add_event(message, attributes=attrs)
316
+ failsafe(_add)
317
+
318
+ def record_metric(self, name: str, value: float, unit: str = None, tags: typing.Dict[str, str] = None, metric_type: str = "counter") -> None:
319
+ def _record():
320
+ meter = self._get_meter()
321
+ if not meter:
322
+ return
323
+ attrs = {k: str(v) for k, v in (tags or {}).items()}
324
+ if metric_type == "counter":
325
+ meter.create_counter(name, unit=unit or "1").add(max(0, int(value)), attributes=attrs)
326
+ elif metric_type == "gauge":
327
+ meter.create_up_down_counter(name, unit=unit or "1").add(int(value), attributes=attrs)
328
+ elif metric_type == "distribution":
329
+ meter.create_histogram(name, unit=unit or "ms").record(value, attributes=attrs)
330
+ failsafe(_record)
331
+
332
+ def capture_exception(self, exception: Exception, context: typing.Dict[str, typing.Any] = None, tags: typing.Dict[str, str] = None) -> None:
333
+ def _capture():
334
+ from opentelemetry import trace
335
+ span = trace.get_current_span()
336
+ if span and span.is_recording():
337
+ span.record_exception(exception)
338
+ [span.set_attribute(f"context.{k}", self._safe_value(v)) for k, v in (context or {}).items()]
339
+ [span.set_attribute(k, v) for k, v in (tags or {}).items()]
340
+ failsafe(_capture)
341
+
342
+ def set_context(self, name: str, data: typing.Dict[str, typing.Any]) -> None:
343
+ def _set():
344
+ from opentelemetry import trace
345
+ span = trace.get_current_span()
346
+ if span and span.is_recording():
347
+ [span.set_attribute(f"{name}.{k}", self._safe_value(v)) for k, v in (data or {}).items()]
348
+ failsafe(_set)
349
+
350
+ def set_tag(self, key: str, value: str) -> None:
351
+ def _set():
352
+ from opentelemetry import trace
353
+ span = trace.get_current_span()
354
+ span and span.is_recording() and span.set_attribute(key, str(value) if value is not None else "")
355
+ failsafe(_set)
356
+
357
+ def set_user(self, user_id: str = None, email: str = None, username: str = None, ip_address: str = None, data: typing.Dict[str, typing.Any] = None) -> None:
358
+ def _set():
359
+ from opentelemetry import trace
360
+ span = trace.get_current_span()
361
+ if span and span.is_recording():
362
+ user_id and span.set_attribute("enduser.id", user_id)
363
+ email and span.set_attribute("enduser.email", email)
364
+ username and span.set_attribute("enduser.username", username)
365
+ ip_address and span.set_attribute("client.address", ip_address)
366
+ [span.set_attribute(f"enduser.{k}", self._safe_value(v)) for k, v in (data or {}).items()]
367
+ failsafe(_set)
368
+
369
+ def start_transaction(self, name: str, op: str = None, attributes: typing.Dict[str, typing.Any] = None) -> lib.SpanContext:
370
+ attrs = {**(attributes or {}), **({"operation": op} if op else {})}
371
+ return self.start_span(name, attributes=attrs or None, kind="server")
372
+
373
+
374
+ # =============================================================================
375
+ # Datadog Implementation
376
+ # =============================================================================
377
+
378
+
379
+ class DatadogSpanContext(lib.SpanContext):
380
+ def __init__(self, span):
381
+ self._span = span
382
+
383
+ def __enter__(self) -> "DatadogSpanContext":
384
+ self._span and failsafe(lambda: self._span.__enter__())
385
+ return self
386
+
387
+ def __exit__(self, exc_type, exc_val, exc_tb) -> None:
388
+ if not self._span:
389
+ return
390
+ exc_val and failsafe(lambda: self._span.set_exc_info(exc_type, exc_val, exc_tb))
391
+ failsafe(lambda: self._span.__exit__(exc_type, exc_val, exc_tb))
392
+
393
+ def _safe_value(self, value: typing.Any) -> typing.Any:
394
+ return value if isinstance(value, (str, int, float, bool)) else str(value) if value is not None else ""
395
+
396
+ def set_attribute(self, key: str, value: typing.Any) -> None:
397
+ self._span and key and failsafe(lambda: self._span.set_tag(key, self._safe_value(value)))
398
+
399
+ def set_status(self, status: str, message: str = None) -> None:
400
+ def _set():
401
+ if (status or "ok").lower() == "error":
402
+ self._span.error = 1
403
+ message and self._span.set_tag("error.message", message)
404
+ else:
405
+ self._span.error = 0
406
+ self._span and failsafe(_set)
407
+
408
+ def record_exception(self, exception: Exception) -> None:
409
+ def _record():
410
+ import sys
411
+ info = sys.exc_info()
412
+ self._span.set_exc_info(info[0], info[1], info[2]) if info[1] is exception else self._span.set_exc_info(type(exception), exception, None)
413
+ self._span and exception and failsafe(_record)
414
+
415
+ def add_event(self, name: str, attributes: typing.Dict[str, typing.Any] = None) -> None:
416
+ self._span and name and failsafe(lambda: self._span.set_tag(f"event.{name}", str(attributes or {})))
417
+
418
+
419
+ class DatadogTelemetry(lib.Telemetry):
420
+ """Datadog-backed telemetry. Requires: ddtrace >= 2.0.0"""
421
+
422
+ def __init__(self):
423
+ self._tracer = None
424
+
425
+ def _get_tracer(self):
426
+ if not self._tracer:
427
+ self._tracer = failsafe(lambda: __import__("ddtrace").tracer)
428
+ return self._tracer
429
+
430
+ def _safe_value(self, value: typing.Any) -> typing.Any:
431
+ return value if isinstance(value, (str, int, float, bool)) else str(value) if value is not None else ""
432
+
433
+ def start_span(self, name: str, attributes: typing.Dict[str, typing.Any] = None, kind: str = None) -> lib.SpanContext:
434
+ try:
435
+ tracer = self._get_tracer()
436
+ if not tracer:
437
+ return lib.NoOpSpanContext()
438
+
439
+ kind_map = {"client": "http", "server": "web", "consumer": "worker", "producer": "worker", "internal": "custom"}
440
+ span = tracer.trace(name, service="karrio", span_type=kind_map.get((kind or "").lower()))
441
+ [span.set_tag(k, self._safe_value(v)) for k, v in (attributes or {}).items()]
442
+
443
+ return DatadogSpanContext(span)
444
+ except Exception:
445
+ return lib.NoOpSpanContext()
446
+
447
+ def add_breadcrumb(self, message: str, category: str, data: typing.Dict[str, typing.Any] = None, level: str = "info") -> None:
448
+ def _add():
449
+ tracer = self._get_tracer()
450
+ span = tracer and tracer.current_span()
451
+ if span:
452
+ cat = category or "default"
453
+ span.set_tag(f"breadcrumb.{cat}", message)
454
+ [span.set_tag(f"breadcrumb.{cat}.{k}", str(v)) for k, v in (data or {}).items()]
455
+ failsafe(_add)
456
+
457
+ def record_metric(self, name: str, value: float, unit: str = None, tags: typing.Dict[str, str] = None, metric_type: str = "counter") -> None:
458
+ def _record():
459
+ try:
460
+ from datadog import statsd
461
+ dd_tags = [f"{k}:{v}" for k, v in (tags or {}).items()]
462
+ {"counter": statsd.increment, "gauge": statsd.gauge, "distribution": statsd.distribution}.get(
463
+ metric_type, statsd.increment
464
+ )(name, value, tags=dd_tags)
465
+ except ImportError:
466
+ tracer = self._get_tracer()
467
+ span = tracer and tracer.current_span()
468
+ span and span.set_metric(name, value)
469
+ failsafe(_record)
470
+
471
+ def capture_exception(self, exception: Exception, context: typing.Dict[str, typing.Any] = None, tags: typing.Dict[str, str] = None) -> None:
472
+ def _capture():
473
+ import sys
474
+ tracer = self._get_tracer()
475
+ span = tracer and tracer.current_span()
476
+ if span:
477
+ info = sys.exc_info()
478
+ span.set_exc_info(info[0], info[1], info[2]) if info[1] is exception else span.set_exc_info(type(exception), exception, None)
479
+ [span.set_tag(f"context.{k}", str(v)) for k, v in (context or {}).items()]
480
+ [span.set_tag(k, v) for k, v in (tags or {}).items()]
481
+ failsafe(_capture)
482
+
483
+ def set_context(self, name: str, data: typing.Dict[str, typing.Any]) -> None:
484
+ def _set():
485
+ tracer = self._get_tracer()
486
+ span = tracer and tracer.current_span()
487
+ span and [span.set_tag(f"{name}.{k}", str(v)) for k, v in (data or {}).items()]
488
+ failsafe(_set)
489
+
490
+ def set_tag(self, key: str, value: str) -> None:
491
+ def _set():
492
+ tracer = self._get_tracer()
493
+ span = tracer and tracer.current_span()
494
+ span and span.set_tag(key, str(value) if value is not None else "")
495
+ failsafe(_set)
496
+
497
+ def set_user(self, user_id: str = None, email: str = None, username: str = None, ip_address: str = None, data: typing.Dict[str, typing.Any] = None) -> None:
498
+ def _set():
499
+ try:
500
+ from ddtrace import tracer
501
+ from ddtrace.contrib.trace_utils import set_user
502
+ if any([user_id, email, username]):
503
+ set_user(tracer, user_id=user_id, email=email, name=username)
504
+ return
505
+ except (ImportError, AttributeError):
506
+ pass
507
+ tracer = self._get_tracer()
508
+ span = tracer and tracer.current_span()
509
+ if span:
510
+ user_id and span.set_tag("usr.id", user_id)
511
+ email and span.set_tag("usr.email", email)
512
+ username and span.set_tag("usr.name", username)
513
+ ip_address and span.set_tag("http.client_ip", ip_address)
514
+ [span.set_tag(f"usr.{k}", str(v)) for k, v in (data or {}).items()]
515
+ failsafe(_set)
516
+
517
+ def start_transaction(self, name: str, op: str = None, attributes: typing.Dict[str, typing.Any] = None) -> lib.SpanContext:
518
+ attrs = {**(attributes or {}), **({"operation": op} if op else {})}
519
+ return self.start_span(name, attributes=attrs or None, kind="server")
520
+
521
+
522
+ # =============================================================================
523
+ # Background Task Utilities
524
+ # =============================================================================
525
+
526
+
527
+ def create_task_tracer(task_name: str = None, context: typing.Dict[str, typing.Any] = None) -> lib.Tracer:
528
+ """Create a Tracer with telemetry for background tasks (Huey/Celery).
529
+
530
+ Works correctly without Django HTTP request context.
531
+ """
532
+ tracer = lib.Tracer()
533
+ tracer.set_telemetry(get_telemetry_instance())
534
+ tracer.set_tag("execution.type", "background_task")
535
+ task_name and tracer.set_tag("task.name", task_name)
536
+
537
+ ctx = context or {}
538
+ ctx.get("user_id") and tracer.set_user(user_id=str(ctx["user_id"]))
539
+ ctx.get("org_id") and tracer.set_tag("org.id", str(ctx["org_id"]))
540
+ ctx.get("test_mode") is not None and tracer.set_tag("test_mode", str(ctx["test_mode"]).lower())
541
+
542
+ return tracer
543
+
544
+
545
+ def with_task_telemetry(task_name: str = None):
546
+ """Decorator that adds telemetry instrumentation to background tasks."""
547
+ def decorator(func):
548
+ @functools.wraps(func)
549
+ def wrapper(*args, **kwargs):
550
+ op_name = task_name or func.__name__
551
+ telemetry = get_telemetry_instance()
552
+
553
+ telemetry.add_breadcrumb(f"Starting background task: {op_name}", "task", {"task_name": op_name}, "info")
554
+
555
+ with telemetry.start_span(f"task.{op_name}", kind="consumer") as span:
556
+ span.set_attribute("task.name", op_name)
557
+ span.set_attribute("task.type", "background")
558
+
559
+ try:
560
+ result = func(*args, **kwargs)
561
+ span.set_status("ok")
562
+ telemetry.record_metric(f"karrio_task_{op_name}_success", 1, tags={"task_name": op_name})
563
+ return result
564
+
565
+ except Exception as e:
566
+ span.set_status("error", str(e))
567
+ span.record_exception(e)
568
+ telemetry.record_metric(f"karrio_task_{op_name}_error", 1, tags={"task_name": op_name, "error_type": type(e).__name__})
569
+ telemetry.capture_exception(e, context={"task_name": op_name}, tags={"task_name": op_name})
570
+ raise
571
+
572
+ return wrapper
573
+ return decorator