dash0-opentelemetry-configuration 0.65b0.post1__tar.gz

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.
Files changed (29) hide show
  1. dash0_opentelemetry_configuration-0.65b0.post1/.gitignore +28 -0
  2. dash0_opentelemetry_configuration-0.65b0.post1/PKG-INFO +72 -0
  3. dash0_opentelemetry_configuration-0.65b0.post1/README.rst +61 -0
  4. dash0_opentelemetry_configuration-0.65b0.post1/pyproject.toml +28 -0
  5. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/README.md +28 -0
  6. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/__init__.py +42 -0
  7. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_common.py +238 -0
  8. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_conversion.py +118 -0
  9. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_exceptions.py +59 -0
  10. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_generated_validator.py +2936 -0
  11. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_logger_provider.py +288 -0
  12. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_meter_provider.py +626 -0
  13. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_propagator.py +150 -0
  14. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_resource.py +219 -0
  15. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_sdk.py +68 -0
  16. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_tracer_provider.py +508 -0
  17. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/_validation_exceptions.py +62 -0
  18. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/file/__init__.py +60 -0
  19. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/file/_env_substitution.py +92 -0
  20. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/file/_loader.py +237 -0
  21. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/instrumentation.py +78 -0
  22. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/models.py +835 -0
  23. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/schema.json +2576 -0
  24. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/_configuration/version/__init__.py +4 -0
  25. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/configuration/__init__.py +5 -0
  26. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/configuration/file/__init__.py +5 -0
  27. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/configuration/instrumentation.py +4 -0
  28. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/configuration/models.py +4 -0
  29. dash0_opentelemetry_configuration-0.65b0.post1/src/opentelemetry/configuration/version/__init__.py +4 -0
@@ -0,0 +1,28 @@
1
+ # Byte-compiled / optimized
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # Distribution / packaging
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+
12
+ # Test / coverage
13
+ .pytest_cache/
14
+ .coverage
15
+ htmlcov/
16
+ .tox/
17
+
18
+ # Environments
19
+ .venv/
20
+ venv/
21
+ .env
22
+
23
+ # Editor / OS
24
+ .idea/
25
+ .vscode/
26
+ .DS_Store
27
+
28
+ tests/e2e-kubernetes/uv.lock
@@ -0,0 +1,72 @@
1
+ Metadata-Version: 2.5
2
+ Name: dash0-opentelemetry-configuration
3
+ Version: 0.65b0.post1
4
+ Summary: Declarative configuration for the Dash0 OpenTelemetry distribution
5
+ License-Expression: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: opentelemetry-api==1.44.0
8
+ Requires-Dist: opentelemetry-sdk==1.44.0
9
+ Requires-Dist: pyyaml
10
+ Description-Content-Type: text/x-rst
11
+
12
+ Dash0 OpenTelemetry Declarative Configuration
13
+ =============================================
14
+
15
+ Declarative (file-based) SDK configuration for the Dash0 OpenTelemetry Python
16
+ distribution, read from ``OTEL_CONFIG_FILE``.
17
+
18
+ The package is experimental, tracking a configuration format that is still at
19
+ ``1.0-rc.1``.
20
+
21
+ Do not edit this package by hand
22
+ --------------------------------
23
+
24
+ Everything under ``src/`` and the whole of ``pyproject.toml`` are generated by
25
+ ``scripts/vendor_configuration.py`` from the ``dash0`` branch of
26
+ https://github.com/dash0hq/opentelemetry-python. The ``UPSTREAM`` file records
27
+ the exact commit. A hand edit here is reverted by the next run of that script,
28
+ and the ``vendored-configuration`` job in CI fails while it is still present.
29
+
30
+ Two files are exempt, because the script leaves them alone: this README, and
31
+ ``src/opentelemetry/_configuration/version/__init__.py``, which holds the
32
+ released version and is advanced by ``scripts/bump_for_release.py``.
33
+
34
+ Where a change belongs
35
+ ----------------------
36
+
37
+ Changes to how configuration files are parsed, validated or applied go on the
38
+ fork's ``dash0`` branch, as commits on top of an upstream commit. That is what
39
+ makes them survive upstream moving: rebase the branch onto the new upstream
40
+ commit, then run::
41
+
42
+ python3 scripts/vendor_configuration.py --commit <new sha>
43
+
44
+ Changes to how the code is packaged for this distribution go in
45
+ ``scripts/vendor_configuration.py``. It owns the rename from
46
+ ``opentelemetry.configuration`` to ``opentelemetry._configuration``, the
47
+ re-export shims at the path the SDK imports, and the dependency pins, which it
48
+ copies from the distribution's own pins rather than repeating them.
49
+
50
+ Why the implementation sits at a private path
51
+ ---------------------------------------------
52
+
53
+ The implementation is installed as ``opentelemetry._configuration``, a path
54
+ upstream does not own, and ``opentelemetry.configuration`` holds only star
55
+ re-export shims. ``opentelemetry/sdk/_configuration/__init__.py`` imports
56
+ ``opentelemetry.configuration`` by name, so the shims are what make
57
+ ``OTEL_CONFIG_FILE`` work, while the private path means installing this
58
+ alongside a future published ``opentelemetry-configuration`` cannot collide
59
+ file for file. The pyproto exporter packages in this workspace use the same
60
+ arrangement.
61
+
62
+ What the fork changes
63
+ ---------------------
64
+
65
+ Upstream validates configuration files with ``jsonschema``, which brings
66
+ ``attrs``, ``referencing``, ``jsonschema-specifications`` and ``rpds-py``. All
67
+ four would land in the tree the injector adds to an application's
68
+ ``sys.path``, and ``rpds-py`` is a compiled extension, so that tree would stop
69
+ being pure Python. The fork compiles the schema to plain Python with
70
+ ``fastjsonschema`` at development time instead, leaving a validator that
71
+ imports ``decimal`` and nothing else. The fork's own test suite checks that
72
+ validator against ``jsonschema`` document by document.
@@ -0,0 +1,61 @@
1
+ Dash0 OpenTelemetry Declarative Configuration
2
+ =============================================
3
+
4
+ Declarative (file-based) SDK configuration for the Dash0 OpenTelemetry Python
5
+ distribution, read from ``OTEL_CONFIG_FILE``.
6
+
7
+ The package is experimental, tracking a configuration format that is still at
8
+ ``1.0-rc.1``.
9
+
10
+ Do not edit this package by hand
11
+ --------------------------------
12
+
13
+ Everything under ``src/`` and the whole of ``pyproject.toml`` are generated by
14
+ ``scripts/vendor_configuration.py`` from the ``dash0`` branch of
15
+ https://github.com/dash0hq/opentelemetry-python. The ``UPSTREAM`` file records
16
+ the exact commit. A hand edit here is reverted by the next run of that script,
17
+ and the ``vendored-configuration`` job in CI fails while it is still present.
18
+
19
+ Two files are exempt, because the script leaves them alone: this README, and
20
+ ``src/opentelemetry/_configuration/version/__init__.py``, which holds the
21
+ released version and is advanced by ``scripts/bump_for_release.py``.
22
+
23
+ Where a change belongs
24
+ ----------------------
25
+
26
+ Changes to how configuration files are parsed, validated or applied go on the
27
+ fork's ``dash0`` branch, as commits on top of an upstream commit. That is what
28
+ makes them survive upstream moving: rebase the branch onto the new upstream
29
+ commit, then run::
30
+
31
+ python3 scripts/vendor_configuration.py --commit <new sha>
32
+
33
+ Changes to how the code is packaged for this distribution go in
34
+ ``scripts/vendor_configuration.py``. It owns the rename from
35
+ ``opentelemetry.configuration`` to ``opentelemetry._configuration``, the
36
+ re-export shims at the path the SDK imports, and the dependency pins, which it
37
+ copies from the distribution's own pins rather than repeating them.
38
+
39
+ Why the implementation sits at a private path
40
+ ---------------------------------------------
41
+
42
+ The implementation is installed as ``opentelemetry._configuration``, a path
43
+ upstream does not own, and ``opentelemetry.configuration`` holds only star
44
+ re-export shims. ``opentelemetry/sdk/_configuration/__init__.py`` imports
45
+ ``opentelemetry.configuration`` by name, so the shims are what make
46
+ ``OTEL_CONFIG_FILE`` work, while the private path means installing this
47
+ alongside a future published ``opentelemetry-configuration`` cannot collide
48
+ file for file. The pyproto exporter packages in this workspace use the same
49
+ arrangement.
50
+
51
+ What the fork changes
52
+ ---------------------
53
+
54
+ Upstream validates configuration files with ``jsonschema``, which brings
55
+ ``attrs``, ``referencing``, ``jsonschema-specifications`` and ``rpds-py``. All
56
+ four would land in the tree the injector adds to an application's
57
+ ``sys.path``, and ``rpds-py`` is a compiled extension, so that tree would stop
58
+ being pure Python. The fork compiles the schema to plain Python with
59
+ ``fastjsonschema`` at development time instead, leaving a validator that
60
+ imports ``decimal`` and nothing else. The fork's own test suite checks that
61
+ validator against ``jsonschema`` document by document.
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "dash0-opentelemetry-configuration"
7
+ dynamic = ["version"]
8
+ description = "Declarative configuration for the Dash0 OpenTelemetry distribution"
9
+ readme = "README.rst"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ dependencies = [
13
+ "opentelemetry-api == 1.44.0",
14
+ "opentelemetry-sdk == 1.44.0",
15
+ "pyyaml",
16
+ ]
17
+
18
+ [tool.hatch.version]
19
+ path = "src/opentelemetry/_configuration/version/__init__.py"
20
+
21
+ [tool.hatch.build.targets.sdist]
22
+ include = ["/src"]
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/opentelemetry"]
26
+
27
+ [tool.pytest.ini_options]
28
+ testpaths = ["tests"]
@@ -0,0 +1,28 @@
1
+ # SDK Declarative Configuration
2
+
3
+ This package implements [OpenTelemetry file-based configuration](https://opentelemetry.io/docs/specs/otel/configuration).
4
+
5
+ ## Files
6
+
7
+ - `schema.json`: vendored copy of the [OpenTelemetry configuration JSON schema](https://github.com/open-telemetry/opentelemetry-configuration)
8
+ - `models.py`: Python dataclasses generated from `schema.json` by [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator), using a custom template from `opentelemetry-configuration/codegen/` (see that directory's README for details)
9
+
10
+ ## Spec conformance
11
+
12
+ Which fields of the configuration schema the Python SDK actually applies at runtime is tracked in the shared [language support status matrix](https://github.com/open-telemetry/opentelemetry-configuration/blob/main/language-support-status.md#python) in the `opentelemetry-configuration` repository. The Python column is the source of truth; update [`schema/meta_schema_language_python.yaml`](https://github.com/open-telemetry/opentelemetry-configuration/blob/main/schema/meta_schema_language_python.yaml) there when wiring lands or changes, and the matrix regenerates.
13
+
14
+ ## Updating the schema
15
+
16
+ 1. Pick the desired release tag from the [opentelemetry-configuration releases](https://github.com/open-telemetry/opentelemetry-configuration/releases) and overwrite `opentelemetry-configuration/src/opentelemetry/configuration/schema.json` with the `opentelemetry_configuration.json` published on that release.
17
+
18
+ 2. Regenerate `models.py`:
19
+
20
+ ```sh
21
+ tox -e generate-config-from-jsonschema
22
+ ```
23
+
24
+ 3. Update any version string references in tests and source:
25
+
26
+ ```sh
27
+ grep -r "OLD_VERSION" opentelemetry-configuration/
28
+ ```
@@ -0,0 +1,42 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """OpenTelemetry SDK Declarative Configuration.
5
+
6
+ This package implements the OpenTelemetry declarative configuration
7
+ specification for the Python SDK. Load a YAML or JSON configuration file
8
+ (or build a configuration model programmatically) and apply it to the
9
+ process-global SDK providers.
10
+
11
+ The standard activation path is the ``OTEL_CONFIG_FILE`` environment
12
+ variable, which the SDK's configurator picks up automatically. For
13
+ programmatic use:
14
+
15
+ >>> from opentelemetry._configuration import (
16
+ ... load_config_file, configure_sdk,
17
+ ... )
18
+ >>> config = load_config_file("otel-config.yaml")
19
+ >>> configure_sdk(config)
20
+
21
+ Construct a configuration programmatically and apply it:
22
+
23
+ >>> from opentelemetry._configuration import (
24
+ ... OpenTelemetryConfiguration, configure_sdk,
25
+ ... )
26
+ >>> configure_sdk(OpenTelemetryConfiguration(file_format="1.0-rc.1"))
27
+
28
+ This package is **experimental**. The API surface, type names, and
29
+ behaviour may change between minor versions.
30
+ """
31
+
32
+ from opentelemetry._configuration._exceptions import ConfigurationError
33
+ from opentelemetry._configuration._sdk import configure_sdk
34
+ from opentelemetry._configuration.file._loader import load_config_file
35
+ from opentelemetry._configuration.models import OpenTelemetryConfiguration
36
+
37
+ __all__ = [
38
+ "ConfigurationError",
39
+ "OpenTelemetryConfiguration",
40
+ "configure_sdk",
41
+ "load_config_file",
42
+ ]
@@ -0,0 +1,238 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ from __future__ import annotations
5
+
6
+ import dataclasses
7
+ import inspect
8
+ import logging
9
+ from collections.abc import Callable
10
+ from typing import Any, Protocol
11
+ from urllib.parse import urlparse
12
+
13
+ from opentelemetry._configuration._exceptions import ConfigurationError
14
+ from opentelemetry.util._importlib_metadata import entry_points
15
+
16
+ _logger = logging.getLogger(__name__)
17
+
18
+
19
+ def _additional_properties(cls):
20
+ """Decorator for dataclasses whose JSON Schema sets additionalProperties.
21
+
22
+ Wraps the dataclass-generated ``__init__`` so that extra keyword
23
+ arguments are captured into an ``additional_properties`` instance
24
+ attribute instead of raising ``TypeError``. This lets user-defined
25
+ component names flow through the config pipeline without modifying
26
+ the codegen output for built-in fields.
27
+
28
+ Applied automatically by the custom template in ``opentelemetry-sdk/codegen/``
29
+ when ``additionalPropertiesType`` is present in the template context
30
+ (set by ``datamodel-codegen`` for schema types with ``additionalProperties``).
31
+ """
32
+ original_init = cls.__init__
33
+ original_sig = inspect.signature(original_init)
34
+ known_fields = frozenset(f.name for f in dataclasses.fields(cls))
35
+
36
+ def _init(self, **kwargs):
37
+ known = {k: v for k, v in kwargs.items() if k in known_fields}
38
+ extra = {k: v for k, v in kwargs.items() if k not in known_fields}
39
+ original_init(self, **known)
40
+ self.additional_properties = extra
41
+
42
+ # Preserve the original parameter list for IDE autocompletion and
43
+ # inspect.signature(), adding **kwargs to signal extras are accepted.
44
+ # setattr used because pyright rejects direct __signature__ assignment.
45
+ params = list(original_sig.parameters.values())
46
+ params.append(inspect.Parameter("kwargs", inspect.Parameter.VAR_KEYWORD))
47
+ setattr(_init, "__signature__", original_sig.replace(parameters=params)) # noqa: B010
48
+
49
+ cls.__init__ = _init
50
+ return cls
51
+
52
+
53
+ def load_entry_point(group: str, name: str) -> type:
54
+ """Load a plugin class from an entry point group by name.
55
+
56
+ Returns the loaded class — callers are responsible for instantiation
57
+ with whatever arguments their config requires.
58
+
59
+ Raises:
60
+ ConfigurationError: If the entry point is not found or fails to load.
61
+ """
62
+ try:
63
+ ep = next(iter(entry_points(group=group, name=name)), None)
64
+ if ep is None:
65
+ raise ConfigurationError(
66
+ f"Plugin '{name}' not found in group '{group}'. "
67
+ "Make sure the package providing this plugin is installed."
68
+ )
69
+ return ep.load()
70
+ except ConfigurationError:
71
+ raise
72
+ except Exception as exc:
73
+ raise ConfigurationError(
74
+ f"Failed to load plugin '{name}' from group '{group}': {exc}"
75
+ ) from exc
76
+
77
+
78
+ class _ComponentConfig(Protocol):
79
+ """Protocol for config dataclasses decorated with @_additional_properties.
80
+
81
+ Values in ``additional_properties`` are nested config dicts (suitable
82
+ for ``**kwargs`` splatting to the user-defined component class) or
83
+ ``None`` (when the YAML uses ``my_plugin:`` or ``my_plugin: null``).
84
+
85
+ Note: the generated models declare ``additional_properties`` as a
86
+ ``ClassVar`` even though the decorator assigns it as an instance
87
+ attribute at runtime. This is tolerated by pyright in ``standard``
88
+ mode but flagged in ``strict`` mode. See #5268.
89
+ """
90
+
91
+ additional_properties: dict[str, dict[str, Any] | None]
92
+
93
+
94
+ def _resolve_component(
95
+ config: _ComponentConfig,
96
+ registry: dict[str, Callable[[Any], Any]],
97
+ entry_point_group: str,
98
+ component_type: str,
99
+ ) -> Any:
100
+ """Resolve a config dataclass to a component instance.
101
+
102
+ Checks built-in factories in ``registry`` first (by matching typed
103
+ field names on ``config``), then falls back to entry point loading
104
+ for plugin components found in ``config.additional_properties``.
105
+
106
+ The JSON schema enforces exactly one component per config block
107
+ (``minProperties: 1, maxProperties: 1``). If multiple typed fields or
108
+ ``additional_properties`` entries are set (e.g. when schema validation
109
+ is bypassed), the first registry match wins.
110
+
111
+ Args:
112
+ config: A dataclass with ``additional_properties`` (from the
113
+ ``@_additional_properties`` decorator).
114
+ registry: Mapping of built-in component names to factory
115
+ callables. Each factory receives the field value from config.
116
+ entry_point_group: The entry point group name for plugin loading.
117
+ component_type: Human-readable name for error messages
118
+ (e.g. "span exporter").
119
+
120
+ Returns:
121
+ The resolved component instance.
122
+
123
+ Raises:
124
+ ConfigurationError: If no component type is specified in config.
125
+ """
126
+ for name, factory in registry.items():
127
+ value = getattr(config, name, None)
128
+ if value is not None:
129
+ return factory(value)
130
+ if config.additional_properties:
131
+ name, plugin_config = next(iter(config.additional_properties.items()))
132
+ return load_entry_point(entry_point_group, name)(
133
+ **(plugin_config or {})
134
+ )
135
+ raise ConfigurationError(f"No {component_type} type specified in config.")
136
+
137
+
138
+ def _parse_headers(
139
+ headers: list | None,
140
+ headers_list: str | None,
141
+ ) -> dict[str, str] | None:
142
+ """Merge headers struct and headers_list into a dict.
143
+
144
+ Returns None if neither is set, letting the exporter read env vars.
145
+ headers struct takes priority over headers_list for the same key.
146
+ """
147
+ if headers is None and headers_list is None:
148
+ return None
149
+ result: dict[str, str] = {}
150
+ if headers_list:
151
+ for item in headers_list.split(","):
152
+ item = item.strip()
153
+ if "=" in item:
154
+ key, value = item.split("=", 1)
155
+ result[key.strip()] = value.strip()
156
+ elif item:
157
+ _logger.warning(
158
+ "Invalid header pair in headers_list (missing '='): %s",
159
+ item,
160
+ )
161
+ if headers:
162
+ for pair in headers:
163
+ if isinstance(pair, dict):
164
+ result[pair["name"]] = pair.get("value") or ""
165
+ else:
166
+ result[pair.name] = pair.value or ""
167
+ return result
168
+
169
+
170
+ def _map_compression(
171
+ value: str | None,
172
+ compression_enum: type,
173
+ *,
174
+ allow_deflate: bool = False,
175
+ ) -> object | None:
176
+ """Map a compression string to the given Compression enum value."""
177
+ if value is None:
178
+ return None
179
+
180
+ value_lower = value.lower()
181
+ supports_deflate = hasattr(compression_enum, "Deflate")
182
+
183
+ if value_lower == "none":
184
+ return None
185
+ if value_lower == "gzip":
186
+ return compression_enum.Gzip # type: ignore[attr-defined]
187
+ if value_lower == "deflate" and allow_deflate and supports_deflate:
188
+ return compression_enum.Deflate # type: ignore[attr-defined]
189
+
190
+ supported_values = ["'gzip'", "'none'"]
191
+ if allow_deflate and supports_deflate:
192
+ supported_values.insert(1, "'deflate'")
193
+
194
+ raise ConfigurationError(
195
+ f"Unsupported compression value '{value}'. Supported values: "
196
+ f"{', '.join(supported_values)}."
197
+ )
198
+
199
+
200
+ def _parse_otlp_file_output_stream(output_stream: str | None) -> str | None:
201
+ """Resolve an output_stream value to a file path, or None for stdout.
202
+
203
+ Per the OTel file exporter spec, output_stream is "stdout" (or
204
+ None, which means the same), or a "file://" URI giving a path.
205
+ """
206
+ if output_stream is None or output_stream == "stdout":
207
+ return None
208
+ try:
209
+ parsed = urlparse(output_stream)
210
+ except ValueError as exc:
211
+ raise ConfigurationError(
212
+ f"Failed to parse output_stream '{output_stream}' for "
213
+ f"otlp_file_development exporter: {exc}"
214
+ ) from exc
215
+ is_local_file_uri = (
216
+ parsed.scheme == "file"
217
+ and parsed.netloc in ("", "localhost")
218
+ and bool(parsed.path)
219
+ )
220
+ has_extra_components = parsed.params or parsed.query or parsed.fragment
221
+ if is_local_file_uri and not has_extra_components:
222
+ path = parsed.path
223
+ if not path.startswith("/"):
224
+ raise ConfigurationError(
225
+ f"Unsupported output_stream '{output_stream}' for "
226
+ "otlp_file_development exporter. Path must be absolute."
227
+ )
228
+ if path.endswith("/"):
229
+ raise ConfigurationError(
230
+ f"Unsupported output_stream '{output_stream}' for "
231
+ "otlp_file_development exporter. Path must be a file, "
232
+ "not a directory."
233
+ )
234
+ return path
235
+ raise ConfigurationError(
236
+ f"Unsupported output_stream '{output_stream}' for otlp_file_development "
237
+ "exporter. Supported values: stdout, file://<path>."
238
+ )
@@ -0,0 +1,118 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Recursive dict-to-dataclass conversion for parsed config data.
5
+
6
+ The YAML/JSON loader produces nested dicts. Factory functions expect typed
7
+ dataclass instances (e.g. ``TracerProvider``, ``SpanProcessor``). This module
8
+ walks each field's type annotation and converts nested dicts into their
9
+ corresponding dataclass types.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Mapping
15
+ from dataclasses import fields, is_dataclass
16
+ from enum import Enum
17
+ from types import UnionType
18
+ from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
19
+
20
+ _T = TypeVar("_T")
21
+
22
+
23
+ def _unwrap_optional(type_hint: Any) -> Any:
24
+ """Strip ``None`` from a ``X | None`` / ``Optional[X]`` annotation.
25
+
26
+ Returns the unwrapped type, or the original hint if not a union with None.
27
+ """
28
+ origin = get_origin(type_hint)
29
+ if origin is UnionType or origin is Union:
30
+ non_none = [t for t in get_args(type_hint) if t is not type(None)]
31
+ if len(non_none) == 1:
32
+ return non_none[0]
33
+ return type_hint
34
+
35
+
36
+ def _convert_value(value: Any, type_hint: Any) -> Any:
37
+ """Convert a value according to its type hint.
38
+
39
+ Recursively converts dicts to dataclasses and lists of dicts to lists of
40
+ dataclasses. Other values (primitives, enums, ``dict[str, Any]`` aliases)
41
+ pass through unchanged.
42
+ """
43
+ if value is None:
44
+ return None
45
+
46
+ unwrapped = _unwrap_optional(type_hint)
47
+ origin = get_origin(unwrapped)
48
+
49
+ # list[X] — recurse on each element
50
+ if origin is list and isinstance(value, list):
51
+ args = get_args(unwrapped)
52
+ if args:
53
+ item_type = args[0]
54
+ return [_convert_value(item, item_type) for item in value]
55
+ return value
56
+
57
+ # Direct dataclass type — recurse
58
+ if (
59
+ isinstance(unwrapped, type)
60
+ and is_dataclass(unwrapped)
61
+ and isinstance(value, dict)
62
+ ):
63
+ return _dict_to_dataclass(value, unwrapped)
64
+
65
+ # Enum type — coerce string/value to the Enum member
66
+ if (
67
+ isinstance(unwrapped, type)
68
+ and issubclass(unwrapped, Enum)
69
+ and not isinstance(value, unwrapped)
70
+ ):
71
+ return unwrapped(value)
72
+
73
+ return value
74
+
75
+
76
+ def _dict_to_dataclass(data: Mapping[str, Any], cls: type[_T]) -> _T:
77
+ """Recursively convert a mapping to a dataclass instance.
78
+
79
+ For each key in ``data``:
80
+ - If it matches a known dataclass field, the value is converted according
81
+ to that field's type annotation (recursing for nested dataclasses).
82
+ - Unknown keys are passed through as kwargs; classes decorated with
83
+ ``@_additional_properties`` will capture them on the instance's
84
+ ``additional_properties`` attribute.
85
+
86
+ ``ClassVar`` fields (e.g. the ``additional_properties`` annotation on
87
+ decorated dataclasses) are ignored as expected.
88
+
89
+ Raises:
90
+ TypeError: If ``cls`` is not a dataclass type.
91
+ """
92
+ if not is_dataclass(cls):
93
+ raise TypeError(f"{cls.__name__} is not a dataclass")
94
+
95
+ # Annotated as ``dict[str, Any]`` so astroid stops tracing into
96
+ # ``get_type_hints`` — under pylint 3.x that path leads into
97
+ # Python 3.14's ``annotationlib`` (which uses t-strings) and crashes.
98
+ hints: dict[str, Any] = dict(get_type_hints(cls, include_extras=False))
99
+ known_fields = {f.name for f in fields(cls)}
100
+ kwargs: dict[str, Any] = {}
101
+
102
+ for key, value in data.items():
103
+ # The OTel configuration schema uses "/" as a namespace separator for
104
+ # development/experimental features (e.g. "otlp_file/development",
105
+ # "instrumentation/development"). Python identifiers cannot contain
106
+ # "/", so the corresponding dataclass fields use "_" instead (e.g.
107
+ # "otlp_file_development"). Without this normalisation the key would
108
+ # not match any known field and would fall through to
109
+ # additional_properties, causing the factory lookup to fail silently.
110
+ field_key = key.replace("/", "_")
111
+ if field_key in known_fields:
112
+ type_hint = hints.get(field_key)
113
+ kwargs[field_key] = _convert_value(value, type_hint)
114
+ else:
115
+ # Unknown key — @_additional_properties decorator will capture it.
116
+ kwargs[key] = value
117
+
118
+ return cls(**kwargs)
@@ -0,0 +1,59 @@
1
+ # Copyright The OpenTelemetry Authors
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+
5
+ class ConfigurationError(Exception):
6
+ """Raised when configuration loading, parsing, validation, or instantiation fails.
7
+
8
+ This includes errors from:
9
+ - File not found or inaccessible
10
+ - Invalid YAML/JSON syntax
11
+ - Schema validation failures
12
+ - Environment variable substitution errors
13
+ - Missing required SDK extensions (e.g., propagator packages not installed)
14
+ """
15
+
16
+
17
+ class MissingDependencyError(ConfigurationError, ImportError):
18
+ """Raised when an optional dependency is not installed.
19
+
20
+ Inherits from both :class:`ConfigurationError` and :class:`ImportError` to
21
+ maintain backwards compatibility with callers that catch ``ImportError``.
22
+
23
+ Args:
24
+ package: The name of the missing package.
25
+ feature: Optional description of the feature that requires the package.
26
+ install_name: Optional package name used in the pip install command
27
+ (defaults to ``package``).
28
+ extras: Optional extras string for the pip install command.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ package: str,
34
+ feature: str | None = None,
35
+ install_name: str | None = None,
36
+ extras: str | None = None,
37
+ ) -> None:
38
+ self.package = package
39
+ self.feature = feature
40
+ self.install_name = install_name or package
41
+ self.extras = extras
42
+
43
+ if extras:
44
+ install_cmd = f'pip install "{self.install_name}[{extras}]"'
45
+ else:
46
+ install_cmd = f"pip install {self.install_name}"
47
+
48
+ if feature:
49
+ message = (
50
+ f"{feature} requires '{package}'. "
51
+ f"Install it with: {install_cmd}"
52
+ )
53
+ else:
54
+ message = (
55
+ f"'{package}' is required but not installed. "
56
+ f"Install it with: {install_cmd}"
57
+ )
58
+
59
+ super().__init__(message)