1212)
1313from opentelemetry .sdk .resources import Resource
1414from opentelemetry .sdk .trace import TracerProvider
15- from opentelemetry .sdk .trace .export import BatchSpanProcessor
16- from opentelemetry .trace import ProxyTracerProvider , get_current_span , get_tracer_provider , set_tracer_provider
15+ from opentelemetry .sdk .trace .export import BatchSpanProcessor , SpanProcessor
1716from opentelemetry .trace import Span as OTSpan
17+ from opentelemetry .trace import get_current_span
1818from opentelemetry .trace .propagation .tracecontext import TraceContextTextMapPropagator
1919
2020from tilebox .workflows .data import Job
3030_OTEL_TRACES_ENDPOINT_ENV_VAR = "OTEL_TRACES_ENDPOINT"
3131_OTEL_EXPORT_INTERVAL_ENV_VAR = "OTEL_EXPORT_INTERVAL"
3232
33+ # the globally configured tilebox opentelemetry tracer provider.
34+ # we explicitly avoid using the global opentelemetry tracer provider, because other libraries might also configure
35+ # that (e.g. pytorch does sometimes), and we don't want to interfere with that.
36+ # as default we don't use a proxy tracer provider, that only returns no-op tracers, because we still want to be able
37+ # to extract trace_ids and spans, in case other runners / workflow clients have tracing configured.
38+ # So instead we use a tracer provider without any exporters, which will still create traces and spans,
39+ # but will not send them anywhere.
40+ _tilebox_tracer_provider = TracerProvider ()
41+ _workflow_tracers = []
42+
43+
44+ def _get_tilebox_tracer_provider () -> TracerProvider :
45+ return _tilebox_tracer_provider
46+
47+
48+ def _set_tilebox_tracer_provider (provider : TracerProvider ) -> None :
49+ global _tilebox_tracer_provider # noqa: PLW0603
50+ _tilebox_tracer_provider = provider
51+
52+ for tracer in _workflow_tracers :
53+ tracer ._swap_provider (provider ) # noqa: SLF001
54+
3355
3456class WorkflowTracer :
3557 def __init__ (self ) -> None :
3658 """Instantiate a tracer from the currently configured global tracer provider."""
37- provider = get_tracer_provider ()
38- if isinstance (provider , ProxyTracerProvider ):
39- # if no tracer provider is configured, we still don't want to use the no-op tracer, because we
40- # want to be able to extract trace_ids and spans, in case other runners / workflow clients have tracing
41- # configured. So instead we use a tracer provider without any exporters, which will still create traces
42- # and spans, but will not send them anywhere.
43- provider = TracerProvider ()
59+ provider = _get_tilebox_tracer_provider ()
60+ self ._tracer = provider .get_tracer (_INSTRUMENTATION_MODULE_NAME )
61+
62+ # keep track of all workflow tracers, to be able to update them in case the
63+ # global tracer provider is replaced.
64+ _workflow_tracers .append (self )
65+
66+ def _swap_provider (self , provider : TracerProvider ) -> None :
67+ """
68+ A callback function that get's invoked in case a new tracer provider is configured, to make sure
69+ existing workflow tracers are updated to use the new provider.
70+ """
4471 self ._tracer = provider .get_tracer (_INSTRUMENTATION_MODULE_NAME )
4572
4673 # functools.wraps is a bit buggy with class methods, so we are not using it here
@@ -62,16 +89,11 @@ def get_trace_parent_of_current_span() -> str:
6289 return carrier ["traceparent" ]
6390
6491
65- def _otel_tracer_provider (
66- service : str | Resource | None = None ,
92+ def _otel_span_exporter (
6793 endpoint : str | None = None ,
6894 headers : dict [str , str ] | None = None ,
6995 export_interval : timedelta | None = None ,
70- ) -> TracerProvider :
71- resource = _get_default_resource (service )
72-
73- provider = TracerProvider (resource = resource )
74-
96+ ) -> SpanProcessor :
7597 if endpoint is None :
7698 endpoint = os .environ .get (_OTEL_TRACES_ENDPOINT_ENV_VAR , None )
7799 if endpoint is None :
@@ -94,11 +116,7 @@ def _otel_tracer_provider(
94116 headers = headers ,
95117 )
96118 schedule_delay = int (export_interval .total_seconds () * 1000 ) if export_interval is not None else None
97- batch_processor = BatchSpanProcessor (exporter , schedule_delay_millis = schedule_delay ) # type: ignore[arg-type]
98-
99- provider .add_span_processor (batch_processor )
100-
101- return provider
119+ return BatchSpanProcessor (exporter , schedule_delay_millis = schedule_delay ) # type: ignore[arg-type]
102120
103121
104122class SpanEventLoggingHandler (logging .Handler ):
@@ -156,8 +174,20 @@ def configure_otel_tracing(
156174 Raises:
157175 ValueError: If no endpoint is provided and no OTEL_TRACES_ENDPOINT environment variable is set.
158176 """
159- provider = _otel_tracer_provider (service , endpoint , headers , export_interval )
160- set_tracer_provider (provider )
177+ provider = _get_tilebox_tracer_provider ()
178+ resource = _get_default_resource (service )
179+ if provider .resource .attributes != resource .attributes :
180+ # It's either the first time we configure tracing, or we are trying to reconfigure it with a different resource.
181+ # That means we need to create a new provider.
182+ provider = TracerProvider (
183+ resource = resource ,
184+ # keep the existing span processor, so that all previously configured exports are still used as well
185+ active_span_processor = provider ._active_span_processor , # noqa: SLF001
186+ )
187+ _set_tilebox_tracer_provider (provider )
188+
189+ exporter = _otel_span_exporter (endpoint , headers , export_interval )
190+ provider .add_span_processor (exporter )
161191
162192 # if we configure tracing, we also want to add log messages to active spans, which is a mixture of a logging
163193 # tracing feature. But configure this here, because we anyways don't need to do this if tracing is not configured.
0 commit comments