Skip to content

Commit bdab2df

Browse files
committed
add TracerProvider creation from declarative config
Implements create_tracer_provider() and configure_tracer_provider() for the declarative configuration pipeline (tracking issue open-telemetry#3631 step 5). Key behaviors: - Never reads OTEL_TRACES_SAMPLER or OTEL_SPAN_*_LIMIT env vars; absent config fields use OTel spec defaults (matching Java SDK behavior) - Default sampler is ParentBased(root=ALWAYS_ON) per the OTel spec - SpanLimits absent fields use hardcoded defaults (128) not env vars - configure_tracer_provider(None) is a no-op per spec/Java/JS behavior - OTLP exporter fields pass None through so the exporter reads its own env vars for unspecified values - Lazy imports for optional OTLP packages with ConfigurationError on missing - Supports all 4 ParentBased delegate samplers Assisted-by: Claude Sonnet 4.6
1 parent 6ed3425 commit bdab2df

3 files changed

Lines changed: 810 additions & 0 deletions

File tree

Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import logging
18+
from typing import TYPE_CHECKING, Dict, Optional
19+
20+
from opentelemetry import trace
21+
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
22+
from opentelemetry.sdk._configuration.models import (
23+
BatchSpanProcessor as BatchSpanProcessorConfig,
24+
)
25+
from opentelemetry.sdk._configuration.models import (
26+
OtlpGrpcExporter as OtlpGrpcExporterConfig,
27+
)
28+
from opentelemetry.sdk._configuration.models import (
29+
OtlpHttpExporter as OtlpHttpExporterConfig,
30+
)
31+
from opentelemetry.sdk._configuration.models import (
32+
ParentBasedSampler as ParentBasedSamplerConfig,
33+
)
34+
from opentelemetry.sdk._configuration.models import (
35+
Sampler as SamplerConfig,
36+
)
37+
from opentelemetry.sdk._configuration.models import (
38+
SimpleSpanProcessor as SimpleSpanProcessorConfig,
39+
)
40+
from opentelemetry.sdk._configuration.models import (
41+
SpanExporter as SpanExporterConfig,
42+
)
43+
from opentelemetry.sdk._configuration.models import (
44+
SpanLimits as SpanLimitsConfig,
45+
)
46+
from opentelemetry.sdk._configuration.models import (
47+
SpanProcessor as SpanProcessorConfig,
48+
)
49+
from opentelemetry.sdk._configuration.models import (
50+
TracerProvider as TracerProviderConfig,
51+
)
52+
from opentelemetry.sdk.resources import Resource
53+
from opentelemetry.sdk.trace import (
54+
SpanLimits,
55+
TracerProvider,
56+
_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
57+
_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
58+
_DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
59+
_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT,
60+
_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT,
61+
)
62+
from opentelemetry.sdk.trace.sampling import (
63+
ALWAYS_OFF,
64+
ALWAYS_ON,
65+
ParentBased,
66+
Sampler,
67+
TraceIdRatioBased,
68+
)
69+
70+
if TYPE_CHECKING:
71+
from opentelemetry.sdk.trace.export import SpanExporter
72+
73+
_logger = logging.getLogger(__name__)
74+
75+
# Default sampler per the OTel spec: parent_based with always_on root.
76+
_DEFAULT_SAMPLER = ParentBased(root=ALWAYS_ON)
77+
78+
79+
def _parse_headers(
80+
headers: Optional[list],
81+
headers_list: Optional[str],
82+
) -> Optional[Dict[str, str]]:
83+
"""Merge headers struct and headers_list into a dict.
84+
85+
Returns None if neither is set, letting the exporter read env vars.
86+
headers struct takes priority over headers_list for the same key.
87+
"""
88+
if headers is None and headers_list is None:
89+
return None
90+
result: Dict[str, str] = {}
91+
if headers_list:
92+
for item in headers_list.split(","):
93+
item = item.strip()
94+
if "=" in item:
95+
key, value = item.split("=", 1)
96+
result[key.strip()] = value.strip()
97+
elif item:
98+
_logger.warning(
99+
"Invalid header pair in headers_list (missing '='): %s",
100+
item,
101+
)
102+
if headers:
103+
for pair in headers:
104+
result[pair.name] = pair.value or ""
105+
return result
106+
107+
108+
def _create_otlp_http_span_exporter(config: OtlpHttpExporterConfig) -> "SpanExporter":
109+
"""Create an OTLP HTTP span exporter from config."""
110+
try:
111+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( # type: ignore[import-untyped]
112+
OTLPSpanExporter,
113+
)
114+
from opentelemetry.exporter.otlp.proto.http import ( # type: ignore[import-untyped]
115+
Compression,
116+
)
117+
except ImportError as exc:
118+
raise ConfigurationError(
119+
"otlp_http span exporter requires 'opentelemetry-exporter-otlp-proto-http'. "
120+
"Install it with: pip install opentelemetry-exporter-otlp-proto-http"
121+
) from exc
122+
123+
compression = _map_compression_http(config.compression, Compression)
124+
headers = _parse_headers(config.headers, config.headers_list)
125+
timeout = (config.timeout / 1000.0) if config.timeout is not None else None
126+
127+
return OTLPSpanExporter(
128+
endpoint=config.endpoint,
129+
headers=headers,
130+
timeout=timeout,
131+
compression=compression,
132+
)
133+
134+
135+
def _map_compression_http(
136+
value: Optional[str], compression_enum: type
137+
) -> Optional[object]:
138+
"""Map a compression string to the HTTP Compression enum value."""
139+
if value is None or value.lower() == "none":
140+
return None
141+
if value.lower() == "gzip":
142+
return compression_enum.Gzip
143+
raise ConfigurationError(
144+
f"Unsupported compression value '{value}'. Supported values: 'gzip', 'none'."
145+
)
146+
147+
148+
def _create_otlp_grpc_span_exporter(config: OtlpGrpcExporterConfig) -> "SpanExporter":
149+
"""Create an OTLP gRPC span exporter from config."""
150+
try:
151+
import grpc # type: ignore[import-untyped]
152+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( # type: ignore[import-untyped]
153+
OTLPSpanExporter,
154+
)
155+
except ImportError as exc:
156+
raise ConfigurationError(
157+
"otlp_grpc span exporter requires 'opentelemetry-exporter-otlp-proto-grpc'. "
158+
"Install it with: pip install opentelemetry-exporter-otlp-proto-grpc"
159+
) from exc
160+
161+
compression = _map_compression_grpc(config.compression, grpc)
162+
headers = _parse_headers(config.headers, config.headers_list)
163+
timeout = (config.timeout / 1000.0) if config.timeout is not None else None
164+
165+
return OTLPSpanExporter(
166+
endpoint=config.endpoint,
167+
headers=headers,
168+
timeout=timeout,
169+
compression=compression,
170+
)
171+
172+
173+
def _map_compression_grpc(
174+
value: Optional[str], grpc_module: object
175+
) -> Optional[object]:
176+
"""Map a compression string to the gRPC Compression enum value."""
177+
if value is None or value.lower() == "none":
178+
return None
179+
if value.lower() == "gzip":
180+
return grpc_module.Compression.Gzip # type: ignore[attr-defined]
181+
raise ConfigurationError(
182+
f"Unsupported compression value '{value}'. Supported values: 'gzip', 'none'."
183+
)
184+
185+
186+
def _create_span_exporter(config: SpanExporterConfig) -> "SpanExporter":
187+
"""Create a span exporter from config."""
188+
if config.otlp_http is not None:
189+
return _create_otlp_http_span_exporter(config.otlp_http)
190+
if config.otlp_grpc is not None:
191+
return _create_otlp_grpc_span_exporter(config.otlp_grpc)
192+
if config.console is not None:
193+
from opentelemetry.sdk.trace.export import ConsoleSpanExporter
194+
195+
return ConsoleSpanExporter()
196+
raise ConfigurationError(
197+
"No exporter type specified in span exporter config. "
198+
"Supported types: otlp_http, otlp_grpc, console."
199+
)
200+
201+
202+
def _create_span_processor(config: SpanProcessorConfig) -> object:
203+
"""Create a span processor from config."""
204+
from opentelemetry.sdk.trace.export import (
205+
BatchSpanProcessor,
206+
SimpleSpanProcessor,
207+
)
208+
209+
if config.batch is not None:
210+
return _create_batch_span_processor(config.batch, BatchSpanProcessor)
211+
if config.simple is not None:
212+
exporter = _create_span_exporter(config.simple.exporter)
213+
return SimpleSpanProcessor(exporter)
214+
raise ConfigurationError(
215+
"No processor type specified in span processor config. "
216+
"Supported types: batch, simple."
217+
)
218+
219+
220+
def _create_batch_span_processor(
221+
config: BatchSpanProcessorConfig, batch_cls: type
222+
) -> object:
223+
"""Build a BatchSpanProcessor from config."""
224+
exporter = _create_span_exporter(config.exporter)
225+
return batch_cls(
226+
exporter,
227+
max_queue_size=config.max_queue_size,
228+
schedule_delay_millis=config.schedule_delay,
229+
max_export_batch_size=config.max_export_batch_size,
230+
export_timeout_millis=config.export_timeout,
231+
)
232+
233+
234+
def _create_sampler(config: SamplerConfig) -> Sampler:
235+
"""Create a sampler from config."""
236+
if config.always_on is not None:
237+
return ALWAYS_ON
238+
if config.always_off is not None:
239+
return ALWAYS_OFF
240+
if config.trace_id_ratio_based is not None:
241+
ratio = config.trace_id_ratio_based.ratio
242+
return TraceIdRatioBased(ratio if ratio is not None else 1.0)
243+
if config.parent_based is not None:
244+
return _create_parent_based_sampler(config.parent_based)
245+
raise ConfigurationError(
246+
f"Unknown or unsupported sampler type in config: {config!r}. "
247+
"Supported types: always_on, always_off, trace_id_ratio_based, parent_based."
248+
)
249+
250+
251+
def _create_parent_based_sampler(config: ParentBasedSamplerConfig) -> Sampler:
252+
"""Create a ParentBased sampler from config, applying SDK defaults for absent delegates."""
253+
root = _create_sampler(config.root) if config.root is not None else ALWAYS_ON
254+
kwargs = {"root": root}
255+
if config.remote_parent_sampled is not None:
256+
kwargs["remote_parent_sampled"] = _create_sampler(
257+
config.remote_parent_sampled
258+
)
259+
if config.remote_parent_not_sampled is not None:
260+
kwargs["remote_parent_not_sampled"] = _create_sampler(
261+
config.remote_parent_not_sampled
262+
)
263+
if config.local_parent_sampled is not None:
264+
kwargs["local_parent_sampled"] = _create_sampler(
265+
config.local_parent_sampled
266+
)
267+
if config.local_parent_not_sampled is not None:
268+
kwargs["local_parent_not_sampled"] = _create_sampler(
269+
config.local_parent_not_sampled
270+
)
271+
return ParentBased(**kwargs) # type: ignore[arg-type]
272+
273+
274+
def _create_span_limits(config: SpanLimitsConfig) -> SpanLimits:
275+
"""Create SpanLimits from config.
276+
277+
Absent fields use the OTel spec defaults (128 for counts, unlimited for lengths).
278+
Explicit values suppress env-var reading — matching Java SDK behavior.
279+
"""
280+
return SpanLimits(
281+
max_span_attributes=(
282+
config.attribute_count_limit
283+
if config.attribute_count_limit is not None
284+
else _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
285+
),
286+
max_events=(
287+
config.event_count_limit
288+
if config.event_count_limit is not None
289+
else _DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT
290+
),
291+
max_links=(
292+
config.link_count_limit
293+
if config.link_count_limit is not None
294+
else _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT
295+
),
296+
max_event_attributes=(
297+
config.event_attribute_count_limit
298+
if config.event_attribute_count_limit is not None
299+
else _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT
300+
),
301+
max_link_attributes=(
302+
config.link_attribute_count_limit
303+
if config.link_attribute_count_limit is not None
304+
else _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT
305+
),
306+
max_attribute_length=(
307+
config.attribute_value_length_limit
308+
),
309+
)
310+
311+
312+
def create_tracer_provider(
313+
config: Optional[TracerProviderConfig],
314+
resource: Optional[Resource] = None,
315+
) -> TracerProvider:
316+
"""Create an SDK TracerProvider from declarative config.
317+
318+
Does NOT read OTEL_TRACES_SAMPLER, OTEL_SPAN_*_LIMIT, or any other env vars
319+
for values that are explicitly controlled by the config. Absent config values
320+
use OTel spec defaults (not env vars), matching Java SDK behavior.
321+
322+
Args:
323+
config: TracerProvider config from the parsed config file, or None.
324+
resource: Resource to attach to the provider.
325+
326+
Returns:
327+
A configured TracerProvider.
328+
"""
329+
sampler = (
330+
_create_sampler(config.sampler)
331+
if config is not None and config.sampler is not None
332+
else _DEFAULT_SAMPLER
333+
)
334+
span_limits = (
335+
_create_span_limits(config.limits)
336+
if config is not None and config.limits is not None
337+
else SpanLimits(
338+
max_span_attributes=_DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
339+
max_events=_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT,
340+
max_links=_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT,
341+
max_event_attributes=_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
342+
max_link_attributes=_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
343+
)
344+
)
345+
346+
provider = TracerProvider(
347+
resource=resource,
348+
sampler=sampler,
349+
span_limits=span_limits,
350+
)
351+
352+
if config is not None:
353+
for proc_config in config.processors:
354+
provider.add_span_processor( # type: ignore[arg-type]
355+
_create_span_processor(proc_config)
356+
)
357+
358+
return provider
359+
360+
361+
def configure_tracer_provider(
362+
config: Optional[TracerProviderConfig],
363+
resource: Optional[Resource] = None,
364+
) -> None:
365+
"""Configure the global TracerProvider from declarative config.
366+
367+
When config is None (tracer_provider section absent from config file),
368+
the global is not set — matching Java/JS SDK behavior and the spec's
369+
"a noop tracer provider is used" default.
370+
371+
Args:
372+
config: TracerProvider config from the parsed config file, or None.
373+
resource: Resource to attach to the provider.
374+
"""
375+
if config is None:
376+
return
377+
trace.set_tracer_provider(create_tracer_provider(config, resource))

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/file/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
create_propagator,
3030
)
3131
from opentelemetry.sdk._configuration._resource import create_resource
32+
from opentelemetry.sdk._configuration._tracer_provider import (
33+
configure_tracer_provider,
34+
create_tracer_provider,
35+
)
3236
from opentelemetry.sdk._configuration.file._env_substitution import (
3337
EnvSubstitutionError,
3438
substitute_env_vars,
@@ -46,4 +50,6 @@
4650
"create_resource",
4751
"create_propagator",
4852
"configure_propagator",
53+
"create_tracer_provider",
54+
"configure_tracer_provider",
4955
]

0 commit comments

Comments
 (0)