Skip to content

Commit d7e5dd5

Browse files
authored
Merge branch 'main' into extended_attributes
2 parents 16def67 + 0345b41 commit d7e5dd5

11 files changed

Lines changed: 272 additions & 44 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

.changelog/5364.fixed

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
`opentelemetry-sdk`: `ProcessResourceDetector` no longer collects or emits
2+
`process.command_args` and `process.command_line` by default since the values
3+
are not sanitized and may contain sensitive information. Users who depend
4+
on these resource attributes must pass `include_command_args=True`.

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

opentelemetry-sdk/src/opentelemetry/sdk/resources/__init__.py

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,24 @@ def detect(self) -> "Resource":
322322

323323

324324
class ProcessResourceDetector(ResourceDetector):
325-
# pylint: disable=no-self-use
325+
"""Detect process resource attributes.
326+
327+
Args:
328+
raise_on_error: Raise errors from detection instead of logging them.
329+
include_command_args: Include ``process.command_args`` and
330+
``process.command_line``. These attributes can contain sensitive
331+
command-line values, so they are excluded by default.
332+
"""
333+
334+
def __init__(
335+
self,
336+
raise_on_error: bool = False,
337+
*,
338+
include_command_args: bool = False,
339+
) -> None:
340+
super().__init__(raise_on_error=raise_on_error)
341+
self._include_command_args = include_command_args
342+
326343
def detect(self) -> "Resource":
327344
_runtime_version = ".".join(
328345
map(
@@ -341,24 +358,23 @@ def detect(self) -> "Resource":
341358
# Use sys.orig_argv, which preserves the original arguments received
342359
# by the interpreter. This correctly captures ``python -m <module>``
343360
# invocations where sys.argv is rewritten to the resolved module path
344-
# and the ``-m <module>`` information is lost. sys.orig_argv also
345-
# aligns with /proc/<pid>/cmdline, which the OTel semantic
346-
# conventions reference for these attributes.
347-
_process_argv = list(sys.orig_argv)
348-
_process_command = _process_argv[0] if _process_argv else ""
349-
_process_command_line = " ".join(_process_argv)
350-
_process_command_args = _process_argv
351-
resource_info = {
361+
# and the ``-m <module>`` information is lost. Only read argv[0] by
362+
# default because full command arguments are opt-in.
363+
_process_command = sys.orig_argv[0] if sys.orig_argv else ""
364+
resource_info: dict[str, AttributeValue] = {
352365
PROCESS_RUNTIME_DESCRIPTION: sys.version,
353366
PROCESS_RUNTIME_NAME: sys.implementation.name,
354367
PROCESS_RUNTIME_VERSION: _runtime_version,
355368
PROCESS_PID: _process_pid,
356369
PROCESS_EXECUTABLE_NAME: _process_executable_name,
357370
PROCESS_EXECUTABLE_PATH: _process_executable_path,
358371
PROCESS_COMMAND: _process_command,
359-
PROCESS_COMMAND_LINE: _process_command_line,
360-
PROCESS_COMMAND_ARGS: _process_command_args,
361372
}
373+
if self._include_command_args:
374+
_process_argv = list(sys.orig_argv)
375+
resource_info[PROCESS_COMMAND_LINE] = " ".join(_process_argv)
376+
resource_info[PROCESS_COMMAND_ARGS] = _process_argv
377+
362378
if hasattr(os, "getppid"):
363379
# pypy3 does not have getppid()
364380
resource_info[PROCESS_PARENT_PID] = os.getppid()
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)