tp-trace 0.1.6__tar.gz → 0.2.0__tar.gz
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.
- {tp_trace-0.1.6 → tp_trace-0.2.0}/PKG-INFO +5 -2
- {tp_trace-0.1.6 → tp_trace-0.2.0}/pyproject.toml +4 -1
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/__init__.py +39 -6
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/config.py +94 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/base.py +22 -1
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/celery/context.py +1 -1
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/celery/instrumentor.py +89 -13
- tp_trace-0.2.0/src/tp_trace/integrations/django/__init__.py +15 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/django/cache.py +7 -4
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/django/integration.py +55 -7
- tp_trace-0.2.0/src/tp_trace/integrations/django/middleware.py +422 -0
- tp_trace-0.2.0/src/tp_trace/integrations/django/request.py +799 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/django/view.py +94 -71
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/manager.py +80 -68
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/postgres/cursor.py +27 -9
- tp_trace-0.2.0/src/tp_trace/metrics.py +606 -0
- tp_trace-0.2.0/src/tp_trace/propagator.py +63 -0
- tp_trace-0.2.0/src/tp_trace/red.py +265 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/route_context.py +152 -1
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/sampler.py +91 -10
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/sanitize.py +12 -1
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/tracing.py +4 -1
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/version.py +1 -1
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace.egg-info/PKG-INFO +5 -2
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace.egg-info/SOURCES.txt +11 -2
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace.egg-info/requires.txt +5 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_celery_integration.py +207 -10
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_django.py +101 -3
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_django_advanced.py +116 -6
- tp_trace-0.2.0/tests/test_django_migration_regressions.py +352 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_init.py +48 -8
- tp_trace-0.2.0/tests/test_metrics.py +966 -0
- tp_trace-0.2.0/tests/test_n_plus_one.py +612 -0
- tp_trace-0.2.0/tests/test_n_plus_one_dashboard_contract.py +173 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_postgres.py +36 -11
- tp_trace-0.2.0/tests/test_red.py +817 -0
- tp_trace-0.2.0/tests/test_red_comparison.py +113 -0
- tp_trace-0.2.0/tests/test_red_dashboard_contract.py +345 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_safety.py +440 -67
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_sampling_rules.py +78 -9
- tp_trace-0.2.0/tests/test_smoke_probe.py +82 -0
- tp_trace-0.2.0/tests/test_traffic_generator.py +105 -0
- tp_trace-0.1.6/src/tp_trace/integrations/django/__init__.py +0 -17
- tp_trace-0.1.6/src/tp_trace/integrations/django/middleware.py +0 -228
- tp_trace-0.1.6/src/tp_trace/integrations/django/middleware_init.py +0 -91
- tp_trace-0.1.6/src/tp_trace/integrations/django/request.py +0 -667
- tp_trace-0.1.6/src/tp_trace/metrics.py +0 -258
- tp_trace-0.1.6/tests/test_metrics.py +0 -372
- {tp_trace-0.1.6 → tp_trace-0.2.0}/README.md +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/setup.cfg +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/config_access.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/exporter.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/__init__.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/boto/__init__.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/boto/integration.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/celery/__init__.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/celery/integration.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/django/auth.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/django/compat.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/django/template.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/postgres/__init__.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/postgres/integration.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/redis/__init__.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/redis/integration.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/requests/__init__.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/requests/client.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/integrations/requests/integration.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace/safety.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace.egg-info/dependency_links.txt +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/src/tp_trace.egg-info/top_level.txt +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_base_integration.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_boto.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_celery_probe.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_redis.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_requests.py +0 -0
- {tp_trace-0.1.6 → tp_trace-0.2.0}/tests/test_sample_telemetry.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: tp_trace
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: tp_trace: High-performance Python Observability SDK for OpenTelemetry
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -12,9 +12,12 @@ Requires-Dist: opentelemetry-instrumentation-psycopg2>=0.44b0
|
|
|
12
12
|
Requires-Dist: opentelemetry-instrumentation-redis>=0.44b0
|
|
13
13
|
Requires-Dist: opentelemetry-instrumentation-botocore>=0.44b0
|
|
14
14
|
Requires-Dist: opentelemetry-instrumentation-celery>=0.54b1
|
|
15
|
+
Requires-Dist: opentelemetry-instrumentation-django>=0.44b0
|
|
15
16
|
Requires-Dist: wrapt>=1.14.0
|
|
17
|
+
Requires-Dist: urllib3<2,>=1.26.0; python_version < "3.9"
|
|
16
18
|
Provides-Extra: django
|
|
17
19
|
Requires-Dist: django>=4.2; extra == "django"
|
|
20
|
+
Requires-Dist: opentelemetry-instrumentation-django>=0.44b0; extra == "django"
|
|
18
21
|
Provides-Extra: dev
|
|
19
22
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
20
23
|
Requires-Dist: botocore>=1.37.38; extra == "dev"
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "tp_trace"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.0"
|
|
8
8
|
description = "tp_trace: High-performance Python Observability SDK for OpenTelemetry"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.8"
|
|
@@ -17,12 +17,15 @@ dependencies = [
|
|
|
17
17
|
"opentelemetry-instrumentation-redis>=0.44b0",
|
|
18
18
|
"opentelemetry-instrumentation-botocore>=0.44b0",
|
|
19
19
|
"opentelemetry-instrumentation-celery>=0.54b1",
|
|
20
|
+
"opentelemetry-instrumentation-django>=0.44b0",
|
|
20
21
|
"wrapt>=1.14.0",
|
|
22
|
+
"urllib3>=1.26.0,<2; python_version < '3.9'",
|
|
21
23
|
]
|
|
22
24
|
|
|
23
25
|
[project.optional-dependencies]
|
|
24
26
|
django = [
|
|
25
27
|
"django>=4.2",
|
|
28
|
+
"opentelemetry-instrumentation-django>=0.44b0",
|
|
26
29
|
]
|
|
27
30
|
dev = [
|
|
28
31
|
"pytest>=7.0.0",
|
|
@@ -18,11 +18,11 @@ from opentelemetry.sdk.trace.sampling import ALWAYS_OFF, Sampler
|
|
|
18
18
|
from opentelemetry.trace import get_current_span, get_tracer
|
|
19
19
|
from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
|
|
20
20
|
from opentelemetry.baggage.propagation import W3CBaggagePropagator
|
|
21
|
-
from opentelemetry.propagators.composite import CompositePropagator
|
|
22
21
|
from opentelemetry.propagate import set_global_textmap
|
|
23
22
|
|
|
24
23
|
from tp_trace.config import SDKConfig
|
|
25
24
|
from tp_trace.exporter import SafeSpanExporter
|
|
25
|
+
from tp_trace.red import RedSpanProcessor, SampledOnlySpanProcessor
|
|
26
26
|
from tp_trace.route_context import RouteEnrichingSpanProcessor
|
|
27
27
|
from tp_trace.integrations import BaseIntegration, get_integration_manager
|
|
28
28
|
from tp_trace.sanitize import sanitize_query_string, sanitize_sql, sanitize_url
|
|
@@ -42,6 +42,8 @@ __all__ = [
|
|
|
42
42
|
"BaseIntegration",
|
|
43
43
|
"get_integration_manager",
|
|
44
44
|
"RouteEnrichingSpanProcessor",
|
|
45
|
+
"RedSpanProcessor",
|
|
46
|
+
"SampledOnlySpanProcessor",
|
|
45
47
|
"SafeSpanExporter",
|
|
46
48
|
]
|
|
47
49
|
|
|
@@ -198,6 +200,12 @@ def init(
|
|
|
198
200
|
except Exception as exc:
|
|
199
201
|
logger.debug("tp_trace: Failed to register RouteEnrichingSpanProcessor: %s", exc)
|
|
200
202
|
|
|
203
|
+
# Register RED metrics aggregator: computes 100% RED metrics for all dependencies
|
|
204
|
+
try:
|
|
205
|
+
provider.add_span_processor(RedSpanProcessor())
|
|
206
|
+
except Exception as exc:
|
|
207
|
+
logger.debug("tp_trace: Failed to register RedSpanProcessor: %s", exc)
|
|
208
|
+
|
|
201
209
|
# Configure Exporter and Processor if not disabled
|
|
202
210
|
if not config.disabled:
|
|
203
211
|
if exporter is None:
|
|
@@ -234,18 +242,21 @@ def init(
|
|
|
234
242
|
else:
|
|
235
243
|
span_processor = SimpleSpanProcessor(safe_exporter)
|
|
236
244
|
|
|
237
|
-
|
|
245
|
+
# Gate exports: only spans with trace_flags.sampled=True are serialized/transmitted
|
|
246
|
+
provider.add_span_processor(SampledOnlySpanProcessor(span_processor))
|
|
238
247
|
|
|
239
248
|
# Set as global tracer provider
|
|
240
249
|
trace.set_tracer_provider(provider)
|
|
241
250
|
_ACTIVE_PROVIDER = provider
|
|
242
251
|
_INITIALIZED = True
|
|
243
252
|
|
|
244
|
-
# Setup W3C Trace Context and Baggage composite propagator
|
|
253
|
+
# Setup W3C Trace Context and Baggage composite propagator wrapped with TpTrace filtering
|
|
254
|
+
from tp_trace.propagator import TpTraceFilteringPropagator
|
|
255
|
+
|
|
245
256
|
try:
|
|
246
|
-
set_global_textmap(
|
|
257
|
+
set_global_textmap(TpTraceFilteringPropagator([TraceContextTextMapPropagator(), W3CBaggagePropagator()]))
|
|
247
258
|
except Exception:
|
|
248
|
-
set_global_textmap(TraceContextTextMapPropagator())
|
|
259
|
+
set_global_textmap(TpTraceFilteringPropagator([TraceContextTextMapPropagator()]))
|
|
249
260
|
|
|
250
261
|
# Auto-patch installed integrations if requested
|
|
251
262
|
if config.auto_patch and not config.disabled:
|
|
@@ -335,8 +346,11 @@ def _reset_for_testing() -> None:
|
|
|
335
346
|
except Exception:
|
|
336
347
|
pass
|
|
337
348
|
try:
|
|
338
|
-
from tp_trace.metrics import set_metrics_recorder
|
|
349
|
+
from tp_trace.metrics import get_metrics_recorder, set_metrics_recorder
|
|
339
350
|
|
|
351
|
+
rec = get_metrics_recorder()
|
|
352
|
+
if rec is not None:
|
|
353
|
+
rec.shutdown()
|
|
340
354
|
set_metrics_recorder(None)
|
|
341
355
|
except Exception:
|
|
342
356
|
pass
|
|
@@ -366,9 +380,28 @@ def set_attribute(key: str, value: Any) -> None:
|
|
|
366
380
|
set_tag(key, value)
|
|
367
381
|
|
|
368
382
|
|
|
383
|
+
def set_view(view_name: Optional[str]) -> Optional[Any]:
|
|
384
|
+
"""Set the active view name on the in-flight context and propagate to child spans / Celery tasks.
|
|
385
|
+
|
|
386
|
+
Returns a token that can be passed to reset_view if manual scope management is needed.
|
|
387
|
+
"""
|
|
388
|
+
from tp_trace.route_context import set_request_view
|
|
389
|
+
|
|
390
|
+
return set_request_view(view_name)
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def reset_view(token: Optional[Any] = None) -> None:
|
|
394
|
+
"""Reset the active view name on the in-flight context."""
|
|
395
|
+
from tp_trace.route_context import reset_request_view
|
|
396
|
+
|
|
397
|
+
reset_request_view(token)
|
|
398
|
+
|
|
399
|
+
|
|
369
400
|
__all__ = [
|
|
370
401
|
"set_tag",
|
|
371
402
|
"set_attribute",
|
|
403
|
+
"set_view",
|
|
404
|
+
"reset_view",
|
|
372
405
|
"init",
|
|
373
406
|
"patch_all",
|
|
374
407
|
"get_config",
|
|
@@ -76,6 +76,38 @@ def _parse_cmd_max_length(val: Any) -> int:
|
|
|
76
76
|
return n
|
|
77
77
|
|
|
78
78
|
|
|
79
|
+
def _parse_positive_int_default(val: Any, default: int, min_val: int = 1, field_name: str = "") -> int:
|
|
80
|
+
"""Non-throwing positive integer parser.
|
|
81
|
+
|
|
82
|
+
Catches type, format, and range errors, emits debug log, and returns default.
|
|
83
|
+
"""
|
|
84
|
+
if val is None:
|
|
85
|
+
return default
|
|
86
|
+
try:
|
|
87
|
+
n = int(val)
|
|
88
|
+
if n >= min_val:
|
|
89
|
+
return n
|
|
90
|
+
logger.debug("tp_trace: %s=%r below minimum %d, falling back to %d", field_name, val, min_val, default)
|
|
91
|
+
return default
|
|
92
|
+
except (TypeError, ValueError) as exc:
|
|
93
|
+
logger.debug("tp_trace: %s=%r invalid integer (%s), falling back to %d", field_name, val, exc, default)
|
|
94
|
+
return default
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _parse_nonnegative_float_default(val: Any, default: float, field_name: str = "") -> float:
|
|
98
|
+
"""Parse a finite non-negative float, falling back safely on invalid input."""
|
|
99
|
+
if val is None:
|
|
100
|
+
return default
|
|
101
|
+
try:
|
|
102
|
+
number = float(val)
|
|
103
|
+
if math.isfinite(number) and number >= 0:
|
|
104
|
+
return number
|
|
105
|
+
except (TypeError, ValueError):
|
|
106
|
+
pass
|
|
107
|
+
logger.debug("tp_trace: %s=%r invalid duration, falling back to %s", field_name, val, default)
|
|
108
|
+
return default
|
|
109
|
+
|
|
110
|
+
|
|
79
111
|
def _parse_endpoint_sample_rules(val: Any) -> Dict[str, float]:
|
|
80
112
|
"""Parse and validate endpoint-specific sampling rules.
|
|
81
113
|
|
|
@@ -214,10 +246,13 @@ class SDKConfig:
|
|
|
214
246
|
trusted_proxies: List[str] = field(default_factory=list)
|
|
215
247
|
db_role_map: Dict[str, str] = field(default_factory=dict)
|
|
216
248
|
auto_patch: bool = True
|
|
249
|
+
# Governs incoming HTTP request propagation (e.g. Django). Internal messaging (Celery) always extracts.
|
|
217
250
|
extract_trace_context: Any = True
|
|
218
251
|
metrics_endpoint: Optional[str] = None
|
|
219
252
|
metrics_enabled: bool = True
|
|
220
253
|
metrics_export_interval_millis: int = 60000
|
|
254
|
+
metric_cardinality_limit: int = 5000
|
|
255
|
+
service_instance_id: Optional[str] = None
|
|
221
256
|
# Masking -> exposure toggles. Defaults expose infrastructure details
|
|
222
257
|
# (Redis endpoint, Redis command contents, URL query strings), matching
|
|
223
258
|
# dd-trace. Deployments that do NOT own the infra can restore masking.
|
|
@@ -225,6 +260,8 @@ class SDKConfig:
|
|
|
225
260
|
redis_mask_command: bool = False
|
|
226
261
|
redis_cmd_max_length: int = 1000
|
|
227
262
|
query_mode: str = "expose"
|
|
263
|
+
n_plus_one_duplicate_threshold: int = 15
|
|
264
|
+
n_plus_one_max_fingerprints: int = 256
|
|
228
265
|
|
|
229
266
|
@classmethod
|
|
230
267
|
def from_env_and_kwargs(
|
|
@@ -250,10 +287,14 @@ class SDKConfig:
|
|
|
250
287
|
metrics_endpoint: Optional[str] = None,
|
|
251
288
|
metrics_enabled: Optional[bool] = None,
|
|
252
289
|
metrics_export_interval_millis: Optional[int] = None,
|
|
290
|
+
metric_cardinality_limit: Optional[int] = None,
|
|
291
|
+
service_instance_id: Optional[str] = None,
|
|
253
292
|
redis_mask_endpoint: Optional[bool] = None,
|
|
254
293
|
redis_mask_command: Optional[bool] = None,
|
|
255
294
|
redis_cmd_max_length: Optional[int] = None,
|
|
256
295
|
query_mode: Optional[str] = None,
|
|
296
|
+
n_plus_one_duplicate_threshold: Optional[int] = None,
|
|
297
|
+
n_plus_one_max_fingerprints: Optional[int] = None,
|
|
257
298
|
**extra: Any,
|
|
258
299
|
) -> "SDKConfig":
|
|
259
300
|
"""Build SDKConfig by prioritizing explicit kwargs over environment variables."""
|
|
@@ -368,6 +409,25 @@ class SDKConfig:
|
|
|
368
409
|
except Exception:
|
|
369
410
|
resolved_metrics_interval = 60000
|
|
370
411
|
|
|
412
|
+
try:
|
|
413
|
+
raw_cardinality_limit = (
|
|
414
|
+
metric_cardinality_limit
|
|
415
|
+
if metric_cardinality_limit is not None
|
|
416
|
+
else extra.get("metric_cardinality_limit")
|
|
417
|
+
or os.getenv("TP_TRACE_METRIC_CARDINALITY_LIMIT")
|
|
418
|
+
)
|
|
419
|
+
resolved_metric_cardinality_limit = max(1, int(raw_cardinality_limit)) if raw_cardinality_limit is not None else 5000
|
|
420
|
+
except Exception:
|
|
421
|
+
resolved_metric_cardinality_limit = 5000
|
|
422
|
+
|
|
423
|
+
resolved_service_instance_id = (
|
|
424
|
+
service_instance_id
|
|
425
|
+
or extra.get("service_instance_id")
|
|
426
|
+
or os.getenv("TP_TRACE_SERVICE_INSTANCE_ID")
|
|
427
|
+
or os.getenv("OTEL_SERVICE_INSTANCE_ID")
|
|
428
|
+
or None
|
|
429
|
+
)
|
|
430
|
+
|
|
371
431
|
env_headers = _parse_headers(os.getenv("TP_TRACE_HEADERS") or os.getenv("OTEL_EXPORTER_OTLP_HEADERS"))
|
|
372
432
|
merged_headers = dict(env_headers)
|
|
373
433
|
if headers:
|
|
@@ -484,6 +544,13 @@ class SDKConfig:
|
|
|
484
544
|
"AUTO_PATCH",
|
|
485
545
|
"extract_trace_context",
|
|
486
546
|
"EXTRACT_TRACE_CONTEXT",
|
|
547
|
+
"n_plus_one_duplicate_threshold",
|
|
548
|
+
"N_PLUS_ONE_DUPLICATE_THRESHOLD",
|
|
549
|
+
# Deprecated timing knobs are ignored rather than becoming resource tags.
|
|
550
|
+
"n_plus_one_min_cumulative_duration_ms",
|
|
551
|
+
"N_PLUS_ONE_MIN_CUMULATIVE_DURATION_MS",
|
|
552
|
+
"n_plus_one_max_fingerprints",
|
|
553
|
+
"N_PLUS_ONE_MAX_FINGERPRINTS",
|
|
487
554
|
}
|
|
488
555
|
for k, v in extra.items():
|
|
489
556
|
if k not in KNOWN_EXTRA_KEYS and not k.startswith("_"):
|
|
@@ -633,6 +700,29 @@ class SDKConfig:
|
|
|
633
700
|
raw_query_mode = query_mode if query_mode is not None else os.getenv("TP_TRACE_QUERY_MODE")
|
|
634
701
|
resolved_query_mode = _parse_query_mode(raw_query_mode)
|
|
635
702
|
|
|
703
|
+
raw_n_plus_one_dup = (
|
|
704
|
+
n_plus_one_duplicate_threshold
|
|
705
|
+
if n_plus_one_duplicate_threshold is not None
|
|
706
|
+
else extra.get("n_plus_one_duplicate_threshold")
|
|
707
|
+
if extra.get("n_plus_one_duplicate_threshold") is not None
|
|
708
|
+
else os.getenv("TP_TRACE_N_PLUS_ONE_DUPLICATE_THRESHOLD")
|
|
709
|
+
or os.getenv("TP_DOG_N_PLUS_ONE_DUPLICATE_THRESHOLD")
|
|
710
|
+
)
|
|
711
|
+
resolved_n_plus_one_dup = _parse_positive_int_default(
|
|
712
|
+
raw_n_plus_one_dup, default=15, min_val=2, field_name="n_plus_one_duplicate_threshold"
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
raw_n_plus_one_max_fp = (
|
|
716
|
+
n_plus_one_max_fingerprints
|
|
717
|
+
if n_plus_one_max_fingerprints is not None
|
|
718
|
+
else extra.get("n_plus_one_max_fingerprints")
|
|
719
|
+
if extra.get("n_plus_one_max_fingerprints") is not None
|
|
720
|
+
else os.getenv("TP_TRACE_N_PLUS_ONE_MAX_FINGERPRINTS") or os.getenv("TP_DOG_N_PLUS_ONE_MAX_FINGERPRINTS")
|
|
721
|
+
)
|
|
722
|
+
resolved_n_plus_one_max_fp = _parse_positive_int_default(
|
|
723
|
+
raw_n_plus_one_max_fp, default=256, min_val=1, field_name="n_plus_one_max_fingerprints"
|
|
724
|
+
)
|
|
725
|
+
|
|
636
726
|
return cls(
|
|
637
727
|
project_name=resolved_project,
|
|
638
728
|
cluster_name=resolved_cluster,
|
|
@@ -661,8 +751,12 @@ class SDKConfig:
|
|
|
661
751
|
metrics_endpoint=resolved_metrics_endpoint,
|
|
662
752
|
metrics_enabled=resolved_metrics_enabled,
|
|
663
753
|
metrics_export_interval_millis=resolved_metrics_interval,
|
|
754
|
+
metric_cardinality_limit=resolved_metric_cardinality_limit,
|
|
755
|
+
service_instance_id=resolved_service_instance_id,
|
|
664
756
|
redis_mask_endpoint=resolved_redis_mask_endpoint,
|
|
665
757
|
redis_mask_command=resolved_redis_mask_command,
|
|
666
758
|
redis_cmd_max_length=resolved_cmd_max_length,
|
|
667
759
|
query_mode=resolved_query_mode,
|
|
760
|
+
n_plus_one_duplicate_threshold=resolved_n_plus_one_dup,
|
|
761
|
+
n_plus_one_max_fingerprints=resolved_n_plus_one_max_fp,
|
|
668
762
|
)
|
|
@@ -29,6 +29,14 @@ class BaseIntegration(abc.ABC):
|
|
|
29
29
|
self._wrapped_keys: set = set()
|
|
30
30
|
self._when_imported_hooks: List[Tuple[str, Callable]] = []
|
|
31
31
|
|
|
32
|
+
@property
|
|
33
|
+
def config(self) -> Optional[SDKConfig]:
|
|
34
|
+
return self._config
|
|
35
|
+
|
|
36
|
+
@config.setter
|
|
37
|
+
def config(self, value: Optional[SDKConfig]) -> None:
|
|
38
|
+
self._config = value
|
|
39
|
+
|
|
32
40
|
@abc.abstractmethod
|
|
33
41
|
def is_installed(self) -> bool:
|
|
34
42
|
"""Check if target library or framework is installed in the current environment."""
|
|
@@ -94,8 +102,10 @@ class BaseIntegration(abc.ABC):
|
|
|
94
102
|
|
|
95
103
|
def unwrap_all(self) -> None:
|
|
96
104
|
"""Restores all wrapped targets to their original unwrapped states in reverse order."""
|
|
105
|
+
failed_targets = []
|
|
97
106
|
while self._wrapped_targets:
|
|
98
107
|
target_obj, attr_name, original = self._wrapped_targets.pop()
|
|
108
|
+
key = (id(target_obj), attr_name)
|
|
99
109
|
try:
|
|
100
110
|
current = getattr(target_obj, attr_name, None)
|
|
101
111
|
if current is not None and hasattr(current, "__wrapped__"):
|
|
@@ -104,6 +114,7 @@ class BaseIntegration(abc.ABC):
|
|
|
104
114
|
setattr(target_obj, attr_name, original)
|
|
105
115
|
elif hasattr(target_obj, attr_name):
|
|
106
116
|
delattr(target_obj, attr_name)
|
|
117
|
+
self._wrapped_keys.discard(key)
|
|
107
118
|
except Exception as exc:
|
|
108
119
|
logger.debug(
|
|
109
120
|
"Error unwrapping %s on %s in %s integration: %s",
|
|
@@ -112,7 +123,10 @@ class BaseIntegration(abc.ABC):
|
|
|
112
123
|
self.name,
|
|
113
124
|
exc,
|
|
114
125
|
)
|
|
115
|
-
|
|
126
|
+
failed_targets.append((target_obj, attr_name, original))
|
|
127
|
+
|
|
128
|
+
if failed_targets:
|
|
129
|
+
self._wrapped_targets.extend(failed_targets)
|
|
116
130
|
|
|
117
131
|
def when_imported(self, module_name: str, callback: Callable[..., Any]) -> None:
|
|
118
132
|
"""
|
|
@@ -166,6 +180,13 @@ class BaseIntegration(abc.ABC):
|
|
|
166
180
|
return True
|
|
167
181
|
try:
|
|
168
182
|
self._remove_patch()
|
|
183
|
+
if self._wrapped_targets:
|
|
184
|
+
logger.debug(
|
|
185
|
+
"Failed to completely uninstrument %s; %d targets remained wrapped",
|
|
186
|
+
self.name,
|
|
187
|
+
len(self._wrapped_targets),
|
|
188
|
+
)
|
|
189
|
+
return False
|
|
169
190
|
self._instrumented = False
|
|
170
191
|
logger.debug("Successfully uninstrumented integration: %s", self.name)
|
|
171
192
|
return True
|
|
@@ -12,7 +12,7 @@ from typing import FrozenSet, Iterable, NamedTuple, Optional
|
|
|
12
12
|
class CeleryCreationMeta(NamedTuple):
|
|
13
13
|
action: str # "apply_async" or "run"
|
|
14
14
|
bounded_operation: str # "celery.apply_async.<admitted_name or unknown>"
|
|
15
|
-
span_name: str # "🟣
|
|
15
|
+
span_name: str # "🟣 <task_name>.apply_async" or "🟣 <task_name>.run" (fallback generic)
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
_CURRENT_CELERY_CREATION_META: contextvars.ContextVar[Optional[CeleryCreationMeta]] = contextvars.ContextVar(
|
|
@@ -64,10 +64,11 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
64
64
|
task_name = getattr(task, "name", None)
|
|
65
65
|
|
|
66
66
|
bounded_op = compute_bounded_operation("run", task_name, registered=task is not None)
|
|
67
|
+
span_name = f"🟣 {task_name}.run" if task_name and task_name != "unknown" else "🟣 celery.run"
|
|
67
68
|
meta = CeleryCreationMeta(
|
|
68
69
|
action="run",
|
|
69
70
|
bounded_operation=bounded_op,
|
|
70
|
-
span_name=
|
|
71
|
+
span_name=span_name,
|
|
71
72
|
)
|
|
72
73
|
|
|
73
74
|
if task is None or task_id is None:
|
|
@@ -76,6 +77,26 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
76
77
|
# creation. Keep our own restoration token for failures before that point.
|
|
77
78
|
restore_token = context_api.attach(context_api.get_current())
|
|
78
79
|
token = set_celery_creation_meta(meta)
|
|
80
|
+
|
|
81
|
+
# Defensive propagation support: Celery stores incoming message headers in task.request.headers.
|
|
82
|
+
# Upstream CeleryGetter does getattr(task.request, key). Ensure W3C propagation attributes
|
|
83
|
+
# on task.request are populated from task.request.headers if missing.
|
|
84
|
+
try:
|
|
85
|
+
req = getattr(task, "request", None)
|
|
86
|
+
if req is not None:
|
|
87
|
+
req_headers = getattr(req, "headers", None)
|
|
88
|
+
if req_headers is None and isinstance(req, dict):
|
|
89
|
+
req_headers = req.get("headers")
|
|
90
|
+
if isinstance(req_headers, dict):
|
|
91
|
+
for k in ("traceparent", "tracestate", "baggage"):
|
|
92
|
+
if k in req_headers and not hasattr(req, k):
|
|
93
|
+
try:
|
|
94
|
+
setattr(req, k, req_headers[k])
|
|
95
|
+
except Exception:
|
|
96
|
+
pass
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
|
|
79
100
|
try:
|
|
80
101
|
try:
|
|
81
102
|
super()._trace_prerun(*args, **kwargs)
|
|
@@ -109,7 +130,7 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
109
130
|
if ctx is not None:
|
|
110
131
|
span, _, _ = ctx
|
|
111
132
|
if span is not None and getattr(span, "is_recording", lambda: True)():
|
|
112
|
-
safe_update_name(span,
|
|
133
|
+
safe_update_name(span, span_name)
|
|
113
134
|
safe_set_attribute(span, "celery.task_id", task_id)
|
|
114
135
|
if task_name:
|
|
115
136
|
safe_set_attribute(span, "celery.task_name", task_name)
|
|
@@ -126,6 +147,24 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
126
147
|
rk = delivery_info.get("routing_key")
|
|
127
148
|
if rk:
|
|
128
149
|
safe_set_attribute(span, "celery.queue", str(rk))
|
|
150
|
+
req_headers = getattr(req, "headers", None)
|
|
151
|
+
if req_headers is None and isinstance(req, dict):
|
|
152
|
+
req_headers = req.get("headers")
|
|
153
|
+
trigger_view = None
|
|
154
|
+
if isinstance(req_headers, dict):
|
|
155
|
+
trigger_view = req_headers.get("tp_trigger_view")
|
|
156
|
+
if not trigger_view:
|
|
157
|
+
# Fallback for tasks executed eagerly in-process within the same thread
|
|
158
|
+
# (e.g. task_always_eager=True in tests or synchronous workflows),
|
|
159
|
+
# where task request headers may not have traversed amqp serialization.
|
|
160
|
+
try:
|
|
161
|
+
from tp_trace.route_context import get_current_view
|
|
162
|
+
|
|
163
|
+
trigger_view = get_current_view()
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
if trigger_view:
|
|
167
|
+
safe_set_attribute(span, "celery.trigger.view", str(trigger_view))
|
|
129
168
|
except Exception:
|
|
130
169
|
logger.debug("Error during _trace_prerun enrichment", exc_info=True)
|
|
131
170
|
|
|
@@ -141,15 +180,32 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
141
180
|
elif hasattr(sender, "name"):
|
|
142
181
|
task_name = getattr(sender, "name")
|
|
143
182
|
else:
|
|
144
|
-
|
|
183
|
+
headers = kwargs.get("headers")
|
|
184
|
+
if isinstance(headers, dict) and headers.get("task"):
|
|
185
|
+
task_name = str(headers.get("task"))
|
|
186
|
+
else:
|
|
187
|
+
task_name = "unknown"
|
|
145
188
|
|
|
146
189
|
bounded_op = compute_bounded_operation("apply_async", task_name, registered=task is not None)
|
|
190
|
+
span_name = f"🟣 {task_name}.apply_async" if task_name and task_name != "unknown" else "🟣 celery.apply_async"
|
|
147
191
|
meta = CeleryCreationMeta(
|
|
148
192
|
action="apply_async",
|
|
149
193
|
bounded_operation=bounded_op,
|
|
150
|
-
span_name=
|
|
194
|
+
span_name=span_name,
|
|
151
195
|
)
|
|
152
196
|
|
|
197
|
+
trigger_view = None
|
|
198
|
+
try:
|
|
199
|
+
from tp_trace.route_context import get_current_view
|
|
200
|
+
|
|
201
|
+
trigger_view = get_current_view()
|
|
202
|
+
except Exception:
|
|
203
|
+
pass
|
|
204
|
+
|
|
205
|
+
headers = kwargs.get("headers")
|
|
206
|
+
if isinstance(headers, dict) and trigger_view:
|
|
207
|
+
headers["tp_trigger_view"] = trigger_view
|
|
208
|
+
|
|
153
209
|
token = set_celery_creation_meta(meta)
|
|
154
210
|
try:
|
|
155
211
|
try:
|
|
@@ -160,6 +216,16 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
160
216
|
logger.debug("Upstream _trace_before_publish failed", exc_info=True)
|
|
161
217
|
return
|
|
162
218
|
|
|
219
|
+
# Defensive propagation injection: ensure trace context is injected even if headers dict
|
|
220
|
+
# was initially empty (upstream checks `if headers: inject(headers)`, skipping empty dicts).
|
|
221
|
+
try:
|
|
222
|
+
if isinstance(headers, dict) and "traceparent" not in headers:
|
|
223
|
+
from opentelemetry.propagate import inject
|
|
224
|
+
|
|
225
|
+
inject(headers)
|
|
226
|
+
except Exception:
|
|
227
|
+
pass
|
|
228
|
+
|
|
163
229
|
# Only enrich a verified producer from upstream's context store.
|
|
164
230
|
# Unknown-task producers can already have ended; the ambient span then
|
|
165
231
|
# belongs to the application and must never be enriched or ended here.
|
|
@@ -168,9 +234,11 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
168
234
|
ctx = utils.retrieve_context(task, task_id, is_publish=True)
|
|
169
235
|
span = ctx[0] if ctx is not None else None
|
|
170
236
|
if span is not None and getattr(span, "is_recording", lambda: True)():
|
|
171
|
-
safe_update_name(span,
|
|
237
|
+
safe_update_name(span, span_name)
|
|
172
238
|
safe_set_attribute(span, "celery.task_id", task_id)
|
|
173
239
|
safe_set_attribute(span, "celery.task_name", task_name)
|
|
240
|
+
if trigger_view:
|
|
241
|
+
safe_set_attribute(span, "celery.trigger.view", trigger_view)
|
|
174
242
|
|
|
175
243
|
# Extract producer queue
|
|
176
244
|
queue_name = None
|
|
@@ -191,13 +259,19 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
191
259
|
logger.debug("Error during _trace_before_publish enrichment", exc_info=True)
|
|
192
260
|
|
|
193
261
|
def _trace_after_publish(self, *args: Any, **kwargs: Any) -> None: # type: ignore[override]
|
|
194
|
-
|
|
195
|
-
|
|
262
|
+
try:
|
|
263
|
+
task = utils.retrieve_task_from_sender(kwargs)
|
|
264
|
+
task_id = utils.retrieve_task_id_from_message(kwargs)
|
|
265
|
+
ctx = utils.retrieve_context(task, task_id, is_publish=True) if (task and task_id) else None
|
|
266
|
+
except Exception:
|
|
267
|
+
task = None
|
|
268
|
+
task_id = None
|
|
269
|
+
ctx = None
|
|
196
270
|
|
|
197
|
-
ctx = utils.retrieve_context(task, task_id, is_publish=True) if (task and task_id) else None
|
|
198
271
|
try:
|
|
199
272
|
super()._trace_after_publish(*args, **kwargs)
|
|
200
|
-
except Exception:
|
|
273
|
+
except Exception as exc:
|
|
274
|
+
logger.debug("Upstream _trace_after_publish failed: %s", exc, exc_info=True)
|
|
201
275
|
if ctx is not None:
|
|
202
276
|
_, activation, _ = ctx
|
|
203
277
|
try:
|
|
@@ -206,7 +280,6 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
206
280
|
pass
|
|
207
281
|
if task and task_id:
|
|
208
282
|
utils.detach_context(task, task_id, is_publish=True)
|
|
209
|
-
raise
|
|
210
283
|
|
|
211
284
|
def _trace_failure(self, *args: Any, **kwargs: Any) -> None: # type: ignore[override]
|
|
212
285
|
upstream_failed = False
|
|
@@ -325,7 +398,8 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
325
398
|
try:
|
|
326
399
|
try:
|
|
327
400
|
super()._trace_postrun(*args, **kwargs)
|
|
328
|
-
except Exception:
|
|
401
|
+
except Exception as exc:
|
|
402
|
+
logger.debug("Upstream _trace_postrun failed: %s", exc, exc_info=True)
|
|
329
403
|
# Upstream finalization failed: best-effort cleanup
|
|
330
404
|
if ctx is not None:
|
|
331
405
|
span, activation, token = ctx
|
|
@@ -345,11 +419,13 @@ class TpTraceCeleryInstrumentor(CeleryInstrumentor):
|
|
|
345
419
|
context_api.detach(token) # type: ignore[arg-type]
|
|
346
420
|
except Exception:
|
|
347
421
|
pass
|
|
348
|
-
raise
|
|
349
422
|
finally:
|
|
350
423
|
restore_token = self._prerun_context_tokens.pop(task_id, None) if task_id else None
|
|
351
424
|
if restore_token is not None:
|
|
352
|
-
|
|
425
|
+
try:
|
|
426
|
+
context_api.detach(restore_token)
|
|
427
|
+
except Exception:
|
|
428
|
+
pass
|
|
353
429
|
# Defensive timing map cleanup against leaks across all upstream versions
|
|
354
430
|
if hasattr(self, "task_id_to_start_time") and isinstance(self.task_id_to_start_time, dict):
|
|
355
431
|
if task_id:
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Django integration package — re-exports DjangoIntegration.
|
|
2
|
+
|
|
3
|
+
Layout follows:
|
|
4
|
+
integrations/django/
|
|
5
|
+
__init__.py — re-export
|
|
6
|
+
integration.py — DjangoIntegration orchestration
|
|
7
|
+
request.py — SERVER request hooks and OTel middleware enhancement
|
|
8
|
+
middleware.py — load_middleware wrapper
|
|
9
|
+
view.py — view span
|
|
10
|
+
template.py — template span
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .integration import DjangoIntegration
|
|
14
|
+
|
|
15
|
+
__all__ = ["DjangoIntegration"]
|
|
@@ -145,10 +145,13 @@ def make_traced_cache_op(op_name: str):
|
|
|
145
145
|
pass
|
|
146
146
|
|
|
147
147
|
if key is not None:
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
try:
|
|
149
|
+
if isinstance(key, (list, tuple, set)):
|
|
150
|
+
span_attrs["django.cache.key"] = ", ".join(str(k) for k in key)
|
|
151
|
+
else:
|
|
152
|
+
span_attrs["django.cache.key"] = str(key)
|
|
153
|
+
except Exception:
|
|
154
|
+
pass
|
|
152
155
|
|
|
153
156
|
token = _in_cache_span.set(True)
|
|
154
157
|
try:
|