Skip to content

Commit 2dfc7c9

Browse files
feat(sdk): add MissingDependencyError for declarative configuration (#5265)
* feat(sdk): add MissingDependencyError for declarative configuration dependency checking Implements a structured MissingDependencyError (subclass of ConfigurationError) that generates a descriptive pip install message. Replaces ad-hoc ConfigurationError / ImportError raises across all declarative config providers (tracer, logger, meter, file loader) for missing optional dependencies like OTLP exporters, Prometheus reader, pyyaml, and jsonschema. Adds unit tests covering the new exception class. Closes #5229 * fix: address PR #5265 review comments - Rename .changelog/5229.added -> .changelog/5265.added - Make MissingDependencyError inherit from both ConfigurationError and ImportError - Add docstring hints for constructor parameters - Use double quotes in pip install command for Windows compatibility Assisted-by: Claude * remove unnecessary test --------- Co-authored-by: Mike Goldsmith <goldsmith.mike@gmail.com>
1 parent 634cec5 commit 2dfc7c9

8 files changed

Lines changed: 172 additions & 33 deletions

File tree

.changelog/5265.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-sdk`: add `MissingDependencyError` exception for declarative configuration and use it for missing optional dependency errors

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,48 @@ class ConfigurationError(Exception):
1212
- Environment variable substitution errors
1313
- Missing required SDK extensions (e.g., propagator packages not installed)
1414
"""
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)

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
_parse_otlp_file_output_stream,
1313
load_entry_point,
1414
)
15-
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
15+
from opentelemetry.sdk._configuration._exceptions import (
16+
ConfigurationError,
17+
MissingDependencyError,
18+
)
1619
from opentelemetry.sdk._configuration.models import (
1720
BatchLogRecordProcessor as BatchLogRecordProcessorConfig,
1821
)
@@ -73,9 +76,9 @@ def _create_otlp_http_log_exporter(
7376
OTLPLogExporter,
7477
)
7578
except ImportError as exc:
76-
raise ConfigurationError(
77-
"otlp_http log exporter requires 'opentelemetry-exporter-otlp-proto-http'. "
78-
"Install it with: pip install opentelemetry-exporter-otlp-proto-http"
79+
raise MissingDependencyError(
80+
package="opentelemetry-exporter-otlp-proto-http",
81+
feature="otlp_http log exporter",
7982
) from exc
8083

8184
compression = _map_compression(
@@ -104,9 +107,9 @@ def _create_otlp_grpc_log_exporter(
104107
OTLPLogExporter,
105108
)
106109
except ImportError as exc:
107-
raise ConfigurationError(
108-
"otlp_grpc log exporter requires 'opentelemetry-exporter-otlp-proto-grpc'. "
109-
"Install it with: pip install opentelemetry-exporter-otlp-proto-grpc"
110+
raise MissingDependencyError(
111+
package="opentelemetry-exporter-otlp-proto-grpc",
112+
feature="otlp_grpc log exporter",
110113
) from exc
111114

112115
compression = _map_compression(config.compression, grpc.Compression)

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

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
_parse_otlp_file_output_stream,
1313
load_entry_point,
1414
)
15-
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
15+
from opentelemetry.sdk._configuration._exceptions import (
16+
ConfigurationError,
17+
MissingDependencyError,
18+
)
1619
from opentelemetry.sdk._configuration.models import (
1720
Aggregation as AggregationConfig,
1821
)
@@ -283,9 +286,9 @@ def _create_otlp_http_metric_exporter(
283286
OTLPMetricExporter,
284287
)
285288
except ImportError as exc:
286-
raise ConfigurationError(
287-
"otlp_http metric exporter requires 'opentelemetry-exporter-otlp-proto-http'. "
288-
"Install it with: pip install opentelemetry-exporter-otlp-proto-http"
289+
raise MissingDependencyError(
290+
package="opentelemetry-exporter-otlp-proto-http",
291+
feature="otlp_http metric exporter",
289292
) from exc
290293

291294
compression = _map_compression(
@@ -320,9 +323,9 @@ def _create_otlp_grpc_metric_exporter(
320323
OTLPMetricExporter,
321324
)
322325
except ImportError as exc:
323-
raise ConfigurationError(
324-
"otlp_grpc metric exporter requires 'opentelemetry-exporter-otlp-proto-grpc'. "
325-
"Install it with: pip install opentelemetry-exporter-otlp-proto-grpc"
326+
raise MissingDependencyError(
327+
package="opentelemetry-exporter-otlp-proto-grpc",
328+
feature="otlp_grpc metric exporter",
326329
) from exc
327330

328331
compression = _map_compression(config.compression, grpc.Compression)
@@ -451,10 +454,9 @@ def _create_prometheus_metric_reader(
451454
start_http_server,
452455
)
453456
except ImportError as exc:
454-
raise ConfigurationError(
455-
"prometheus pull metric exporter requires "
456-
"'opentelemetry-exporter-prometheus'. "
457-
"Install it with: pip install opentelemetry-exporter-prometheus"
457+
raise MissingDependencyError(
458+
package="opentelemetry-exporter-prometheus",
459+
feature="prometheus pull metric exporter",
458460
) from exc
459461

460462
disable_target_info = (

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
_parse_otlp_file_output_stream,
1313
load_entry_point,
1414
)
15-
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
15+
from opentelemetry.sdk._configuration._exceptions import (
16+
ConfigurationError,
17+
MissingDependencyError,
18+
)
1619
from opentelemetry.sdk._configuration.models import (
1720
ExperimentalComposableRuleBasedSampler as RuleBasedSamplerConfig,
1821
)
@@ -116,9 +119,9 @@ def _create_otlp_http_span_exporter(
116119
OTLPSpanExporter,
117120
)
118121
except ImportError as exc:
119-
raise ConfigurationError(
120-
"otlp_http span exporter requires 'opentelemetry-exporter-otlp-proto-http'. "
121-
"Install it with: pip install opentelemetry-exporter-otlp-proto-http"
122+
raise MissingDependencyError(
123+
package="opentelemetry-exporter-otlp-proto-http",
124+
feature="otlp_http span exporter",
122125
) from exc
123126

124127
compression = _map_compression(
@@ -147,9 +150,9 @@ def _create_otlp_grpc_span_exporter(
147150
OTLPSpanExporter,
148151
)
149152
except ImportError as exc:
150-
raise ConfigurationError(
151-
"otlp_grpc span exporter requires 'opentelemetry-exporter-otlp-proto-grpc'. "
152-
"Install it with: pip install opentelemetry-exporter-otlp-proto-grpc"
153+
raise MissingDependencyError(
154+
package="opentelemetry-exporter-otlp-proto-grpc",
155+
feature="otlp_grpc span exporter",
153156
) from exc
154157

155158
compression = _map_compression(config.compression, grpc.Compression)

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
'1.0'
1414
"""
1515

16-
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
16+
from opentelemetry.sdk._configuration._exceptions import (
17+
ConfigurationError,
18+
MissingDependencyError,
19+
)
1720
from opentelemetry.sdk._configuration._logger_provider import (
1821
configure_logger_provider,
1922
create_logger_provider,
@@ -43,6 +46,7 @@
4346
"configure_sdk",
4447
"substitute_env_vars",
4548
"ConfigurationError",
49+
"MissingDependencyError",
4650
"EnvSubstitutionError",
4751
"create_resource",
4852
"create_propagator",

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from typing import Any
1212

1313
from opentelemetry.sdk._configuration._conversion import _dict_to_dataclass
14-
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
14+
from opentelemetry.sdk._configuration._exceptions import (
15+
ConfigurationError,
16+
MissingDependencyError,
17+
)
1518
from opentelemetry.sdk._configuration.file._env_substitution import (
1619
substitute_env_vars,
1720
)
@@ -20,17 +23,21 @@
2023
try:
2124
import yaml
2225
except ImportError as exc:
23-
raise ImportError(
24-
"File configuration requires pyyaml. "
25-
"Install with: pip install opentelemetry-sdk[file-configuration]"
26+
raise MissingDependencyError(
27+
package="pyyaml",
28+
feature="File configuration",
29+
install_name="opentelemetry-sdk",
30+
extras="file-configuration",
2631
) from exc
2732

2833
try:
2934
import jsonschema
3035
except ImportError as exc:
31-
raise ImportError(
32-
"File configuration requires jsonschema. "
33-
"Install with: pip install opentelemetry-sdk[file-configuration]"
36+
raise MissingDependencyError(
37+
package="jsonschema",
38+
feature="File configuration",
39+
install_name="opentelemetry-sdk",
40+
extras="file-configuration",
3441
) from exc
3542

3643
# Schema version vendored in schema.json. ``file_format`` values are accepted
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Copyright The OpenTelemetry Authors
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import unittest
5+
6+
from opentelemetry.sdk._configuration._exceptions import (
7+
ConfigurationError,
8+
MissingDependencyError,
9+
)
10+
11+
12+
class TestMissingDependencyError(unittest.TestCase):
13+
def test_is_configuration_error_subclass(self):
14+
self.assertTrue(issubclass(MissingDependencyError, ConfigurationError))
15+
16+
def test_minimal_constructor(self):
17+
exc = MissingDependencyError(package="foo")
18+
self.assertEqual(exc.package, "foo")
19+
self.assertIsNone(exc.feature)
20+
self.assertEqual(exc.install_name, "foo")
21+
self.assertIsNone(exc.extras)
22+
self.assertIn("'foo'", str(exc))
23+
self.assertIn("pip install foo", str(exc))
24+
25+
def test_with_feature(self):
26+
exc = MissingDependencyError(package="bar", feature="Baz exporter")
27+
self.assertEqual(exc.package, "bar")
28+
self.assertEqual(exc.feature, "Baz exporter")
29+
self.assertIn("Baz exporter requires 'bar'", str(exc))
30+
self.assertIn("pip install bar", str(exc))
31+
32+
def test_with_custom_install_name(self):
33+
exc = MissingDependencyError(
34+
package="pyyaml",
35+
install_name="opentelemetry-sdk",
36+
extras="file-configuration",
37+
)
38+
self.assertEqual(exc.install_name, "opentelemetry-sdk")
39+
self.assertEqual(exc.extras, "file-configuration")
40+
self.assertIn(
41+
'pip install "opentelemetry-sdk[file-configuration]"', str(exc)
42+
)
43+
44+
def test_with_feature_and_extras(self):
45+
exc = MissingDependencyError(
46+
package="jsonschema",
47+
feature="File configuration",
48+
install_name="opentelemetry-sdk",
49+
extras="file-configuration",
50+
)
51+
self.assertIn("File configuration requires 'jsonschema'", str(exc))
52+
self.assertIn(
53+
'pip install "opentelemetry-sdk[file-configuration]"', str(exc)
54+
)
55+
56+
def test_can_be_caught_as_configuration_error(self):
57+
with self.assertRaises(ConfigurationError):
58+
raise MissingDependencyError(package="test")
59+
60+
def test_can_be_caught_as_exception(self):
61+
with self.assertRaises(Exception):
62+
raise MissingDependencyError(package="test")
63+
64+
def test_can_be_caught_as_import_error(self):
65+
with self.assertRaises(ImportError):
66+
raise MissingDependencyError(package="test")
67+
68+
def test_is_import_error_subclass(self):
69+
self.assertTrue(issubclass(MissingDependencyError, ImportError))
70+
71+
def test_issubclass_import_error(self):
72+
self.assertIsInstance(
73+
MissingDependencyError(package="test"), ImportError
74+
)

0 commit comments

Comments
 (0)