opentelemetry-configuration 0.65b0__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 (42) hide show
  1. opentelemetry_configuration-0.65b0/.gitignore +73 -0
  2. opentelemetry_configuration-0.65b0/PKG-INFO +73 -0
  3. opentelemetry_configuration-0.65b0/README.rst +47 -0
  4. opentelemetry_configuration-0.65b0/pyproject.toml +49 -0
  5. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/README.md +28 -0
  6. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/__init__.py +42 -0
  7. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/_common.py +238 -0
  8. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/_conversion.py +118 -0
  9. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/_exceptions.py +59 -0
  10. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/_logger_provider.py +288 -0
  11. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/_meter_provider.py +626 -0
  12. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/_propagator.py +106 -0
  13. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/_resource.py +219 -0
  14. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/_sdk.py +68 -0
  15. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/_tracer_provider.py +508 -0
  16. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/file/__init__.py +60 -0
  17. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/file/_env_substitution.py +92 -0
  18. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/file/_loader.py +268 -0
  19. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/instrumentation.py +78 -0
  20. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/models.py +835 -0
  21. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/schema.json +2576 -0
  22. opentelemetry_configuration-0.65b0/src/opentelemetry/configuration/version/__init__.py +4 -0
  23. opentelemetry_configuration-0.65b0/tests/__init__.py +2 -0
  24. opentelemetry_configuration-0.65b0/tests/file/__init__.py +2 -0
  25. opentelemetry_configuration-0.65b0/tests/file/data/config_with_env_vars.yaml +7 -0
  26. opentelemetry_configuration-0.65b0/tests/file/data/invalid_yaml.yaml +3 -0
  27. opentelemetry_configuration-0.65b0/tests/file/data/minimal_config.json +3 -0
  28. opentelemetry_configuration-0.65b0/tests/file/data/minimal_config.yaml +1 -0
  29. opentelemetry_configuration-0.65b0/tests/file/test_env_substitution.py +150 -0
  30. opentelemetry_configuration-0.65b0/tests/file/test_loader.py +369 -0
  31. opentelemetry_configuration-0.65b0/tests/test_common.py +461 -0
  32. opentelemetry_configuration-0.65b0/tests/test_conversion.py +110 -0
  33. opentelemetry_configuration-0.65b0/tests/test_exceptions.py +74 -0
  34. opentelemetry_configuration-0.65b0/tests/test_instrumentation.py +250 -0
  35. opentelemetry_configuration-0.65b0/tests/test_logger_provider.py +496 -0
  36. opentelemetry_configuration-0.65b0/tests/test_meter_provider.py +977 -0
  37. opentelemetry_configuration-0.65b0/tests/test_meter_provider_exemplar_filter.py +58 -0
  38. opentelemetry_configuration-0.65b0/tests/test_models.py +12 -0
  39. opentelemetry_configuration-0.65b0/tests/test_propagator.py +301 -0
  40. opentelemetry_configuration-0.65b0/tests/test_resource.py +622 -0
  41. opentelemetry_configuration-0.65b0/tests/test_sdk.py +149 -0
  42. opentelemetry_configuration-0.65b0/tests/test_tracer_provider.py +909 -0
@@ -0,0 +1,73 @@
1
+ *.py[cod]
2
+ *.sw[op]
3
+
4
+ # C extensions
5
+ *.so
6
+
7
+ # Packages
8
+ *.egg
9
+ *.egg-info
10
+ dist
11
+ build
12
+ eggs
13
+ parts
14
+ bin
15
+ include
16
+ var
17
+ sdist
18
+ develop-eggs
19
+ .installed.cfg
20
+ pyvenv.cfg
21
+ lib
22
+ share/
23
+ lib64
24
+ __pycache__
25
+ venv*/
26
+ .venv*/
27
+
28
+ # Installer logs
29
+ pip-log.txt
30
+
31
+ # Unit test / coverage reports
32
+ coverage.xml
33
+ .coverage
34
+ .nox
35
+ .tox
36
+ .cache
37
+ htmlcov
38
+
39
+ .agent/
40
+
41
+ # Translations
42
+ *.mo
43
+
44
+ # Mac
45
+ .DS_Store
46
+
47
+ # Mr Developer
48
+ .mr.developer.cfg
49
+ .project
50
+ .pydevproject
51
+
52
+ # JetBrains
53
+ .idea
54
+
55
+ # VSCode
56
+ .vscode
57
+
58
+ # Sphinx
59
+ _build/
60
+
61
+ # mypy
62
+ .mypy_cache/
63
+ target
64
+
65
+ # Django example
66
+
67
+ docs/examples/django/db.sqlite3
68
+
69
+ # Semantic conventions
70
+ scripts/semconv/semantic-conventions
71
+
72
+ # Benchmark result files
73
+ *-benchmark.json
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: opentelemetry-configuration
3
+ Version: 0.65b0
4
+ Summary: OpenTelemetry Python Declarative Configuration (experimental)
5
+ Project-URL: Homepage, https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-configuration
6
+ Project-URL: Repository, https://github.com/open-telemetry/opentelemetry-python
7
+ Author-email: OpenTelemetry Authors <cncf-opentelemetry-contributors@lists.cncf.io>
8
+ License-Expression: Apache-2.0
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Framework :: OpenTelemetry
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: jsonschema>=4.0
22
+ Requires-Dist: opentelemetry-api==1.44.0
23
+ Requires-Dist: opentelemetry-sdk==1.44.0
24
+ Requires-Dist: pyyaml>=5.4
25
+ Description-Content-Type: text/x-rst
26
+
27
+ OpenTelemetry Python SDK Declarative Configuration
28
+ ===================================================
29
+
30
+ |pypi|
31
+
32
+ .. |pypi| image:: https://badge.fury.io/py/opentelemetry-configuration.svg
33
+ :target: https://pypi.org/project/opentelemetry-configuration/
34
+
35
+ .. warning::
36
+
37
+ This package is **experimental**. The API surface, type names, and behaviour
38
+ may change between minor versions. Pin a specific version in production.
39
+
40
+ This package implements the `OpenTelemetry declarative configuration
41
+ specification <https://opentelemetry.io/docs/specs/otel/configuration/>`_ for
42
+ the Python SDK. It parses a YAML or JSON configuration file (or
43
+ programmatically-constructed configuration model) into typed dataclasses and
44
+ applies the result to the global SDK providers.
45
+
46
+ The standard activation path is the ``OTEL_CONFIG_FILE`` environment variable,
47
+ which the SDK's configurator picks up automatically. Set the variable and run
48
+ your app under ``opentelemetry-instrument``; no code change is required.
49
+
50
+ For programmatic use:
51
+
52
+ .. code-block:: python
53
+
54
+ from opentelemetry.configuration import configure_sdk, load_config_file
55
+
56
+ config = load_config_file("otel-config.yaml")
57
+ configure_sdk(config)
58
+
59
+ Installation
60
+ ------------
61
+
62
+ ::
63
+
64
+ pip install opentelemetry-configuration
65
+
66
+ References
67
+ ----------
68
+
69
+ * `OpenTelemetry declarative configuration specification
70
+ <https://opentelemetry.io/docs/specs/otel/configuration/>`_
71
+ * `Language support status matrix
72
+ <https://github.com/open-telemetry/opentelemetry-configuration/blob/main/language-support-status.md#python>`_
73
+ * `OpenTelemetry Project <https://opentelemetry.io/>`_
@@ -0,0 +1,47 @@
1
+ OpenTelemetry Python SDK Declarative Configuration
2
+ ===================================================
3
+
4
+ |pypi|
5
+
6
+ .. |pypi| image:: https://badge.fury.io/py/opentelemetry-configuration.svg
7
+ :target: https://pypi.org/project/opentelemetry-configuration/
8
+
9
+ .. warning::
10
+
11
+ This package is **experimental**. The API surface, type names, and behaviour
12
+ may change between minor versions. Pin a specific version in production.
13
+
14
+ This package implements the `OpenTelemetry declarative configuration
15
+ specification <https://opentelemetry.io/docs/specs/otel/configuration/>`_ for
16
+ the Python SDK. It parses a YAML or JSON configuration file (or
17
+ programmatically-constructed configuration model) into typed dataclasses and
18
+ applies the result to the global SDK providers.
19
+
20
+ The standard activation path is the ``OTEL_CONFIG_FILE`` environment variable,
21
+ which the SDK's configurator picks up automatically. Set the variable and run
22
+ your app under ``opentelemetry-instrument``; no code change is required.
23
+
24
+ For programmatic use:
25
+
26
+ .. code-block:: python
27
+
28
+ from opentelemetry.configuration import configure_sdk, load_config_file
29
+
30
+ config = load_config_file("otel-config.yaml")
31
+ configure_sdk(config)
32
+
33
+ Installation
34
+ ------------
35
+
36
+ ::
37
+
38
+ pip install opentelemetry-configuration
39
+
40
+ References
41
+ ----------
42
+
43
+ * `OpenTelemetry declarative configuration specification
44
+ <https://opentelemetry.io/docs/specs/otel/configuration/>`_
45
+ * `Language support status matrix
46
+ <https://github.com/open-telemetry/opentelemetry-configuration/blob/main/language-support-status.md#python>`_
47
+ * `OpenTelemetry Project <https://opentelemetry.io/>`_
@@ -0,0 +1,49 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "opentelemetry-configuration"
7
+ dynamic = ["version"]
8
+ description = "OpenTelemetry Python Declarative Configuration (experimental)"
9
+ readme = "README.rst"
10
+ license = "Apache-2.0"
11
+ requires-python = ">=3.10"
12
+ authors = [
13
+ { name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Framework :: OpenTelemetry",
18
+ "Intended Audience :: Developers",
19
+ "Programming Language :: Python",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Programming Language :: Python :: 3.13",
25
+ "Programming Language :: Python :: 3.14",
26
+ "Typing :: Typed",
27
+ ]
28
+ dependencies = [
29
+ "opentelemetry-api == 1.44.0",
30
+ "opentelemetry-sdk == 1.44.0",
31
+ "pyyaml >= 5.4",
32
+ "jsonschema >= 4.0",
33
+ ]
34
+
35
+ [project.urls]
36
+ Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-configuration"
37
+ Repository = "https://github.com/open-telemetry/opentelemetry-python"
38
+
39
+ [tool.hatch.version]
40
+ path = "src/opentelemetry/configuration/version/__init__.py"
41
+
42
+ [tool.hatch.build.targets.sdist]
43
+ include = [
44
+ "/src",
45
+ "/tests",
46
+ ]
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/opentelemetry"]
@@ -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)