tp-trace 0.1.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.

Potentially problematic release.


This version of tp-trace might be problematic. Click here for more details.

Files changed (42) hide show
  1. tp_trace/__init__.py +377 -0
  2. tp_trace/config.py +662 -0
  3. tp_trace/config_access.py +29 -0
  4. tp_trace/exporter.py +224 -0
  5. tp_trace/integrations/__init__.py +20 -0
  6. tp_trace/integrations/base.py +174 -0
  7. tp_trace/integrations/boto/__init__.py +5 -0
  8. tp_trace/integrations/boto/integration.py +79 -0
  9. tp_trace/integrations/celery/__init__.py +5 -0
  10. tp_trace/integrations/celery/context.py +109 -0
  11. tp_trace/integrations/celery/instrumentor.py +378 -0
  12. tp_trace/integrations/celery/integration.py +232 -0
  13. tp_trace/integrations/django/__init__.py +16 -0
  14. tp_trace/integrations/django/auth.py +51 -0
  15. tp_trace/integrations/django/cache.py +163 -0
  16. tp_trace/integrations/django/compat.py +49 -0
  17. tp_trace/integrations/django/integration.py +249 -0
  18. tp_trace/integrations/django/middleware.py +225 -0
  19. tp_trace/integrations/django/middleware_init.py +96 -0
  20. tp_trace/integrations/django/request.py +658 -0
  21. tp_trace/integrations/django/template.py +204 -0
  22. tp_trace/integrations/django/view.py +294 -0
  23. tp_trace/integrations/manager.py +177 -0
  24. tp_trace/integrations/postgres/__init__.py +12 -0
  25. tp_trace/integrations/postgres/cursor.py +269 -0
  26. tp_trace/integrations/postgres/integration.py +219 -0
  27. tp_trace/integrations/redis/__init__.py +5 -0
  28. tp_trace/integrations/redis/integration.py +312 -0
  29. tp_trace/integrations/requests/__init__.py +5 -0
  30. tp_trace/integrations/requests/client.py +126 -0
  31. tp_trace/integrations/requests/integration.py +49 -0
  32. tp_trace/metrics.py +266 -0
  33. tp_trace/route_context.py +293 -0
  34. tp_trace/safety.py +159 -0
  35. tp_trace/sampler.py +156 -0
  36. tp_trace/sanitize.py +189 -0
  37. tp_trace/tracing.py +206 -0
  38. tp_trace/version.py +3 -0
  39. tp_trace-0.1.5.dist-info/METADATA +64 -0
  40. tp_trace-0.1.5.dist-info/RECORD +42 -0
  41. tp_trace-0.1.5.dist-info/WHEEL +5 -0
  42. tp_trace-0.1.5.dist-info/top_level.txt +1 -0
tp_trace/__init__.py ADDED
@@ -0,0 +1,377 @@
1
+ """
2
+ tp_trace: Python Observability SDK for OpenTelemetry.
3
+
4
+ Provides complete distributed tracing and request waterfall instrumentation for Django and PostgreSQL.
5
+ """
6
+
7
+ import atexit
8
+ import logging
9
+ import threading
10
+ from typing import Any, Dict, List, Optional
11
+
12
+ from opentelemetry import trace
13
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
14
+ from opentelemetry.sdk.resources import Resource
15
+ from opentelemetry.sdk.trace import TracerProvider, SpanProcessor
16
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor, SimpleSpanProcessor, SpanExporter
17
+ from opentelemetry.sdk.trace.sampling import ALWAYS_OFF, Sampler
18
+ from opentelemetry.trace import get_current_span, get_tracer
19
+ from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator
20
+ from opentelemetry.baggage.propagation import W3CBaggagePropagator
21
+ from opentelemetry.propagators.composite import CompositePropagator
22
+ from opentelemetry.propagate import set_global_textmap
23
+
24
+ from tp_trace.config import SDKConfig
25
+ from tp_trace.exporter import SafeSpanExporter
26
+ from tp_trace.route_context import RouteEnrichingSpanProcessor
27
+ from tp_trace.integrations import BaseIntegration, get_integration_manager
28
+ from tp_trace.sanitize import sanitize_query_string, sanitize_sql, sanitize_url
29
+ from tp_trace.tracing import traced_span
30
+ from tp_trace.version import __version__
31
+
32
+ __all__ = [
33
+ "init",
34
+ "get_config",
35
+ "patch_all",
36
+ "traced_span",
37
+ "sanitize_query_string",
38
+ "sanitize_sql",
39
+ "sanitize_url",
40
+ "__version__",
41
+ "SDKConfig",
42
+ "BaseIntegration",
43
+ "get_integration_manager",
44
+ "RouteEnrichingSpanProcessor",
45
+ "SafeSpanExporter",
46
+ ]
47
+
48
+ logger = logging.getLogger("tp_trace")
49
+
50
+ _INITIALIZED = False
51
+ _INIT_LOCK = threading.Lock()
52
+ _ACTIVE_PROVIDER: Optional[TracerProvider] = None
53
+ _ACTIVE_CONFIG: Optional[SDKConfig] = None
54
+ _ATEXIT_REGISTERED = False
55
+
56
+
57
+ def init(
58
+ project: Optional[str] = None,
59
+ project_name: Optional[str] = None,
60
+ cluster_name: Optional[str] = None,
61
+ environment: Optional[str] = None,
62
+ version: Optional[str] = None,
63
+ endpoint: Optional[str] = None,
64
+ traces_endpoint: Optional[str] = None,
65
+ headers: Optional[Dict[str, str]] = None,
66
+ sample_rate: Optional[float] = None,
67
+ disabled: Optional[bool] = None,
68
+ debug: Optional[bool] = None,
69
+ resource_attributes: Optional[Dict[str, Any]] = None,
70
+ integrations: Optional[Dict[str, bool]] = None,
71
+ exporter: Optional[SpanExporter] = None,
72
+ span_processor: Optional[SpanProcessor] = None,
73
+ export_batch: bool = True,
74
+ auto_patch: bool = True,
75
+ extract_trace_context: Optional[Any] = None,
76
+ ignore_endpoints: Optional[List[str]] = None,
77
+ endpoint_sample_rules: Optional[Dict[str, float]] = None,
78
+ metric_readers: Optional[List[Any]] = None,
79
+ **kwargs: Any,
80
+ ) -> TracerProvider:
81
+ """
82
+ Initialize the tp_trace OpenTelemetry SDK.
83
+
84
+ Configures the global TracerProvider, W3C context propagation,
85
+ and OTLP HTTP trace exporter. Thread-safe and idempotent.
86
+
87
+ By default, auto_patch=True so all installed integrations (Django,
88
+ PostgreSQL, Redis, requests) are automatically detected and patched.
89
+ A single call in your Django settings.py is all you need:
90
+
91
+ import tp_trace
92
+ tp_trace.init(project_name="my-service")
93
+
94
+ The project name is auto-detected from Django settings if not provided.
95
+ The OTLP endpoint defaults to OTEL_EXPORTER_OTLP_ENDPOINT env var,
96
+ then http://localhost:4318.
97
+
98
+ Resource attributes follow OTel semantic conventions:
99
+ service.name (= the project), service.version, deployment.environment.name, service.instance.id
100
+ In tp_trace's own vocabulary the application is the "project" and a running
101
+ component such as PostgreSQL or Redis is a "service".
102
+ """
103
+ global _INITIALIZED, _ACTIVE_PROVIDER, _ACTIVE_CONFIG
104
+
105
+ with _INIT_LOCK:
106
+ if _INITIALIZED and _ACTIVE_PROVIDER is not None:
107
+ logger.debug("tp_trace is already initialized. Returning existing provider.")
108
+ return _ACTIVE_PROVIDER
109
+
110
+ try:
111
+ config = SDKConfig.from_env_and_kwargs(
112
+ project=project,
113
+ cluster_name=cluster_name,
114
+ project_name=project_name,
115
+ environment=environment,
116
+ version=version,
117
+ endpoint=endpoint,
118
+ traces_endpoint=traces_endpoint,
119
+ headers=headers,
120
+ sample_rate=sample_rate,
121
+ disabled=disabled,
122
+ debug=debug,
123
+ resource_attributes=resource_attributes,
124
+ integrations=integrations,
125
+ auto_patch=auto_patch,
126
+ extract_trace_context=extract_trace_context,
127
+ ignore_endpoints=ignore_endpoints,
128
+ endpoint_sample_rules=endpoint_sample_rules,
129
+ **kwargs,
130
+ )
131
+ except Exception as exc:
132
+ logger.error(
133
+ "tp_trace: Invalid configuration detected (%s: %s). "
134
+ "Falling back to default safe configuration to prevent host application crash.",
135
+ type(exc).__name__,
136
+ exc,
137
+ )
138
+ config = SDKConfig()
139
+ _ACTIVE_CONFIG = config
140
+
141
+ if config.debug:
142
+ logger.setLevel(logging.DEBUG)
143
+
144
+ # Build Resource attributes
145
+ resource_data = {
146
+ "service.name": config.project_name,
147
+ "project_name": config.project_name,
148
+ "telemetry.sdk.name": "tp_trace",
149
+ "telemetry.sdk.language": "python",
150
+ "telemetry.sdk.version": __version__,
151
+ }
152
+ if config.cluster_name is not None:
153
+ resource_data["cluster_name"] = str(config.cluster_name)
154
+ if config.environment is not None:
155
+ resource_data["deployment.environment.name"] = str(config.environment)
156
+ resource_data["deployment.environment"] = str(config.environment)
157
+ if config.version is not None:
158
+ resource_data["service.version"] = str(config.version)
159
+ if config.tags:
160
+ resource_data.update(config.tags)
161
+ if config.resource_attributes:
162
+ resource_data.update(config.resource_attributes)
163
+ try:
164
+ import socket
165
+ import os
166
+ resource_data.setdefault("service.instance.id", f"{socket.gethostname()}-{os.getpid()}")
167
+ except Exception:
168
+ pass
169
+ resource = Resource.create(resource_data)
170
+
171
+ # Configure Sampler
172
+ sampler: Sampler
173
+ if config.disabled:
174
+ sampler = ALWAYS_OFF
175
+ else:
176
+ from tp_trace.sampler import create_tp_trace_sampler
177
+ sampler = create_tp_trace_sampler(
178
+ global_sample_rate=config.sample_rate,
179
+ ignore_endpoints=config.ignore_endpoints,
180
+ endpoint_sample_rules=config.endpoint_sample_rules,
181
+ )
182
+
183
+ # Create TracerProvider
184
+ provider = TracerProvider(resource=resource, sampler=sampler)
185
+
186
+ # Enrich spans with the in-flight Django http.route and global tags
187
+ static_span_attrs = {
188
+ "project_name": config.project_name,
189
+ "cluster_name": config.cluster_name,
190
+ }
191
+ if config.tags:
192
+ static_span_attrs.update(config.tags)
193
+
194
+ try:
195
+ provider.add_span_processor(RouteEnrichingSpanProcessor(static_attributes=static_span_attrs))
196
+ except Exception as exc:
197
+ logger.debug("tp_trace: Failed to register RouteEnrichingSpanProcessor: %s", exc)
198
+
199
+ # Configure Exporter and Processor if not disabled
200
+ if not config.disabled:
201
+ if exporter is None:
202
+ if config.traces_endpoint:
203
+ otlp_endpoint = config.traces_endpoint
204
+ else:
205
+ base_ep = config.endpoint.rstrip("/")
206
+ otlp_endpoint = base_ep if base_ep.endswith("/v1/traces") else f"{base_ep}/v1/traces"
207
+
208
+ try:
209
+ exporter = OTLPSpanExporter(
210
+ endpoint=otlp_endpoint,
211
+ headers=config.headers or None,
212
+ )
213
+ except Exception as exc:
214
+ logger.warning("tp_trace: Failed to create OTLPSpanExporter: %s", exc)
215
+ exporter = None
216
+
217
+ if exporter is not None:
218
+ # Wrap with SafeSpanExporter: telemetry errors NEVER crash or disrupt the host app
219
+ safe_exporter = SafeSpanExporter(
220
+ exporter,
221
+ endpoint=otlp_endpoint if "otlp_endpoint" in locals() else None,
222
+ )
223
+
224
+ if span_processor is None:
225
+ if export_batch:
226
+ span_processor = BatchSpanProcessor(
227
+ safe_exporter,
228
+ max_queue_size=2048,
229
+ max_export_batch_size=256,
230
+ schedule_delay_millis=1000,
231
+ )
232
+ else:
233
+ span_processor = SimpleSpanProcessor(safe_exporter)
234
+
235
+ provider.add_span_processor(span_processor)
236
+
237
+ # Set as global tracer provider
238
+ trace.set_tracer_provider(provider)
239
+ _ACTIVE_PROVIDER = provider
240
+ _INITIALIZED = True
241
+
242
+ # Setup W3C Trace Context and Baggage composite propagator
243
+ try:
244
+ set_global_textmap(CompositePropagator([TraceContextTextMapPropagator(), W3CBaggagePropagator()]))
245
+ except Exception:
246
+ set_global_textmap(TraceContextTextMapPropagator())
247
+
248
+ # Auto-patch installed integrations if requested
249
+ if config.auto_patch and not config.disabled:
250
+ try:
251
+ manager = get_integration_manager()
252
+ manager.apply_integrations(config=config)
253
+ except Exception as exc:
254
+ logger.debug("Failed to auto-apply integrations during init: %s", exc)
255
+
256
+ # Initialize independent RED metrics recorder
257
+ try:
258
+ from tp_trace.metrics import init_metrics
259
+ init_metrics(config=config, metric_readers=metric_readers, resource=resource)
260
+ except Exception as exc:
261
+ logger.debug("tp_trace: Failed to initialize independent RED metrics: %s", exc)
262
+
263
+ # Register shutdown on process exit exactly once
264
+ global _ATEXIT_REGISTERED
265
+ if not _ATEXIT_REGISTERED:
266
+ def _shutdown():
267
+ global _ACTIVE_PROVIDER
268
+ try:
269
+ if _ACTIVE_PROVIDER is not None:
270
+ _ACTIVE_PROVIDER.shutdown()
271
+ except Exception:
272
+ pass
273
+ try:
274
+ from tp_trace.metrics import get_metrics_recorder
275
+ rec = get_metrics_recorder()
276
+ if rec is not None and rec.meter_provider is not None:
277
+ rec.meter_provider.shutdown()
278
+ except Exception:
279
+ pass
280
+
281
+ atexit.register(_shutdown)
282
+ _ATEXIT_REGISTERED = True
283
+
284
+ logger.info(
285
+ "tp_trace initialized successfully (project=%s, env=%s, endpoint=%s)",
286
+ config.project_name,
287
+ config.environment,
288
+ config.endpoint,
289
+ )
290
+
291
+ return provider
292
+
293
+
294
+ def get_config() -> SDKConfig:
295
+ """Get active SDKConfig or default configuration."""
296
+ if _ACTIVE_CONFIG is not None:
297
+ return _ACTIVE_CONFIG
298
+ return SDKConfig.from_env_and_kwargs()
299
+
300
+
301
+ def patch_all(**kwargs: Any) -> List[str]:
302
+ """Auto-detect and apply all installed integrations (Django, PostgreSQL, etc.)."""
303
+ manager = get_integration_manager()
304
+ return manager.apply_integrations(config=get_config(), **kwargs)
305
+
306
+
307
+ def _reset_for_testing() -> None:
308
+ """Internal helper to reset singleton state between unit tests."""
309
+ global _INITIALIZED, _ACTIVE_PROVIDER, _ACTIVE_CONFIG
310
+ with _INIT_LOCK:
311
+ if _ACTIVE_PROVIDER is not None:
312
+ try:
313
+ _ACTIVE_PROVIDER.shutdown()
314
+ except Exception:
315
+ pass
316
+ _INITIALIZED = False
317
+ _ACTIVE_PROVIDER = None
318
+ _ACTIVE_CONFIG = None
319
+ # Uninstrument all active integrations
320
+ try:
321
+ get_integration_manager().uninstrument_all()
322
+ except Exception:
323
+ pass
324
+ # Reset OpenTelemetry global state safely for testing
325
+ if hasattr(trace, "_TRACER_PROVIDER"):
326
+ trace._TRACER_PROVIDER = None # type: ignore
327
+ if hasattr(trace, "_TRACER_PROVIDER_SET_ONCE"):
328
+ try:
329
+ trace._TRACER_PROVIDER_SET_ONCE._done = False # type: ignore
330
+ except Exception:
331
+ pass
332
+ try:
333
+ from tp_trace.metrics import set_metrics_recorder
334
+ set_metrics_recorder(None)
335
+ except Exception:
336
+ pass
337
+ try:
338
+ from tp_trace.integrations.django.request import _CALLBACK_ARITY_CACHE
339
+ _CALLBACK_ARITY_CACHE.clear()
340
+ except Exception:
341
+ pass
342
+
343
+
344
+
345
+
346
+ def set_tag(key: str, value: Any) -> None:
347
+ """Set an attribute on the active span AND propagate it to all downstream child spans (DB, Redis, HTTP)."""
348
+ from tp_trace.route_context import set_request_tag
349
+ set_request_tag(key, value)
350
+ span = get_current_span()
351
+ if span and hasattr(span, "set_attribute"):
352
+ try:
353
+ span.set_attribute(key, value)
354
+ except Exception:
355
+ pass
356
+
357
+
358
+ def set_attribute(key: str, value: Any) -> None:
359
+ """Alias for set_tag."""
360
+ set_tag(key, value)
361
+
362
+ __all__ = [
363
+ "set_tag",
364
+ "set_attribute",
365
+ "init",
366
+ "patch_all",
367
+ "get_config",
368
+ "get_tracer",
369
+ "get_current_span",
370
+ "traced_span",
371
+ "get_integration_manager",
372
+ "BaseIntegration",
373
+ "SDKConfig",
374
+ "sanitize_url",
375
+ "sanitize_sql",
376
+ "__version__",
377
+ ]