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.
- tp_trace/__init__.py +377 -0
- tp_trace/config.py +662 -0
- tp_trace/config_access.py +29 -0
- tp_trace/exporter.py +224 -0
- tp_trace/integrations/__init__.py +20 -0
- tp_trace/integrations/base.py +174 -0
- tp_trace/integrations/boto/__init__.py +5 -0
- tp_trace/integrations/boto/integration.py +79 -0
- tp_trace/integrations/celery/__init__.py +5 -0
- tp_trace/integrations/celery/context.py +109 -0
- tp_trace/integrations/celery/instrumentor.py +378 -0
- tp_trace/integrations/celery/integration.py +232 -0
- tp_trace/integrations/django/__init__.py +16 -0
- tp_trace/integrations/django/auth.py +51 -0
- tp_trace/integrations/django/cache.py +163 -0
- tp_trace/integrations/django/compat.py +49 -0
- tp_trace/integrations/django/integration.py +249 -0
- tp_trace/integrations/django/middleware.py +225 -0
- tp_trace/integrations/django/middleware_init.py +96 -0
- tp_trace/integrations/django/request.py +658 -0
- tp_trace/integrations/django/template.py +204 -0
- tp_trace/integrations/django/view.py +294 -0
- tp_trace/integrations/manager.py +177 -0
- tp_trace/integrations/postgres/__init__.py +12 -0
- tp_trace/integrations/postgres/cursor.py +269 -0
- tp_trace/integrations/postgres/integration.py +219 -0
- tp_trace/integrations/redis/__init__.py +5 -0
- tp_trace/integrations/redis/integration.py +312 -0
- tp_trace/integrations/requests/__init__.py +5 -0
- tp_trace/integrations/requests/client.py +126 -0
- tp_trace/integrations/requests/integration.py +49 -0
- tp_trace/metrics.py +266 -0
- tp_trace/route_context.py +293 -0
- tp_trace/safety.py +159 -0
- tp_trace/sampler.py +156 -0
- tp_trace/sanitize.py +189 -0
- tp_trace/tracing.py +206 -0
- tp_trace/version.py +3 -0
- tp_trace-0.1.5.dist-info/METADATA +64 -0
- tp_trace-0.1.5.dist-info/RECORD +42 -0
- tp_trace-0.1.5.dist-info/WHEEL +5 -0
- 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
|
+
]
|