-
Notifications
You must be signed in to change notification settings - Fork 866
feat(config): add resource and propagator creation from declarative config #4979
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
xrmx
merged 14 commits into
open-telemetry:main
from
MikeGoldsmith:mike/config-resource-propagator
Mar 25, 2026
Merged
Changes from 3 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
5c26f00
config: add resource and propagator creation from declarative config
MikeGoldsmith 8232012
update changelog with PR number
MikeGoldsmith 8329ae4
fix pylint, pyright and ruff errors in resource/propagator config
MikeGoldsmith 506d816
address review feedback: use _DEFAULT_RESOURCE, fix bool_array coercion
MikeGoldsmith 8232d48
fix linter
MikeGoldsmith 6ed3425
Merge branch 'main' of github.com:open-telemetry/opentelemetry-python…
MikeGoldsmith 99753f9
address review feedback: single coercion table, simplify attributes m…
MikeGoldsmith 8ba91d8
use Callable type annotation on _array helper
MikeGoldsmith 516aecc
Merge remote-tracking branch 'upstream/main' into mike/config-resourc…
MikeGoldsmith 9cfdcce
add detection infrastructure foundations for resource detectors
MikeGoldsmith 103ff08
move service.name default into base resource
MikeGoldsmith 7f51034
remove unused logging import from _propagator.py
MikeGoldsmith 16b89b1
add test verifying OTEL_PROPAGATORS env var is ignored by configure_p…
MikeGoldsmith a171842
remove backwards compat re-export of ConfigurationError from _loader.py
MikeGoldsmith File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
25 changes: 25 additions & 0 deletions
25
opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_exceptions.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Copyright The OpenTelemetry Authors | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
|
|
||
| class ConfigurationError(Exception): | ||
| """Raised when configuration loading, parsing, validation, or instantiation fails. | ||
|
|
||
| This includes errors from: | ||
| - File not found or inaccessible | ||
| - Invalid YAML/JSON syntax | ||
| - Schema validation failures | ||
| - Environment variable substitution errors | ||
| - Missing required SDK extensions (e.g., propagator packages not installed) | ||
| """ |
125 changes: 125 additions & 0 deletions
125
opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_propagator.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| # Copyright The OpenTelemetry Authors | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Optional | ||
|
|
||
| from opentelemetry.baggage.propagation import W3CBaggagePropagator | ||
| from opentelemetry.propagate import set_global_textmap | ||
| from opentelemetry.propagators.composite import CompositePropagator | ||
| from opentelemetry.propagators.textmap import TextMapPropagator | ||
| from opentelemetry.sdk._configuration._exceptions import ConfigurationError | ||
| from opentelemetry.sdk._configuration.models import ( | ||
| Propagator as PropagatorConfig, | ||
| ) | ||
| from opentelemetry.sdk._configuration.models import ( | ||
| TextMapPropagator as TextMapPropagatorConfig, | ||
| ) | ||
| from opentelemetry.trace.propagation.tracecontext import ( | ||
| TraceContextTextMapPropagator, | ||
| ) | ||
| from opentelemetry.util._importlib_metadata import entry_points | ||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _load_entry_point_propagator(name: str) -> TextMapPropagator: | ||
| """Load a propagator by name from the opentelemetry_propagator entry point group.""" | ||
| try: | ||
| eps = list(entry_points(group="opentelemetry_propagator", name=name)) | ||
| if not eps: | ||
| raise ConfigurationError( | ||
| f"Propagator '{name}' not found. " | ||
| "It may not be installed or may be misspelled." | ||
| ) | ||
| return eps[0].load()() | ||
| except ConfigurationError: | ||
| raise | ||
| except Exception as exc: | ||
| raise ConfigurationError( | ||
| f"Failed to load propagator '{name}': {exc}" | ||
| ) from exc | ||
|
|
||
|
|
||
| def _propagators_from_textmap_config( | ||
| config: TextMapPropagatorConfig, | ||
| ) -> list[TextMapPropagator]: | ||
| """Resolve a single TextMapPropagator config entry to a list of propagators.""" | ||
| result: list[TextMapPropagator] = [] | ||
| if config.tracecontext is not None: | ||
| result.append(TraceContextTextMapPropagator()) | ||
| if config.baggage is not None: | ||
| result.append(W3CBaggagePropagator()) | ||
| if config.b3 is not None: | ||
| result.append(_load_entry_point_propagator("b3")) | ||
| if config.b3multi is not None: | ||
| result.append(_load_entry_point_propagator("b3multi")) | ||
| return result | ||
|
|
||
|
|
||
| def create_propagator( | ||
| config: Optional[PropagatorConfig], | ||
| ) -> CompositePropagator: | ||
| """Create a CompositePropagator from declarative config. | ||
|
|
||
| If config is None or has no propagators defined, returns an empty | ||
| CompositePropagator (no-op), ensuring "what you see is what you get" | ||
| semantics — the env-var-based default propagators are not used. | ||
|
|
||
| Args: | ||
| config: Propagator config from the parsed config file, or None. | ||
|
|
||
| Returns: | ||
| A CompositePropagator wrapping all configured propagators. | ||
| """ | ||
| if config is None: | ||
| return CompositePropagator([]) | ||
|
|
||
| propagators: list[TextMapPropagator] = [] | ||
| seen_types: set[type] = set() | ||
|
|
||
| def _add_deduped(propagator: TextMapPropagator) -> None: | ||
| if type(propagator) not in seen_types: | ||
| seen_types.add(type(propagator)) | ||
| propagators.append(propagator) | ||
|
|
||
| # Process structured composite list | ||
| if config.composite: | ||
| for entry in config.composite: | ||
| for propagator in _propagators_from_textmap_config(entry): | ||
| _add_deduped(propagator) | ||
|
|
||
| # Process composite_list (comma-separated propagator names via entry_points) | ||
| if config.composite_list: | ||
| for name in config.composite_list.split(","): | ||
| name = name.strip() | ||
| if not name or name.lower() == "none": | ||
| continue | ||
| _add_deduped(_load_entry_point_propagator(name)) | ||
|
|
||
| return CompositePropagator(propagators) | ||
|
|
||
|
|
||
| def configure_propagator(config: Optional[PropagatorConfig]) -> None: | ||
| """Configure the global text map propagator from declarative config. | ||
|
|
||
| Always calls set_global_textmap to override any defaults (including the | ||
| env-var-based tracecontext+baggage default set by the SDK). | ||
|
|
||
| Args: | ||
| config: Propagator config from the parsed config file, or None. | ||
| """ | ||
| set_global_textmap(create_propagator(config)) | ||
150 changes: 150 additions & 0 deletions
150
opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_resource.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| # Copyright The OpenTelemetry Authors | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from typing import Optional | ||
| from urllib import parse | ||
|
|
||
| from opentelemetry.sdk._configuration.models import ( | ||
| AttributeNameValue, | ||
| AttributeType, | ||
| ) | ||
| from opentelemetry.sdk._configuration.models import Resource as ResourceConfig | ||
| from opentelemetry.sdk.resources import ( | ||
| _OPENTELEMETRY_SDK_VERSION, | ||
| SERVICE_NAME, | ||
| TELEMETRY_SDK_LANGUAGE, | ||
| TELEMETRY_SDK_NAME, | ||
| TELEMETRY_SDK_VERSION, | ||
| Resource, | ||
| ) | ||
|
|
||
| _logger = logging.getLogger(__name__) | ||
|
|
||
| # Dispatch table for scalar type coercions | ||
| _SCALAR_COERCIONS = { | ||
| AttributeType.string: str, | ||
| AttributeType.int: int, | ||
| AttributeType.double: float, | ||
| } | ||
|
|
||
| # Dispatch table for array type coercions | ||
| _ARRAY_COERCIONS = { | ||
| AttributeType.string_array: str, | ||
| AttributeType.bool_array: bool, | ||
| AttributeType.int_array: int, | ||
| AttributeType.double_array: float, | ||
| } | ||
|
|
||
|
|
||
| def _coerce_bool(value: object) -> bool: | ||
| if isinstance(value, str): | ||
| return value.lower() not in ("false", "0", "") | ||
| return bool(value) | ||
|
|
||
|
|
||
| def _coerce_attribute_value(attr: AttributeNameValue) -> object: | ||
|
MikeGoldsmith marked this conversation as resolved.
|
||
| """Coerce an attribute value to the correct Python type based on AttributeType.""" | ||
| value = attr.value | ||
| attr_type = attr.type | ||
|
|
||
| if attr_type is None: | ||
| return value | ||
| if attr_type == AttributeType.bool: | ||
| return _coerce_bool(value) | ||
|
MikeGoldsmith marked this conversation as resolved.
Outdated
|
||
| scalar_coercer = _SCALAR_COERCIONS.get(attr_type) | ||
| if scalar_coercer is not None: | ||
| return scalar_coercer(value) # type: ignore[arg-type] | ||
| array_coercer = _ARRAY_COERCIONS.get(attr_type) | ||
| if array_coercer is not None: | ||
| return [array_coercer(item) for item in value] # type: ignore[union-attr,arg-type] | ||
| return value | ||
|
|
||
|
|
||
| def _parse_attributes_list(attributes_list: str) -> dict[str, str]: | ||
|
pmcollins marked this conversation as resolved.
|
||
| """Parse a comma-separated key=value string into a dict. | ||
|
|
||
| Format is the same as OTEL_RESOURCE_ATTRIBUTES: key=value,key=value | ||
| Values are always strings (no type coercion). | ||
| """ | ||
| result: dict[str, str] = {} | ||
| for item in attributes_list.split(","): | ||
| item = item.strip() | ||
| if not item: | ||
| continue | ||
| if "=" not in item: | ||
| _logger.warning( | ||
| "Invalid resource attribute pair in attributes_list: %s", | ||
| item, | ||
| ) | ||
| continue | ||
| key, value = item.split("=", maxsplit=1) | ||
| result[key.strip()] = parse.unquote(value.strip()) | ||
| return result | ||
|
|
||
|
|
||
| def _sdk_default_attributes() -> dict[str, str]: | ||
| """Return the SDK telemetry attributes (equivalent to Java's Resource.getDefault()).""" | ||
| return { | ||
|
MikeGoldsmith marked this conversation as resolved.
Outdated
|
||
| TELEMETRY_SDK_LANGUAGE: "python", | ||
| TELEMETRY_SDK_NAME: "opentelemetry", | ||
| TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION, | ||
| } | ||
|
|
||
|
|
||
| def create_resource(config: Optional[ResourceConfig]) -> Resource: | ||
| """Create an SDK Resource from declarative config. | ||
|
|
||
| Does NOT read OTEL_RESOURCE_ATTRIBUTES or run any resource detectors. | ||
| Starts from SDK telemetry defaults (telemetry.sdk.*) and merges config | ||
| attributes on top, matching Java SDK behavior. | ||
|
|
||
| Args: | ||
| config: Resource config from the parsed config file, or None. | ||
|
|
||
| Returns: | ||
| A Resource with SDK defaults merged with any config-specified attributes. | ||
| """ | ||
| base = Resource(_sdk_default_attributes()) | ||
|
|
||
| if config is None: | ||
| service_resource = Resource({SERVICE_NAME: "unknown_service"}) | ||
| return base.merge(service_resource) | ||
|
|
||
| # Build attributes from config.attributes list | ||
| config_attrs: dict[str, object] = {} | ||
| if config.attributes: | ||
| for attr in config.attributes: | ||
| config_attrs[attr.name] = _coerce_attribute_value(attr) | ||
|
|
||
| # Parse attributes_list (key=value,key=value string format) | ||
| if config.attributes_list: | ||
| list_attrs = _parse_attributes_list(config.attributes_list) | ||
| # attributes_list entries do not override explicit attributes | ||
|
MikeGoldsmith marked this conversation as resolved.
Outdated
|
||
| for attr_key, attr_val in list_attrs.items(): | ||
| if attr_key not in config_attrs: | ||
| config_attrs[attr_key] = attr_val | ||
|
|
||
| schema_url = config.schema_url | ||
|
|
||
| config_resource = Resource(config_attrs, schema_url) # type: ignore[arg-type] | ||
| result = base.merge(config_resource) | ||
|
|
||
| # Add default service.name if not specified (matches Java SDK behavior) | ||
| if not result.attributes.get(SERVICE_NAME): | ||
| result = result.merge(Resource({SERVICE_NAME: "unknown_service"})) | ||
|
|
||
| return result | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.