Skip to content

Commit 89a22bc

Browse files
committed
Merge branch 'main' of github.com:open-telemetry/opentelemetry-python into mike/config-meter-provider
2 parents fe44d64 + 6faa58c commit 89a22bc

6 files changed

Lines changed: 946 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212

1313
## Unreleased
1414

15-
- Enabled the flake8-tidy-import plugins rules for the ruff linter. These rules throw warnings for relative imports in the modules.
15+
- `opentelemetry-sdk`: Add `host` resource detector support to declarative file configuration via `detection_development.detectors[].host`
16+
([#5002](https://github.com/open-telemetry/opentelemetry-python/pull/5002))
17+
- `opentelemetry-sdk`: Add `container` resource detector support to declarative file configuration via `detection_development.detectors[].container`, using entry point loading of the `opentelemetry-resource-detector-containerid` contrib package
18+
([#5004](https://github.com/open-telemetry/opentelemetry-python/pull/5004))
19+
- `opentelemetry-sdk`: Add `create_tracer_provider`/`configure_tracer_provider` to declarative file configuration, enabling TracerProvider instantiation from config files without reading env vars
20+
([#4985](https://github.com/open-telemetry/opentelemetry-python/pull/4985))
21+
- Enabled the flake8-tidy-import plugins rules for the ruff linter. These rules throw warnings for relative imports in the modules.
1622
([#5019](https://github.com/open-telemetry/opentelemetry-python/pull/5019))
1723
- `opentelemetry-sdk`: Fix `AttributeError` in `ExplicitBucketHistogramAggregation` when applied to non-Histogram instruments without explicit boundaries
1824
([#5034](https://github.com/open-telemetry/opentelemetry-python/pull/5034))

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
SERVICE_NAME,
3232
ProcessResourceDetector,
3333
Resource,
34+
_HostResourceDetector,
3435
)
36+
from opentelemetry.util._importlib_metadata import entry_points
3537

3638
_logger = logging.getLogger(__name__)
3739

@@ -150,6 +152,34 @@ def _run_detectors(
150152
is updated in-place; later detectors overwrite earlier ones for the
151153
same key.
152154
"""
155+
if detector_config.host is not None:
156+
detected_attrs.update(_HostResourceDetector().detect().attributes)
157+
158+
if detector_config.container is not None:
159+
# The container detector is not part of the core SDK. It is provided
160+
# by the opentelemetry-resource-detector-containerid contrib package,
161+
# which registers itself under the opentelemetry_resource_detector
162+
# entry point group as "container". Loading via entry point matches
163+
# the env-var config counterpart (OTEL_EXPERIMENTAL_RESOURCE_DETECTORS)
164+
# and avoids a hard import dependency on contrib. See also:
165+
# https://github.com/open-telemetry/opentelemetry-configuration/issues/570
166+
ep = next(
167+
iter(
168+
entry_points(
169+
group="opentelemetry_resource_detector", name="container"
170+
)
171+
),
172+
None,
173+
)
174+
if ep is None:
175+
_logger.warning(
176+
"container resource detector requested but "
177+
"'opentelemetry-resource-detector-containerid' is not "
178+
"installed; install it to enable container detection"
179+
)
180+
else:
181+
detected_attrs.update(ep.load()().detect().attributes)
182+
153183
if detector_config.process is not None:
154184
detected_attrs.update(ProcessResourceDetector().detect().attributes)
155185

Lines changed: 327 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,327 @@
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 Optional
19+
20+
from opentelemetry import trace
21+
from opentelemetry.sdk._configuration._common import _parse_headers
22+
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
23+
from opentelemetry.sdk._configuration.models import (
24+
OtlpGrpcExporter as OtlpGrpcExporterConfig,
25+
)
26+
from opentelemetry.sdk._configuration.models import (
27+
OtlpHttpExporter as OtlpHttpExporterConfig,
28+
)
29+
from opentelemetry.sdk._configuration.models import (
30+
ParentBasedSampler as ParentBasedSamplerConfig,
31+
)
32+
from opentelemetry.sdk._configuration.models import (
33+
Sampler as SamplerConfig,
34+
)
35+
from opentelemetry.sdk._configuration.models import (
36+
SpanExporter as SpanExporterConfig,
37+
)
38+
from opentelemetry.sdk._configuration.models import (
39+
SpanLimits as SpanLimitsConfig,
40+
)
41+
from opentelemetry.sdk._configuration.models import (
42+
SpanProcessor as SpanProcessorConfig,
43+
)
44+
from opentelemetry.sdk._configuration.models import (
45+
TracerProvider as TracerProviderConfig,
46+
)
47+
from opentelemetry.sdk.resources import Resource
48+
from opentelemetry.sdk.trace import (
49+
_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
50+
_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
51+
_DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
52+
_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT,
53+
_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT,
54+
SpanLimits,
55+
TracerProvider,
56+
)
57+
from opentelemetry.sdk.trace.export import (
58+
BatchSpanProcessor,
59+
ConsoleSpanExporter,
60+
SimpleSpanProcessor,
61+
SpanExporter,
62+
)
63+
from opentelemetry.sdk.trace.sampling import (
64+
ALWAYS_OFF,
65+
ALWAYS_ON,
66+
ParentBased,
67+
Sampler,
68+
TraceIdRatioBased,
69+
)
70+
71+
_logger = logging.getLogger(__name__)
72+
73+
# Default sampler per the OTel spec: parent_based with always_on root.
74+
_DEFAULT_SAMPLER = ParentBased(root=ALWAYS_ON)
75+
76+
77+
def _create_otlp_http_span_exporter(
78+
config: OtlpHttpExporterConfig,
79+
) -> SpanExporter:
80+
"""Create an OTLP HTTP span exporter from config."""
81+
try:
82+
# pylint: disable=import-outside-toplevel,no-name-in-module
83+
from opentelemetry.exporter.otlp.proto.http import ( # type: ignore[import-untyped] # noqa: PLC0415
84+
Compression,
85+
)
86+
from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( # type: ignore[import-untyped] # noqa: PLC0415
87+
OTLPSpanExporter,
88+
)
89+
except ImportError as exc:
90+
raise ConfigurationError(
91+
"otlp_http span exporter requires 'opentelemetry-exporter-otlp-proto-http'. "
92+
"Install it with: pip install opentelemetry-exporter-otlp-proto-http"
93+
) from exc
94+
95+
compression = _map_compression(config.compression, Compression)
96+
headers = _parse_headers(config.headers, config.headers_list)
97+
timeout = (config.timeout / 1000.0) if config.timeout is not None else None
98+
99+
return OTLPSpanExporter( # type: ignore[return-value]
100+
endpoint=config.endpoint,
101+
headers=headers,
102+
timeout=timeout,
103+
compression=compression, # type: ignore[arg-type]
104+
)
105+
106+
107+
def _map_compression(
108+
value: Optional[str], compression_enum: type
109+
) -> Optional[object]:
110+
"""Map a compression string to the given Compression enum value."""
111+
if value is None or value.lower() == "none":
112+
return None
113+
if value.lower() == "gzip":
114+
return compression_enum.Gzip # type: ignore[attr-defined]
115+
raise ConfigurationError(
116+
f"Unsupported compression value '{value}'. Supported values: 'gzip', 'none'."
117+
)
118+
119+
120+
def _create_otlp_grpc_span_exporter(
121+
config: OtlpGrpcExporterConfig,
122+
) -> SpanExporter:
123+
"""Create an OTLP gRPC span exporter from config."""
124+
try:
125+
# pylint: disable=import-outside-toplevel,no-name-in-module
126+
import grpc # type: ignore[import-untyped] # noqa: PLC0415
127+
128+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( # type: ignore[import-untyped] # noqa: PLC0415
129+
OTLPSpanExporter,
130+
)
131+
except ImportError as exc:
132+
raise ConfigurationError(
133+
"otlp_grpc span exporter requires 'opentelemetry-exporter-otlp-proto-grpc'. "
134+
"Install it with: pip install opentelemetry-exporter-otlp-proto-grpc"
135+
) from exc
136+
137+
compression = _map_compression(config.compression, grpc.Compression)
138+
headers = _parse_headers(config.headers, config.headers_list)
139+
timeout = (config.timeout / 1000.0) if config.timeout is not None else None
140+
141+
return OTLPSpanExporter( # type: ignore[return-value]
142+
endpoint=config.endpoint,
143+
headers=headers,
144+
timeout=timeout,
145+
compression=compression, # type: ignore[arg-type]
146+
)
147+
148+
149+
def _create_span_exporter(config: SpanExporterConfig) -> SpanExporter:
150+
"""Create a span exporter from config."""
151+
if config.otlp_http is not None:
152+
return _create_otlp_http_span_exporter(config.otlp_http)
153+
if config.otlp_grpc is not None:
154+
return _create_otlp_grpc_span_exporter(config.otlp_grpc)
155+
if config.console is not None:
156+
return ConsoleSpanExporter()
157+
raise ConfigurationError(
158+
"No exporter type specified in span exporter config. "
159+
"Supported types: otlp_http, otlp_grpc, console."
160+
)
161+
162+
163+
def _create_span_processor(
164+
config: SpanProcessorConfig,
165+
) -> BatchSpanProcessor | SimpleSpanProcessor:
166+
"""Create a span processor from config."""
167+
if config.batch is not None:
168+
exporter = _create_span_exporter(config.batch.exporter)
169+
return BatchSpanProcessor(
170+
exporter,
171+
max_queue_size=config.batch.max_queue_size,
172+
schedule_delay_millis=config.batch.schedule_delay,
173+
max_export_batch_size=config.batch.max_export_batch_size,
174+
export_timeout_millis=config.batch.export_timeout,
175+
)
176+
if config.simple is not None:
177+
return SimpleSpanProcessor(
178+
_create_span_exporter(config.simple.exporter)
179+
)
180+
raise ConfigurationError(
181+
"No processor type specified in span processor config. "
182+
"Supported types: batch, simple."
183+
)
184+
185+
186+
def _create_sampler(config: SamplerConfig) -> Sampler:
187+
"""Create a sampler from config."""
188+
if config.always_on is not None:
189+
return ALWAYS_ON
190+
if config.always_off is not None:
191+
return ALWAYS_OFF
192+
if config.trace_id_ratio_based is not None:
193+
ratio = config.trace_id_ratio_based.ratio
194+
return TraceIdRatioBased(ratio if ratio is not None else 1.0)
195+
if config.parent_based is not None:
196+
return _create_parent_based_sampler(config.parent_based)
197+
raise ConfigurationError(
198+
f"Unknown or unsupported sampler type in config: {config!r}. "
199+
"Supported types: always_on, always_off, trace_id_ratio_based, parent_based."
200+
)
201+
202+
203+
def _create_parent_based_sampler(config: ParentBasedSamplerConfig) -> Sampler:
204+
"""Create a ParentBased sampler from config, applying SDK defaults for absent delegates."""
205+
root = (
206+
_create_sampler(config.root) if config.root is not None else ALWAYS_ON
207+
)
208+
kwargs: dict = {"root": root}
209+
if config.remote_parent_sampled is not None:
210+
kwargs["remote_parent_sampled"] = _create_sampler(
211+
config.remote_parent_sampled
212+
)
213+
if config.remote_parent_not_sampled is not None:
214+
kwargs["remote_parent_not_sampled"] = _create_sampler(
215+
config.remote_parent_not_sampled
216+
)
217+
if config.local_parent_sampled is not None:
218+
kwargs["local_parent_sampled"] = _create_sampler(
219+
config.local_parent_sampled
220+
)
221+
if config.local_parent_not_sampled is not None:
222+
kwargs["local_parent_not_sampled"] = _create_sampler(
223+
config.local_parent_not_sampled
224+
)
225+
return ParentBased(**kwargs)
226+
227+
228+
def _create_span_limits(config: SpanLimitsConfig) -> SpanLimits:
229+
"""Create SpanLimits from config.
230+
231+
Absent fields use the OTel spec defaults (128 for counts, unlimited for lengths).
232+
Explicit values suppress env-var reading — matching Java SDK behavior.
233+
"""
234+
return SpanLimits(
235+
max_span_attributes=(
236+
config.attribute_count_limit
237+
if config.attribute_count_limit is not None
238+
else _DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT
239+
),
240+
max_events=(
241+
config.event_count_limit
242+
if config.event_count_limit is not None
243+
else _DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT
244+
),
245+
max_links=(
246+
config.link_count_limit
247+
if config.link_count_limit is not None
248+
else _DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT
249+
),
250+
max_event_attributes=(
251+
config.event_attribute_count_limit
252+
if config.event_attribute_count_limit is not None
253+
else _DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT
254+
),
255+
max_link_attributes=(
256+
config.link_attribute_count_limit
257+
if config.link_attribute_count_limit is not None
258+
else _DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT
259+
),
260+
max_attribute_length=config.attribute_value_length_limit,
261+
)
262+
263+
264+
def create_tracer_provider(
265+
config: Optional[TracerProviderConfig],
266+
resource: Optional[Resource] = None,
267+
) -> TracerProvider:
268+
"""Create an SDK TracerProvider from declarative config.
269+
270+
Does NOT read OTEL_TRACES_SAMPLER, OTEL_SPAN_*_LIMIT, or any other env vars
271+
for values that are explicitly controlled by the config. Absent config values
272+
use OTel spec defaults (not env vars), matching Java SDK behavior.
273+
274+
Args:
275+
config: TracerProvider config from the parsed config file, or None.
276+
resource: Resource to attach to the provider.
277+
278+
Returns:
279+
A configured TracerProvider.
280+
"""
281+
sampler = (
282+
_create_sampler(config.sampler)
283+
if config is not None and config.sampler is not None
284+
else _DEFAULT_SAMPLER
285+
)
286+
span_limits = (
287+
_create_span_limits(config.limits)
288+
if config is not None and config.limits is not None
289+
else SpanLimits(
290+
max_span_attributes=_DEFAULT_OTEL_SPAN_ATTRIBUTE_COUNT_LIMIT,
291+
max_events=_DEFAULT_OTEL_SPAN_EVENT_COUNT_LIMIT,
292+
max_links=_DEFAULT_OTEL_SPAN_LINK_COUNT_LIMIT,
293+
max_event_attributes=_DEFAULT_OTEL_EVENT_ATTRIBUTE_COUNT_LIMIT,
294+
max_link_attributes=_DEFAULT_OTEL_LINK_ATTRIBUTE_COUNT_LIMIT,
295+
)
296+
)
297+
298+
provider = TracerProvider(
299+
resource=resource,
300+
sampler=sampler,
301+
span_limits=span_limits,
302+
)
303+
304+
if config is not None:
305+
for proc_config in config.processors:
306+
provider.add_span_processor(_create_span_processor(proc_config))
307+
308+
return provider
309+
310+
311+
def configure_tracer_provider(
312+
config: Optional[TracerProviderConfig],
313+
resource: Optional[Resource] = None,
314+
) -> None:
315+
"""Configure the global TracerProvider from declarative config.
316+
317+
When config is None (tracer_provider section absent from config file),
318+
the global is not set — matching Java/JS SDK behavior and the spec's
319+
"a noop tracer provider is used" default.
320+
321+
Args:
322+
config: TracerProvider config from the parsed config file, or None.
323+
resource: Resource to attach to the provider.
324+
"""
325+
if config is None:
326+
return
327+
trace.set_tracer_provider(create_tracer_provider(config, resource))

0 commit comments

Comments
 (0)