karrio-server 2025.5rc15__py3-none-any.whl → 2025.5rc18__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.5rc15
1
+ 2025.5rc18
@@ -0,0 +1,146 @@
1
+ """
2
+ OpenTelemetry instrumentation for Huey task queue.
3
+
4
+ This module provides tracing support for Huey tasks, enabling distributed tracing
5
+ across API requests and background tasks.
6
+ """
7
+ import functools
8
+ import logging
9
+ from typing import Any, Callable, Dict, Optional
10
+
11
+ from opentelemetry import trace, context, propagate
12
+ from opentelemetry.trace import Status, StatusCode
13
+ from opentelemetry.semconv.trace import SpanAttributes
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class HueyInstrumentor:
19
+ """Instrumentation for Huey task queue."""
20
+
21
+ _instance = None
22
+ _instrumented = False
23
+
24
+ def __new__(cls):
25
+ if cls._instance is None:
26
+ cls._instance = super().__new__(cls)
27
+ return cls._instance
28
+
29
+ def instrument(self, huey_instance=None):
30
+ """
31
+ Instrument Huey for OpenTelemetry tracing.
32
+
33
+ Args:
34
+ huey_instance: The Huey instance to instrument. If None, will try to
35
+ import from Django settings.
36
+ """
37
+ if self._instrumented:
38
+ logger.debug("Huey already instrumented")
39
+ return
40
+
41
+ try:
42
+ if huey_instance is None:
43
+ from django.conf import settings
44
+ huey_instance = settings.HUEY
45
+
46
+ # Wrap the task decorator
47
+ original_task = huey_instance.task
48
+ huey_instance.task = self._wrap_task_decorator(original_task, huey_instance)
49
+
50
+ # Wrap periodic tasks
51
+ if hasattr(huey_instance, 'periodic_task'):
52
+ original_periodic = huey_instance.periodic_task
53
+ huey_instance.periodic_task = self._wrap_task_decorator(original_periodic, huey_instance)
54
+
55
+ self._instrumented = True
56
+ logger.info("Huey instrumented for OpenTelemetry")
57
+
58
+ except Exception as e:
59
+ logger.warning(f"Failed to instrument Huey: {e}")
60
+
61
+ def _wrap_task_decorator(self, original_decorator: Callable, huey_instance) -> Callable:
62
+ """Wrap the Huey task decorator to add tracing."""
63
+
64
+ @functools.wraps(original_decorator)
65
+ def wrapped_decorator(*args, **kwargs):
66
+ decorated = original_decorator(*args, **kwargs)
67
+
68
+ def task_wrapper(fn):
69
+ task_fn = decorated(fn)
70
+
71
+ @functools.wraps(task_fn)
72
+ def traced_task(*task_args, **task_kwargs):
73
+ tracer = trace.get_tracer(__name__)
74
+
75
+ # Extract trace context from task kwargs if present
76
+ trace_context = task_kwargs.pop('_otel_context', None)
77
+ if trace_context:
78
+ ctx = propagate.extract(trace_context)
79
+ token = context.attach(ctx)
80
+ else:
81
+ token = None
82
+
83
+ # Start span for the task
84
+ task_name = fn.__name__
85
+ with tracer.start_as_current_span(
86
+ f"huey.task.{task_name}",
87
+ kind=trace.SpanKind.CONSUMER,
88
+ ) as span:
89
+ try:
90
+ # Set span attributes
91
+ span.set_attribute("messaging.system", "huey")
92
+ span.set_attribute("messaging.destination", task_name)
93
+ span.set_attribute("messaging.operation", "process")
94
+ span.set_attribute("task.name", task_name)
95
+
96
+ # Execute the task
97
+ result = task_fn(*task_args, **task_kwargs)
98
+ span.set_status(Status(StatusCode.OK))
99
+ return result
100
+
101
+ except Exception as e:
102
+ span.set_status(Status(StatusCode.ERROR, str(e)))
103
+ span.record_exception(e)
104
+ raise
105
+ finally:
106
+ if token:
107
+ context.detach(token)
108
+
109
+ # Preserve original attributes
110
+ traced_task.task = task_fn.task if hasattr(task_fn, 'task') else task_fn
111
+ if hasattr(task_fn, '__name__'):
112
+ traced_task.__name__ = task_fn.__name__
113
+ if hasattr(task_fn, '__module__'):
114
+ traced_task.__module__ = task_fn.__module__
115
+
116
+ return traced_task
117
+
118
+ return task_wrapper
119
+
120
+ return wrapped_decorator
121
+
122
+
123
+ def inject_trace_context(task_kwargs: Dict[str, Any]) -> Dict[str, Any]:
124
+ """
125
+ Inject current trace context into task kwargs for propagation.
126
+
127
+ This should be called when enqueuing a task to propagate the trace context
128
+ to the background worker.
129
+
130
+ Args:
131
+ task_kwargs: The kwargs to be passed to the task
132
+
133
+ Returns:
134
+ Updated kwargs with trace context
135
+ """
136
+ carrier = {}
137
+ propagate.inject(carrier)
138
+ if carrier:
139
+ task_kwargs['_otel_context'] = carrier
140
+ return task_kwargs
141
+
142
+
143
+ def instrument_huey():
144
+ """Convenience function to instrument Huey."""
145
+ instrumentor = HueyInstrumentor()
146
+ instrumentor.instrument()
@@ -56,3 +56,128 @@ if SENTRY_DSN:
56
56
  # django.contrib.auth) you may enable sending PII data.
57
57
  send_default_pii=True,
58
58
  )
59
+
60
+
61
+ # OpenTelemetry Configuration
62
+ OTEL_ENABLED = config("OTEL_ENABLED", default=False, cast=bool)
63
+ OTEL_SERVICE_NAME = config("OTEL_SERVICE_NAME", default="karrio-api")
64
+ OTEL_EXPORTER_OTLP_ENDPOINT = config("OTEL_EXPORTER_OTLP_ENDPOINT", default=None)
65
+ OTEL_EXPORTER_OTLP_PROTOCOL = config("OTEL_EXPORTER_OTLP_PROTOCOL", default="grpc")
66
+ OTEL_EXPORTER_OTLP_HEADERS = config("OTEL_EXPORTER_OTLP_HEADERS", default="")
67
+ OTEL_TRACES_EXPORTER = config("OTEL_TRACES_EXPORTER", default="otlp")
68
+ OTEL_METRICS_EXPORTER = config("OTEL_METRICS_EXPORTER", default="otlp")
69
+ OTEL_LOGS_EXPORTER = config("OTEL_LOGS_EXPORTER", default="otlp")
70
+ OTEL_RESOURCE_ATTRIBUTES = config("OTEL_RESOURCE_ATTRIBUTES", default="")
71
+ OTEL_ENVIRONMENT = config("OTEL_ENVIRONMENT", default=config("ENV", default="production"))
72
+
73
+ # Only initialize OpenTelemetry if enabled and endpoint is configured
74
+ if OTEL_ENABLED and OTEL_EXPORTER_OTLP_ENDPOINT:
75
+ import logging
76
+ from opentelemetry import trace, metrics
77
+ from opentelemetry.sdk.trace import TracerProvider
78
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
79
+ from opentelemetry.sdk.metrics import MeterProvider
80
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
81
+ from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
82
+ from opentelemetry.instrumentation.django import DjangoInstrumentor
83
+ from opentelemetry.instrumentation.requests import RequestsInstrumentor
84
+ from opentelemetry.instrumentation.logging import LoggingInstrumentor
85
+ from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor
86
+ from opentelemetry.instrumentation.redis import RedisInstrumentor
87
+
88
+ # Import appropriate exporter based on protocol
89
+ if OTEL_EXPORTER_OTLP_PROTOCOL == "grpc":
90
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
91
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
92
+ else: # http/protobuf
93
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
94
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
95
+
96
+ # Parse headers if provided
97
+ headers = {}
98
+ if OTEL_EXPORTER_OTLP_HEADERS:
99
+ for header_pair in OTEL_EXPORTER_OTLP_HEADERS.split(","):
100
+ if "=" in header_pair:
101
+ key, value = header_pair.split("=", 1)
102
+ headers[key.strip()] = value.strip()
103
+
104
+ # Parse resource attributes
105
+ resource_attributes = {
106
+ SERVICE_NAME: OTEL_SERVICE_NAME,
107
+ SERVICE_VERSION: config("VERSION", default="unknown"),
108
+ "environment": OTEL_ENVIRONMENT,
109
+ "deployment.environment": OTEL_ENVIRONMENT,
110
+ }
111
+
112
+ if OTEL_RESOURCE_ATTRIBUTES:
113
+ for attr_pair in OTEL_RESOURCE_ATTRIBUTES.split(","):
114
+ if "=" in attr_pair:
115
+ key, value = attr_pair.split("=", 1)
116
+ resource_attributes[key.strip()] = value.strip()
117
+
118
+ # Create resource
119
+ resource = Resource(attributes=resource_attributes)
120
+
121
+ # Configure Trace Provider
122
+ trace_provider = TracerProvider(resource=resource)
123
+ trace.set_tracer_provider(trace_provider)
124
+
125
+ # Configure span exporter
126
+ span_exporter = OTLPSpanExporter(
127
+ endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
128
+ headers=headers if headers else None,
129
+ )
130
+ span_processor = BatchSpanProcessor(span_exporter)
131
+ trace_provider.add_span_processor(span_processor)
132
+
133
+ # Configure Metrics Provider
134
+ metric_exporter = OTLPMetricExporter(
135
+ endpoint=OTEL_EXPORTER_OTLP_ENDPOINT,
136
+ headers=headers if headers else None,
137
+ )
138
+ metric_reader = PeriodicExportingMetricReader(
139
+ exporter=metric_exporter,
140
+ export_interval_millis=30000, # Export metrics every 30 seconds
141
+ )
142
+ meter_provider = MeterProvider(
143
+ resource=resource,
144
+ metric_readers=[metric_reader],
145
+ )
146
+ metrics.set_meter_provider(meter_provider)
147
+
148
+ # Instrument Django
149
+ DjangoInstrumentor().instrument(
150
+ is_sql_commentor_enabled=True, # Add trace context to SQL queries
151
+ request_hook=lambda span, request: span.set_attribute("http.client_ip", request.META.get("REMOTE_ADDR", "")),
152
+ response_hook=lambda span, request, response: span.set_attribute("http.response.size", len(response.content) if hasattr(response, 'content') else 0),
153
+ )
154
+
155
+ # Instrument other libraries
156
+ RequestsInstrumentor().instrument() # HTTP client requests
157
+ LoggingInstrumentor().instrument(set_logging_format=True) # Add trace context to logs
158
+
159
+ # Instrument database if PostgreSQL is used
160
+ if config("DATABASE_ENGINE", default="").endswith("postgresql"):
161
+ try:
162
+ Psycopg2Instrumentor().instrument()
163
+ except Exception:
164
+ pass # Psycopg2 might not be installed
165
+
166
+ # Instrument Redis if configured
167
+ if config("REDIS_HOST", default=None):
168
+ try:
169
+ RedisInstrumentor().instrument()
170
+ except Exception:
171
+ pass # Redis might not be installed
172
+
173
+ # Instrument Huey task queue (temporarily disabled due to compatibility issues)
174
+ # try:
175
+ # from karrio.server.lib.otel_huey import instrument_huey
176
+ # instrument_huey()
177
+ # except Exception as e:
178
+ # logger = logging.getLogger(__name__)
179
+ # logger.warning(f"Failed to instrument Huey: {e}")
180
+
181
+ # Log that OpenTelemetry is enabled
182
+ logger = logging.getLogger(__name__)
183
+ logger.info(f"OpenTelemetry enabled: Service={OTEL_SERVICE_NAME}, Endpoint={OTEL_EXPORTER_OTLP_ENDPOINT}")
@@ -51,3 +51,19 @@ else:
51
51
  filename=WORKER_DB_FILE_NAME,
52
52
  **({"immediate": WORKER_IMMEDIATE_MODE} if WORKER_IMMEDIATE_MODE else {}),
53
53
  )
54
+
55
+
56
+ # Apply OpenTelemetry instrumentation to Huey if enabled
57
+ OTEL_ENABLED = decouple.config("OTEL_ENABLED", default=False, cast=bool)
58
+ OTEL_EXPORTER_OTLP_ENDPOINT = decouple.config("OTEL_EXPORTER_OTLP_ENDPOINT", default=None)
59
+
60
+ if OTEL_ENABLED and OTEL_EXPORTER_OTLP_ENDPOINT:
61
+ try:
62
+ # Import and apply instrumentation to the Huey instance
63
+ from karrio.server.lib.otel_huey import HueyInstrumentor
64
+ instrumentor = HueyInstrumentor()
65
+ instrumentor.instrument(HUEY)
66
+ except Exception as e:
67
+ import logging
68
+ logger = logging.getLogger(__name__)
69
+ logger.warning(f"Failed to instrument Huey in worker settings: {e}")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: karrio_server
3
- Version: 2025.5rc15
3
+ Version: 2025.5rc18
4
4
  Summary: Multi-carrier shipping API
5
5
  Author-email: karrio <hello@karrio.io>
6
6
  License-Expression: Apache-2.0
@@ -33,6 +33,15 @@ Requires-Dist: python-decouple
33
33
  Requires-Dist: karrio_server_core
34
34
  Requires-Dist: sentry-sdk
35
35
  Requires-Dist: whitenoise
36
+ Requires-Dist: opentelemetry-instrumentation-django
37
+ Requires-Dist: opentelemetry-api
38
+ Requires-Dist: opentelemetry-sdk
39
+ Requires-Dist: opentelemetry-exporter-otlp
40
+ Requires-Dist: opentelemetry-instrumentation-requests
41
+ Requires-Dist: opentelemetry-instrumentation-logging
42
+ Requires-Dist: opentelemetry-instrumentation-psycopg2
43
+ Requires-Dist: opentelemetry-instrumentation-redis
44
+ Requires-Dist: opentelemetry-semantic-conventions
36
45
 
37
46
  # <a href="https://karrio.io" target="_blank"><img alt="Karrio" src="https://docs.karrio.io/img/logo.svg" height="50px" /></a>
38
47
 
@@ -1,17 +1,18 @@
1
- karrio/server/VERSION,sha256=lFo0e02tTzOvzYVZoSB_8ida1fuxK64T6yOESGDIWF8,10
1
+ karrio/server/VERSION,sha256=pU20An_zTwHpnPk8666yTFrIhNSOAR9TMWIH4n9MUrU,10
2
2
  karrio/server/__init__.py,sha256=iOEMwnlORWezdO8-2vxBIPSR37D7JGjluZ8f55vzxls,81
3
3
  karrio/server/__main__.py,sha256=hy2-Zb2wSVe_Pu6zWZ-BhiM4rBaGZ3D16HSuhudygqg,632
4
4
  karrio/server/asgi.py,sha256=LsZYMWo8U9zURVPdHnvUsziOhMjdCdQoD2-gMJbS2U0,462
5
5
  karrio/server/workers.py,sha256=wOlWmXC7zRJV86IfbQnfUZsnCIvvWtXzFHH_EIkg1J0,179
6
6
  karrio/server/wsgi.py,sha256=SpWqkEYlMsj89_znZ8p8IjH3EgTVRWRq_9eS8t64dMw,403
7
+ karrio/server/lib/otel_huey.py,sha256=6MP6vX6b6x6RPF2K1m8B8L8S9GK1Q3vANmLidsxh65k,5428
7
8
  karrio/server/settings/__init__.py,sha256=iw-NBcReOnDYpnvSEBdYDfV7jC0040jYdupnmSdElec,866
8
- karrio/server/settings/apm.py,sha256=CfUn6z7hermfw0m-2EvzHyWRLGZErnWGaaoWlYIRYZw,1710
9
+ karrio/server/settings/apm.py,sha256=963moZDKcwEelUh1AmLQY1RRsm5qk3neugJXxk6fBY0,7322
9
10
  karrio/server/settings/base.py,sha256=iryAK2fL_mul9UjHp4lU57S0Lzk-AEvpUSSf0_E1E9U,20375
10
11
  karrio/server/settings/cache.py,sha256=BCFIjbLKBgYs34i099Z_i8EC55lRF39oJfsPHoXq3vc,1112
11
12
  karrio/server/settings/constance.py,sha256=wKi7u-NORAPjIJMIbl3k5kPRkUA6jJJWe9kxsV3aoeA,7113
12
13
  karrio/server/settings/debug.py,sha256=fFyK2XX47UGeK0eRNSV-9ZLaComay5QvJW0692vaH98,527
13
14
  karrio/server/settings/email.py,sha256=bHBLKM_v3HTkmjrz_Msdj_Z7_kMzAb7i6pvJCZuzu1E,1403
14
- karrio/server/settings/workers.py,sha256=s3km5UZcultq4O1adWZWso7jvRyfhc4jk3T1Jx0EoCA,1697
15
+ karrio/server/settings/workers.py,sha256=xoXvaZgxKKVlvB3xJgq0PFCTEhNQf9rte_Qy6BUSY28,2360
15
16
  karrio/server/static/extra/branding/android-chrome-192x192.png,sha256=qSwEKKBtk4udSHb0OWGC5-jfkP5cVpULDD1Cdkuk8PU,7005
16
17
  karrio/server/static/extra/branding/android-chrome-512x512.png,sha256=YFwVPnPChO30tTuyrwSc_z548H7C6OFXh4CCu2krvHA,21062
17
18
  karrio/server/static/extra/branding/favicon-16x16.png,sha256=wkELpij29bIvvKr5sDcjfNeYvj7i0yk-__bJbZckEK8,1085
@@ -72,8 +73,8 @@ karrio/server/templates/admin/base_site.html,sha256=kbcdvehXZ1EHaw07JL7fSZmjrnVM
72
73
  karrio/server/templates/openapi/openapi.html,sha256=3ApCZ5pE6Wjv7CJllVbqD2WiDQuLy-BFS-IIHurXhBY,1133
73
74
  karrio/server/urls/__init__.py,sha256=Ah-XqaqRsfecQgCGRHjxmXe8O7a0avq5ocU90tkVwQI,1998
74
75
  karrio/server/urls/jwt.py,sha256=QN2L-EpUEQCF2UGYPu_VVlA49Fc0BtcY7Ov3-xpp7_U,6772
75
- karrio_server-2025.5rc15.dist-info/METADATA,sha256=Doodw-t7ej8MMPRIE4DuDzAT9VUqLO596UYcOsvGLWg,3860
76
- karrio_server-2025.5rc15.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
77
- karrio_server-2025.5rc15.dist-info/entry_points.txt,sha256=c2eftt6MpJjyp0OFv1OmO9nUYSDemt9fGq_RDdvpGLw,55
78
- karrio_server-2025.5rc15.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
79
- karrio_server-2025.5rc15.dist-info/RECORD,,
76
+ karrio_server-2025.5rc18.dist-info/METADATA,sha256=IEM9FXoEZAlk_SsSV7DQI8EUJVOZW7DE9QXdTSZYTF0,4283
77
+ karrio_server-2025.5rc18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
78
+ karrio_server-2025.5rc18.dist-info/entry_points.txt,sha256=c2eftt6MpJjyp0OFv1OmO9nUYSDemt9fGq_RDdvpGLw,55
79
+ karrio_server-2025.5rc18.dist-info/top_level.txt,sha256=D1D7x8R3cTfjF_15mfiO7wCQ5QMtuM4x8GaPr7z5i78,12
80
+ karrio_server-2025.5rc18.dist-info/RECORD,,