summaryrefslogtreecommitdiff
path: root/opentelemetry-sdk/tests
diff options
Diffstat (limited to 'opentelemetry-sdk/tests')
-rw-r--r--opentelemetry-sdk/tests/_configuration/file/test_env_substitution.py2
-rw-r--r--opentelemetry-sdk/tests/_configuration/file/test_loader.py136
-rw-r--r--opentelemetry-sdk/tests/_configuration/test_common.py82
-rw-r--r--opentelemetry-sdk/tests/_configuration/test_configurator_file_routing.py89
-rw-r--r--opentelemetry-sdk/tests/_configuration/test_conversion.py110
-rw-r--r--opentelemetry-sdk/tests/_configuration/test_meter_provider.py241
-rw-r--r--opentelemetry-sdk/tests/_configuration/test_resource.py4
-rw-r--r--opentelemetry-sdk/tests/_configuration/test_sdk.py149
-rw-r--r--opentelemetry-sdk/tests/_configuration/test_tracer_provider.py298
-rw-r--r--opentelemetry-sdk/tests/metrics/test_measurement_consumer.py148
-rw-r--r--opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py17
-rw-r--r--opentelemetry-sdk/tests/metrics/test_metrics.py56
-rw-r--r--opentelemetry-sdk/tests/metrics/test_view_instrument_match.py1
-rw-r--r--opentelemetry-sdk/tests/resources/test_resources.py218
-rw-r--r--opentelemetry-sdk/tests/test_configurator.py121
-rw-r--r--opentelemetry-sdk/tests/trace/composite_sampler/test_rule_based.py226
16 files changed, 1796 insertions, 102 deletions
diff --git a/opentelemetry-sdk/tests/_configuration/file/test_env_substitution.py b/opentelemetry-sdk/tests/_configuration/file/test_env_substitution.py
index fee46c9..b9f71f3 100644
--- a/opentelemetry-sdk/tests/_configuration/file/test_env_substitution.py
+++ b/opentelemetry-sdk/tests/_configuration/file/test_env_substitution.py
@@ -119,7 +119,7 @@ line2: value2"""
{"SERVICE_NAME": "legit-service\nmalicious_key: injected_value"},
):
result = substitute_env_vars(
- "file_format: '0.1'\nservice_name: ${SERVICE_NAME}"
+ "file_format: '1.0'\nservice_name: ${SERVICE_NAME}"
)
parsed = yaml.safe_load(result)
self.assertNotIn("malicious_key", parsed)
diff --git a/opentelemetry-sdk/tests/_configuration/file/test_loader.py b/opentelemetry-sdk/tests/_configuration/file/test_loader.py
index b38f301..ecb485c 100644
--- a/opentelemetry-sdk/tests/_configuration/file/test_loader.py
+++ b/opentelemetry-sdk/tests/_configuration/file/test_loader.py
@@ -7,11 +7,32 @@ import unittest
from pathlib import Path
from unittest.mock import patch
+from opentelemetry.sdk._configuration._tracer_provider import (
+ create_tracer_provider,
+)
from opentelemetry.sdk._configuration.file import (
ConfigurationError,
load_config_file,
)
+from opentelemetry.sdk._configuration.models import (
+ BatchSpanProcessor as BatchSpanProcessorConfig,
+)
from opentelemetry.sdk._configuration.models import OpenTelemetryConfiguration
+from opentelemetry.sdk._configuration.models import (
+ ParentBasedSampler as ParentBasedSamplerConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ SpanProcessor as SpanProcessorConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ TracerProvider as TracerProviderConfig,
+)
+from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider
+from opentelemetry.sdk.trace.export import (
+ BatchSpanProcessor,
+ ConsoleSpanExporter,
+)
+from opentelemetry.sdk.trace.sampling import ParentBased, TraceIdRatioBased
class TestConfigLoader(unittest.TestCase):
@@ -231,3 +252,118 @@ class TestConfigLoader(unittest.TestCase):
self.assertIn("schema", str(ctx.exception).lower())
finally:
os.unlink(temp_path)
+
+
+class TestConfigLoaderEndToEnd(unittest.TestCase):
+ """Smoke-test the full YAML -> typed config -> SDK object pipeline.
+
+ Unit tests in test_conversion.py exercise the dict-to-dataclass
+ conversion in isolation; these tests verify it composes with the
+ real loader and downstream factory functions on a representative
+ nested configuration.
+ """
+
+ _YAML = """
+file_format: '1.0'
+tracer_provider:
+ processors:
+ - batch:
+ exporter:
+ console: {}
+ sampler:
+ parent_based:
+ root:
+ trace_id_ratio_based: {ratio: 0.5}
+"""
+
+ def _load(self) -> OpenTelemetryConfiguration:
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".yaml", delete=False
+ ) as fh:
+ fh.write(self._YAML)
+ path = fh.name
+ try:
+ return load_config_file(path)
+ finally:
+ os.unlink(path)
+
+ def test_nested_fields_are_typed_dataclasses(self):
+ config = self._load()
+
+ self.assertIsInstance(config.tracer_provider, TracerProviderConfig)
+ self.assertIsInstance(
+ config.tracer_provider.sampler.parent_based,
+ ParentBasedSamplerConfig,
+ )
+ # Lists of dataclasses are converted element-wise.
+ self.assertIsInstance(
+ config.tracer_provider.processors[0], SpanProcessorConfig
+ )
+ self.assertIsInstance(
+ config.tracer_provider.processors[0].batch,
+ BatchSpanProcessorConfig,
+ )
+
+ # pylint: disable=protected-access
+ def test_typed_config_feeds_factory_function(self):
+ config = self._load()
+
+ provider = create_tracer_provider(config.tracer_provider)
+
+ self.assertIsInstance(provider, SdkTracerProvider)
+ # Sampler wiring from the YAML: parent_based(trace_id_ratio_based(0.5)).
+ self.assertIsInstance(provider.sampler, ParentBased)
+ self.assertIsInstance(provider.sampler._root, TraceIdRatioBased)
+ self.assertEqual(provider.sampler._root.rate, 0.5)
+ # Span processor wiring from the YAML: batch(console).
+ processors = provider._active_span_processor._span_processors
+ self.assertEqual(len(processors), 1)
+ self.assertIsInstance(processors[0], BatchSpanProcessor)
+ self.assertIsInstance(processors[0].span_exporter, ConsoleSpanExporter)
+
+
+class TestFileFormatValidation(unittest.TestCase):
+ """Validate the file_format version per the configuration spec."""
+
+ @staticmethod
+ def _load(file_format: str) -> OpenTelemetryConfiguration:
+ with tempfile.NamedTemporaryFile(
+ mode="w", suffix=".yaml", delete=False
+ ) as fh:
+ fh.write(f'file_format: "{file_format}"')
+ path = fh.name
+ try:
+ return load_config_file(path)
+ finally:
+ os.unlink(path)
+
+ def test_supported_version_is_accepted(self):
+ config = self._load("1.0")
+ self.assertEqual(config.file_format, "1.0")
+
+ def test_pre_release_meta_tag_is_accepted(self):
+ # The meta tag is stripped; "1.0-rc.2" is treated as 1.0.
+ config = self._load("1.0-rc.2")
+ self.assertEqual(config.file_format, "1.0-rc.2")
+
+ def test_newer_minor_is_accepted_with_warning(self):
+ with self.assertLogs(
+ "opentelemetry.sdk._configuration.file._loader", level="WARNING"
+ ) as logs:
+ config = self._load("1.1")
+ self.assertEqual(config.file_format, "1.1")
+ self.assertTrue(
+ any("newer minor version" in message for message in logs.output)
+ )
+
+ def test_unsupported_major_is_rejected(self):
+ for version in ("2.0", "0.4"):
+ with self.subTest(version=version):
+ with self.assertRaises(ConfigurationError) as ctx:
+ self._load(version)
+ self.assertIn("file_format", str(ctx.exception))
+
+ def test_malformed_version_is_rejected(self):
+ with self.assertRaises(ConfigurationError) as ctx:
+ self._load("not-a-version")
+ self.assertIn("file_format", str(ctx.exception))
diff --git a/opentelemetry-sdk/tests/_configuration/test_common.py b/opentelemetry-sdk/tests/_configuration/test_common.py
index d8236a8..46469f1 100644
--- a/opentelemetry-sdk/tests/_configuration/test_common.py
+++ b/opentelemetry-sdk/tests/_configuration/test_common.py
@@ -12,6 +12,7 @@ from opentelemetry.sdk._configuration._common import (
_additional_properties,
_map_compression,
_parse_headers,
+ _resolve_component,
load_entry_point,
)
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
@@ -289,3 +290,84 @@ class TestGeneratedModelsHaveAdditionalProperties(unittest.TestCase):
def test_push_metric_exporter(self):
self._assert_supports_additional_properties(PushMetricExporter)
+
+
+class TestResolveComponent(unittest.TestCase):
+ def setUp(self):
+ @_additional_properties
+ @dataclass
+ class _Config:
+ builtin_a: dict | None = None
+ builtin_b: str | None = None
+ additional_properties: ClassVar[dict[str, Any]]
+
+ self.cls = _Config
+ self.registry = {
+ "builtin_a": lambda v: ("resolved_a", v),
+ "builtin_b": lambda v: ("resolved_b", v),
+ }
+
+ def test_resolves_builtin_from_registry(self):
+ config = self.cls(builtin_a={"key": "val"})
+ result = _resolve_component(
+ config, self.registry, "test_group", "test component"
+ )
+ self.assertEqual(result, ("resolved_a", {"key": "val"}))
+
+ def test_resolves_plugin_via_entry_point(self):
+ mock_instance = MagicMock()
+ mock_class = MagicMock(return_value=mock_instance)
+ with patch(
+ "opentelemetry.sdk._configuration._common.entry_points",
+ return_value=[MagicMock(**{"load.return_value": mock_class})],
+ ):
+ # pylint: disable=unexpected-keyword-arg
+ config = self.cls(my_plugin={"opt": "val"})
+ result = _resolve_component(
+ config, self.registry, "test_group", "test component"
+ )
+ self.assertIs(result, mock_instance)
+ mock_class.assert_called_once_with(opt="val")
+
+ def test_plugin_with_empty_config(self):
+ mock_instance = MagicMock()
+ mock_class = MagicMock(return_value=mock_instance)
+ with patch(
+ "opentelemetry.sdk._configuration._common.entry_points",
+ return_value=[MagicMock(**{"load.return_value": mock_class})],
+ ):
+ # pylint: disable=unexpected-keyword-arg
+ config = self.cls(my_plugin={})
+ _resolve_component(
+ config, self.registry, "test_group", "test component"
+ )
+ mock_class.assert_called_once_with()
+
+ def test_no_component_raises_configuration_error(self):
+ config = self.cls()
+ with self.assertRaises(ConfigurationError):
+ _resolve_component(
+ config, self.registry, "test_group", "test component"
+ )
+
+ def test_plugin_not_found_raises_configuration_error(self):
+ with patch(
+ "opentelemetry.sdk._configuration._common.entry_points",
+ return_value=[],
+ ):
+ # pylint: disable=unexpected-keyword-arg
+ config = self.cls(missing_plugin={})
+ with self.assertRaises(ConfigurationError):
+ _resolve_component(
+ config, self.registry, "test_group", "test component"
+ )
+
+ def test_first_registry_match_wins_when_multiple_set(self):
+ """When multiple built-in fields are set (which the schema should
+ prevent), the first registry match wins."""
+ config = self.cls(builtin_a={"a": 1}, builtin_b="b")
+ result = _resolve_component(
+ config, self.registry, "test_group", "test component"
+ )
+ # builtin_a comes first in the registry dict
+ self.assertEqual(result, ("resolved_a", {"a": 1}))
diff --git a/opentelemetry-sdk/tests/_configuration/test_configurator_file_routing.py b/opentelemetry-sdk/tests/_configuration/test_configurator_file_routing.py
new file mode 100644
index 0000000..89b2098
--- /dev/null
+++ b/opentelemetry-sdk/tests/_configuration/test_configurator_file_routing.py
@@ -0,0 +1,89 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+# Tests access private members of SDK classes to assert correct configuration.
+# pylint: disable=protected-access,no-self-use
+
+import unittest
+from unittest.mock import patch
+
+from opentelemetry.sdk._configuration import _OTelSDKConfigurator
+from opentelemetry.sdk._configuration._exceptions import ConfigurationError
+from opentelemetry.sdk.environment_variables import OTEL_CONFIG_FILE
+
+
+class TestConfiguratorFileRouting(unittest.TestCase):
+ def tearDown(self):
+ # _BaseConfigurator caches instances via a singleton; reset so sibling
+ # tests (e.g. test_configurator.py's CustomConfigurator subclass) are
+ # not affected by this class's singleton state.
+ _OTelSDKConfigurator._instance = None
+
+ @patch.dict("os.environ", {}, clear=True)
+ @patch("opentelemetry.sdk._configuration._initialize_components")
+ def test_env_var_unset_runs_env_var_path(self, mock_init_components):
+ _OTelSDKConfigurator()._configure(auto_instrumentation_version="X")
+ mock_init_components.assert_called_once_with(
+ auto_instrumentation_version="X"
+ )
+
+ @patch.dict("os.environ", {OTEL_CONFIG_FILE: "/tmp/otel.yaml"})
+ @patch("opentelemetry.sdk._configuration._sdk.configure_sdk")
+ @patch("opentelemetry.sdk._configuration.file._loader.load_config_file")
+ @patch("opentelemetry.sdk._configuration._initialize_components")
+ def test_env_var_set_routes_to_declarative_path(
+ self, mock_init_components, mock_load, mock_configure_sdk
+ ):
+ sentinel_config = object()
+ mock_load.return_value = sentinel_config
+
+ _OTelSDKConfigurator()._configure()
+
+ mock_load.assert_called_once_with("/tmp/otel.yaml")
+ mock_configure_sdk.assert_called_once_with(sentinel_config)
+ mock_init_components.assert_not_called()
+
+ @patch.dict("os.environ", {OTEL_CONFIG_FILE: "/does/not/exist.yaml"})
+ @patch("opentelemetry.sdk._configuration._initialize_components")
+ def test_env_var_set_missing_file_propagates(self, mock_init_components):
+ with self.assertRaises(ConfigurationError):
+ _OTelSDKConfigurator()._configure()
+ mock_init_components.assert_not_called()
+
+ @patch.dict("os.environ", {OTEL_CONFIG_FILE: "/tmp/otel.yaml"})
+ @patch("opentelemetry.sdk._configuration._sdk.configure_sdk")
+ @patch("opentelemetry.sdk._configuration.file._loader.load_config_file")
+ def test_env_var_set_with_kwargs_warns_and_ignores(
+ self, mock_load, mock_configure_sdk
+ ):
+ mock_load.return_value = object()
+
+ with self.assertLogs(
+ "opentelemetry.sdk._configuration", level="WARNING"
+ ) as captured:
+ _OTelSDKConfigurator()._configure(
+ sampler="X", auto_instrumentation_version="Y"
+ )
+
+ self.assertTrue(
+ any(
+ "OTEL_CONFIG_FILE" in msg and "sampler" in msg
+ for msg in captured.output
+ ),
+ f"Expected warning about ignored kwargs, got: {captured.output}",
+ )
+ mock_configure_sdk.assert_called_once()
+
+ @patch.dict("os.environ", {}, clear=True)
+ @patch("opentelemetry.sdk._configuration._initialize_components")
+ def test_distro_override_pattern_still_works(self, mock_init_components):
+ class CustomConfigurator(_OTelSDKConfigurator):
+ def _configure(self, **kwargs):
+ kwargs["sampler"] = "TEST_SAMPLER"
+ super()._configure(**kwargs)
+
+ CustomConfigurator()._configure(auto_instrumentation_version="V")
+
+ mock_init_components.assert_called_once_with(
+ auto_instrumentation_version="V", sampler="TEST_SAMPLER"
+ )
diff --git a/opentelemetry-sdk/tests/_configuration/test_conversion.py b/opentelemetry-sdk/tests/_configuration/test_conversion.py
new file mode 100644
index 0000000..c6cc082
--- /dev/null
+++ b/opentelemetry-sdk/tests/_configuration/test_conversion.py
@@ -0,0 +1,110 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+# Tests access private members of SDK classes to assert correct configuration.
+# pylint: disable=protected-access
+
+import unittest
+from dataclasses import dataclass
+from typing import Any, ClassVar
+
+from opentelemetry.sdk._configuration._common import _additional_properties
+from opentelemetry.sdk._configuration._conversion import _dict_to_dataclass
+from opentelemetry.sdk._configuration.models import ExemplarFilter
+
+
+@dataclass
+class _Inner:
+ value: int | None = None
+
+
+@dataclass
+class _Middle:
+ inner: _Inner | None = None
+ items: list[_Inner] | None = None
+
+
+@dataclass
+class _Outer:
+ middle: _Middle | None = None
+ name: str | None = None
+
+
+@_additional_properties
+@dataclass
+class _WithExtras:
+ known: str | None = None
+ additional_properties: ClassVar[dict[str, Any]]
+
+
+@dataclass
+class _WithEnum:
+ filter: ExemplarFilter | None = None
+
+
+class TestDictToDataclass(unittest.TestCase):
+ def test_raises_on_non_dataclass(self):
+ # _dict_to_dataclass is internal and assumes cls is a dataclass.
+ with self.assertRaises(TypeError) as ctx:
+ _dict_to_dataclass({"x": 1}, dict)
+ self.assertIn("not a dataclass", str(ctx.exception))
+
+ def test_converts_flat_dict(self):
+ result = _dict_to_dataclass({"value": 42}, _Inner)
+ self.assertIsInstance(result, _Inner)
+ self.assertEqual(result.value, 42)
+
+ def test_converts_nested_dataclass(self):
+ result = _dict_to_dataclass(
+ {"middle": {"inner": {"value": 7}}}, _Outer
+ )
+ self.assertIsInstance(result, _Outer)
+ self.assertIsInstance(result.middle, _Middle)
+ self.assertIsInstance(result.middle.inner, _Inner)
+ self.assertEqual(result.middle.inner.value, 7)
+
+ def test_converts_list_of_dataclasses(self):
+ result = _dict_to_dataclass(
+ {"middle": {"items": [{"value": 1}, {"value": 2}]}}, _Outer
+ )
+ self.assertEqual(len(result.middle.items), 2)
+ self.assertIsInstance(result.middle.items[0], _Inner)
+ self.assertEqual(result.middle.items[0].value, 1)
+ self.assertEqual(result.middle.items[1].value, 2)
+
+ def test_none_value_preserved(self):
+ result = _dict_to_dataclass({"middle": None, "name": "test"}, _Outer)
+ self.assertIsNone(result.middle)
+ self.assertEqual(result.name, "test")
+
+ def test_missing_optional_fields_default_to_none(self):
+ result = _dict_to_dataclass({}, _Outer)
+ self.assertIsNone(result.middle)
+ self.assertIsNone(result.name)
+
+ def test_unknown_keys_routed_to_additional_properties(self):
+ result = _dict_to_dataclass(
+ {"known": "yes", "my_plugin": {"opt": True}}, _WithExtras
+ )
+ self.assertEqual(result.known, "yes")
+ self.assertEqual(
+ result.additional_properties, {"my_plugin": {"opt": True}}
+ )
+
+ def test_primitive_values_pass_through(self):
+ result = _dict_to_dataclass({"name": "hello"}, _Outer)
+ self.assertEqual(result.name, "hello")
+
+ def test_empty_list_converted(self):
+ result = _dict_to_dataclass({"middle": {"items": []}}, _Outer)
+ self.assertEqual(result.middle.items, [])
+
+ def test_enum_value_coerced_from_string(self):
+ result = _dict_to_dataclass({"filter": "always_on"}, _WithEnum)
+ self.assertIs(result.filter, ExemplarFilter.always_on)
+
+ def test_enum_value_already_enum_passes_through(self):
+ result = _dict_to_dataclass(
+ {"filter": ExemplarFilter.trace_based}, _WithEnum
+ )
+ self.assertIs(result.filter, ExemplarFilter.trace_based)
diff --git a/opentelemetry-sdk/tests/_configuration/test_meter_provider.py b/opentelemetry-sdk/tests/_configuration/test_meter_provider.py
index f3a48ac..566329c 100644
--- a/opentelemetry-sdk/tests/_configuration/test_meter_provider.py
+++ b/opentelemetry-sdk/tests/_configuration/test_meter_provider.py
@@ -27,6 +27,9 @@ from opentelemetry.sdk._configuration.models import (
ExemplarFilter as ExemplarFilterConfig,
)
from opentelemetry.sdk._configuration.models import (
+ ExperimentalPrometheusMetricExporter as PrometheusMetricExporterConfig,
+)
+from opentelemetry.sdk._configuration.models import (
ExplicitBucketHistogramAggregation as ExplicitBucketConfig,
)
from opentelemetry.sdk._configuration.models import (
@@ -53,6 +56,12 @@ from opentelemetry.sdk._configuration.models import (
PeriodicMetricReader as PeriodicMetricReaderConfig,
)
from opentelemetry.sdk._configuration.models import (
+ PullMetricExporter as PullMetricExporterConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ PullMetricReader as PullMetricReaderConfig,
+)
+from opentelemetry.sdk._configuration.models import (
PushMetricExporter as PushMetricExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
@@ -99,7 +108,7 @@ class TestCreateMeterProviderBasic(unittest.TestCase):
def test_none_config_no_readers(self):
provider = create_meter_provider(None)
- self.assertEqual(len(provider._sdk_config.metric_readers), 0)
+ self.assertEqual(len(provider._metric_readers), 0)
def test_none_config_uses_trace_based_exemplar_filter(self):
provider = create_meter_provider(None)
@@ -130,7 +139,7 @@ class TestCreateMeterProviderBasic(unittest.TestCase):
)
with patch.dict(os.environ, {"OTEL_METRIC_EXPORT_INTERVAL": "999999"}):
provider = create_meter_provider(config)
- reader = provider._sdk_config.metric_readers[0]
+ reader = provider._metric_readers[0]
self.assertIsInstance(reader, PeriodicExportingMetricReader)
self.assertEqual(reader._export_interval_millis, 60000.0)
@@ -154,11 +163,6 @@ class TestCreateMeterProviderBasic(unittest.TestCase):
arg = mock_set.call_args[0][0]
self.assertIsInstance(arg, MeterProvider)
- def test_empty_readers_list(self):
- config = MeterProviderConfig(readers=[])
- provider = create_meter_provider(config)
- self.assertEqual(len(provider._sdk_config.metric_readers), 0)
-
class TestCreateMetricReaders(unittest.TestCase):
@staticmethod
@@ -180,7 +184,7 @@ class TestCreateMetricReaders(unittest.TestCase):
PushMetricExporterConfig(console=ConsoleMetricExporterConfig())
)
provider = create_meter_provider(config)
- reader = provider._sdk_config.metric_readers[0]
+ reader = provider._metric_readers[0]
self.assertIsInstance(reader, PeriodicExportingMetricReader)
self.assertIsInstance(reader._exporter, ConsoleMetricExporter)
@@ -189,7 +193,7 @@ class TestCreateMetricReaders(unittest.TestCase):
PushMetricExporterConfig(console=ConsoleMetricExporterConfig())
)
provider = create_meter_provider(config)
- reader = provider._sdk_config.metric_readers[0]
+ reader = provider._metric_readers[0]
self.assertEqual(reader._export_interval_millis, 60000.0)
def test_periodic_reader_default_timeout(self):
@@ -197,7 +201,7 @@ class TestCreateMetricReaders(unittest.TestCase):
PushMetricExporterConfig(console=ConsoleMetricExporterConfig())
)
provider = create_meter_provider(config)
- reader = provider._sdk_config.metric_readers[0]
+ reader = provider._metric_readers[0]
self.assertEqual(reader._export_timeout_millis, 30000.0)
def test_periodic_reader_explicit_interval(self):
@@ -206,7 +210,7 @@ class TestCreateMetricReaders(unittest.TestCase):
interval=5000,
)
provider = create_meter_provider(config)
- reader = provider._sdk_config.metric_readers[0]
+ reader = provider._metric_readers[0]
self.assertEqual(reader._export_interval_millis, 5000.0)
def test_periodic_reader_explicit_timeout(self):
@@ -215,7 +219,7 @@ class TestCreateMetricReaders(unittest.TestCase):
timeout=10000,
)
provider = create_meter_provider(config)
- reader = provider._sdk_config.metric_readers[0]
+ reader = provider._metric_readers[0]
self.assertEqual(reader._export_timeout_millis, 10000.0)
def test_otlp_http_missing_package_raises(self):
@@ -306,13 +310,216 @@ class TestCreateMetricReaders(unittest.TestCase):
create_meter_provider(config)
self.assertIn("otlp-proto-grpc", str(ctx.exception))
- def test_pull_reader_raises(self):
+
+class TestCreatePullMetricReaders(unittest.TestCase):
+ def test_pull_prometheus_creates_reader(self):
+ mock_reader_cls = MagicMock()
+ mock_start_server = MagicMock()
+ mock_module = MagicMock()
+ mock_module.PrometheusMetricReader = mock_reader_cls
+ mock_module.start_http_server = mock_start_server
+
+ with patch.dict(
+ sys.modules,
+ {"opentelemetry.exporter.prometheus": mock_module},
+ ):
+ config = MeterProviderConfig(
+ readers=[
+ MetricReaderConfig(
+ pull=PullMetricReaderConfig(
+ exporter=PullMetricExporterConfig(
+ prometheus_development=PrometheusMetricExporterConfig(
+ host="0.0.0.0",
+ port=9090,
+ without_target_info_development=True,
+ )
+ )
+ )
+ )
+ ]
+ )
+ provider = create_meter_provider(config)
+
+ mock_reader_cls.assert_called_once_with(disable_target_info=True)
+ mock_start_server.assert_called_once_with(port=9090, addr="0.0.0.0")
+ self.assertEqual(len(provider._metric_readers), 1)
+
+ def test_pull_prometheus_defaults(self):
+ mock_reader_cls = MagicMock()
+ mock_start_server = MagicMock()
+ mock_module = MagicMock()
+ mock_module.PrometheusMetricReader = mock_reader_cls
+ mock_module.start_http_server = mock_start_server
+
+ with patch.dict(
+ sys.modules,
+ {"opentelemetry.exporter.prometheus": mock_module},
+ ):
+ config = MeterProviderConfig(
+ readers=[
+ MetricReaderConfig(
+ pull=PullMetricReaderConfig(
+ exporter=PullMetricExporterConfig(
+ prometheus_development=PrometheusMetricExporterConfig()
+ )
+ )
+ )
+ ]
+ )
+ provider = create_meter_provider(config)
+
+ mock_reader_cls.assert_called_once_with(disable_target_info=False)
+ mock_start_server.assert_called_once_with(port=9464, addr="localhost")
+ self.assertEqual(len(provider._metric_readers), 1)
+
+ def test_pull_prometheus_missing_package_raises(self):
+ with patch.dict(
+ sys.modules,
+ {"opentelemetry.exporter.prometheus": None},
+ ):
+ config = MeterProviderConfig(
+ readers=[
+ MetricReaderConfig(
+ pull=PullMetricReaderConfig(
+ exporter=PullMetricExporterConfig(
+ prometheus_development=PrometheusMetricExporterConfig()
+ )
+ )
+ )
+ ]
+ )
+ with self.assertRaises(ConfigurationError):
+ create_meter_provider(config)
+
+ def test_pull_no_exporter_raises(self):
config = MeterProviderConfig(
- readers=[MetricReaderConfig(pull=MagicMock())]
+ readers=[
+ MetricReaderConfig(
+ pull=PullMetricReaderConfig(
+ exporter=PullMetricExporterConfig()
+ )
+ )
+ ]
)
with self.assertRaises(ConfigurationError):
create_meter_provider(config)
+ def test_pull_plugin_loads_via_entry_point(self):
+ mock_reader = MagicMock()
+ mock_class = MagicMock(return_value=mock_reader)
+ mock_entry_points = MagicMock(
+ return_value=[MagicMock(**{"load.return_value": mock_class})]
+ )
+ with patch(
+ "opentelemetry.sdk._configuration._common.entry_points",
+ mock_entry_points,
+ ):
+ config = MeterProviderConfig(
+ readers=[
+ MetricReaderConfig(
+ pull=PullMetricReaderConfig(
+ # pylint: disable=unexpected-keyword-arg
+ exporter=PullMetricExporterConfig(
+ my_custom_reader={"port": 8080}
+ )
+ )
+ )
+ ]
+ )
+ provider = create_meter_provider(config)
+ self.assertEqual(len(provider._metric_readers), 1)
+ mock_class.assert_called_once_with(port=8080)
+ mock_entry_points.assert_called_once_with(
+ group="opentelemetry_pull_metric_exporter",
+ name="my_custom_reader",
+ )
+
+ def test_pull_plugin_not_found_raises(self):
+ with patch(
+ "opentelemetry.sdk._configuration._common.entry_points",
+ return_value=[],
+ ):
+ config = MeterProviderConfig(
+ readers=[
+ MetricReaderConfig(
+ pull=PullMetricReaderConfig(
+ # pylint: disable=unexpected-keyword-arg
+ exporter=PullMetricExporterConfig(
+ no_such_reader={}
+ )
+ )
+ )
+ ]
+ )
+ with self.assertRaises(ConfigurationError):
+ create_meter_provider(config)
+
+ def test_pull_producers_warns(self):
+ mock_module = MagicMock()
+
+ with patch.dict(
+ sys.modules,
+ {"opentelemetry.exporter.prometheus": mock_module},
+ ):
+ config = MeterProviderConfig(
+ readers=[
+ MetricReaderConfig(
+ pull=PullMetricReaderConfig(
+ exporter=PullMetricExporterConfig(
+ prometheus_development=PrometheusMetricExporterConfig()
+ ),
+ producers=[MagicMock()],
+ )
+ )
+ ]
+ )
+ with self.assertLogs(
+ "opentelemetry.sdk._configuration._meter_provider",
+ level="WARNING",
+ ) as cm:
+ create_meter_provider(config)
+ self.assertTrue(any("MetricProducer" in msg for msg in cm.output))
+
+ def test_pull_cardinality_limits_warns(self):
+ mock_module = MagicMock()
+
+ with patch.dict(
+ sys.modules,
+ {"opentelemetry.exporter.prometheus": mock_module},
+ ):
+ config = MeterProviderConfig(
+ readers=[
+ MetricReaderConfig(
+ pull=PullMetricReaderConfig(
+ exporter=PullMetricExporterConfig(
+ prometheus_development=PrometheusMetricExporterConfig()
+ ),
+ cardinality_limits=MagicMock(),
+ )
+ )
+ ]
+ )
+ with self.assertLogs(
+ "opentelemetry.sdk._configuration._meter_provider",
+ level="WARNING",
+ ) as cm:
+ create_meter_provider(config)
+ self.assertTrue(any("cardinality_limits" in msg for msg in cm.output))
+
+
+class TestCreateMetricReadersGeneral(unittest.TestCase):
+ @staticmethod
+ def _make_periodic_config(exporter_config):
+ return MeterProviderConfig(
+ readers=[
+ MetricReaderConfig(
+ periodic=PeriodicMetricReaderConfig(
+ exporter=exporter_config
+ )
+ )
+ ]
+ )
+
def test_no_reader_type_raises(self):
config = MeterProviderConfig(readers=[MetricReaderConfig()])
with self.assertRaises(ConfigurationError):
@@ -335,7 +542,7 @@ class TestCreateMetricReaders(unittest.TestCase):
PushMetricExporterConfig(my_custom_exporter={})
)
provider = create_meter_provider(config)
- self.assertEqual(len(provider._sdk_config.metric_readers), 1)
+ self.assertEqual(len(provider._metric_readers), 1)
def test_unknown_metric_exporter_raises_configuration_error(self):
with patch(
@@ -369,7 +576,7 @@ class TestCreateMetricReaders(unittest.TestCase):
]
)
provider = create_meter_provider(config)
- self.assertEqual(len(provider._sdk_config.metric_readers), 2)
+ self.assertEqual(len(provider._metric_readers), 2)
class TestTemporalityAndAggregation(unittest.TestCase):
@@ -393,7 +600,7 @@ class TestTemporalityAndAggregation(unittest.TestCase):
@staticmethod
def _get_exporter(config):
provider = create_meter_provider(config)
- return provider._sdk_config.metric_readers[0]._exporter
+ return provider._metric_readers[0]._exporter
def test_default_temporality_is_cumulative(self):
exporter = self._get_exporter(self._make_console_config())
diff --git a/opentelemetry-sdk/tests/_configuration/test_resource.py b/opentelemetry-sdk/tests/_configuration/test_resource.py
index 539a6e2..ac323bc 100644
--- a/opentelemetry-sdk/tests/_configuration/test_resource.py
+++ b/opentelemetry-sdk/tests/_configuration/test_resource.py
@@ -311,10 +311,10 @@ class TestServiceResourceDetector(unittest.TestCase):
resource = create_resource(self._config_with_service())
self.assertIn(SERVICE_INSTANCE_ID, resource.attributes)
- def test_service_instance_id_is_unique_per_call(self):
+ def test_service_instance_id_is_stable_within_process(self):
r1 = create_resource(self._config_with_service())
r2 = create_resource(self._config_with_service())
- self.assertNotEqual(
+ self.assertEqual(
r1.attributes[SERVICE_INSTANCE_ID],
r2.attributes[SERVICE_INSTANCE_ID],
)
diff --git a/opentelemetry-sdk/tests/_configuration/test_sdk.py b/opentelemetry-sdk/tests/_configuration/test_sdk.py
new file mode 100644
index 0000000..b223ad3
--- /dev/null
+++ b/opentelemetry-sdk/tests/_configuration/test_sdk.py
@@ -0,0 +1,149 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+# Tests access private members of SDK classes to assert correct configuration.
+# pylint: disable=protected-access
+
+import unittest
+from unittest.mock import patch
+
+from opentelemetry.sdk._configuration._sdk import configure_sdk
+from opentelemetry.sdk._configuration.models import (
+ OpenTelemetryConfiguration,
+)
+from opentelemetry.sdk._configuration.models import (
+ Propagator as PropagatorConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ Resource as ResourceConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ SimpleSpanProcessor as SimpleSpanProcessorConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ SpanExporter as SpanExporterConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ SpanProcessor as SpanProcessorConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ TracerProvider as TracerProviderConfig,
+)
+from opentelemetry.sdk.trace import TracerProvider as SdkTracerProvider
+
+_MIN_CONFIG_KWARGS = {"file_format": "1.0"}
+
+
+def _config(**kwargs) -> OpenTelemetryConfiguration:
+ return OpenTelemetryConfiguration(**{**_MIN_CONFIG_KWARGS, **kwargs})
+
+
+class TestConfigureSdk(unittest.TestCase):
+ @patch("opentelemetry.sdk._configuration._sdk.configure_propagator")
+ @patch("opentelemetry.sdk._configuration._sdk.configure_logger_provider")
+ @patch("opentelemetry.sdk._configuration._sdk.configure_meter_provider")
+ @patch("opentelemetry.sdk._configuration._sdk.configure_tracer_provider")
+ @patch("opentelemetry.sdk._configuration._sdk.create_resource")
+ # pylint: disable=no-self-use
+ def test_calls_each_signal_with_resource(
+ self,
+ mock_create_resource,
+ mock_tracer,
+ mock_meter,
+ mock_logger,
+ mock_propagator,
+ ):
+ sentinel_resource = object()
+ mock_create_resource.return_value = sentinel_resource
+
+ resource_cfg = ResourceConfig()
+ tracer_cfg = TracerProviderConfig(processors=[])
+ propagator_cfg = PropagatorConfig()
+ config = _config(
+ resource=resource_cfg,
+ tracer_provider=tracer_cfg,
+ propagator=propagator_cfg,
+ )
+
+ configure_sdk(config)
+
+ mock_create_resource.assert_called_once_with(resource_cfg)
+ mock_tracer.assert_called_once_with(tracer_cfg, sentinel_resource)
+ mock_meter.assert_called_once_with(None, sentinel_resource)
+ mock_logger.assert_called_once_with(None, sentinel_resource)
+ mock_propagator.assert_called_once_with(propagator_cfg)
+
+ @patch("opentelemetry.sdk._configuration._sdk.configure_propagator")
+ @patch("opentelemetry.sdk._configuration._sdk.configure_logger_provider")
+ @patch("opentelemetry.sdk._configuration._sdk.configure_meter_provider")
+ @patch("opentelemetry.sdk._configuration._sdk.configure_tracer_provider")
+ @patch("opentelemetry.sdk._configuration._sdk.create_resource")
+ # pylint: disable=no-self-use
+ def test_disabled_skips_everything(
+ self,
+ mock_create_resource,
+ mock_tracer,
+ mock_meter,
+ mock_logger,
+ mock_propagator,
+ ):
+ config = _config(
+ disabled=True,
+ tracer_provider=TracerProviderConfig(processors=[]),
+ )
+
+ configure_sdk(config)
+
+ mock_create_resource.assert_not_called()
+ mock_tracer.assert_not_called()
+ mock_meter.assert_not_called()
+ mock_logger.assert_not_called()
+ mock_propagator.assert_not_called()
+
+ @patch("opentelemetry.sdk._configuration._sdk.configure_propagator")
+ @patch("opentelemetry.sdk._configuration._sdk.configure_logger_provider")
+ @patch("opentelemetry.sdk._configuration._sdk.configure_meter_provider")
+ @patch("opentelemetry.sdk._configuration._sdk.configure_tracer_provider")
+ @patch("opentelemetry.sdk._configuration._sdk.create_resource")
+ def test_absent_sections_pass_none(
+ self,
+ mock_create_resource, # noqa: ARG002
+ mock_tracer,
+ mock_meter,
+ mock_logger,
+ mock_propagator,
+ ):
+ configure_sdk(_config())
+
+ # Each configure_* is called exactly once, with config=None.
+ self.assertEqual(mock_tracer.call_args.args[0], None)
+ self.assertEqual(mock_meter.call_args.args[0], None)
+ self.assertEqual(mock_logger.call_args.args[0], None)
+ self.assertEqual(mock_propagator.call_args.args[0], None)
+
+
+class TestConfigureSdkIntegration(unittest.TestCase):
+ """End-to-end: build a real OpenTelemetryConfiguration and apply it."""
+
+ @patch(
+ "opentelemetry.sdk._configuration._tracer_provider.trace.set_tracer_provider"
+ )
+ def test_applies_tracer_provider_globally(self, mock_set_tracer):
+ config = _config(
+ tracer_provider=TracerProviderConfig(
+ processors=[
+ SpanProcessorConfig(
+ simple=SimpleSpanProcessorConfig(
+ exporter=SpanExporterConfig(console={})
+ )
+ )
+ ]
+ )
+ )
+
+ configure_sdk(config)
+
+ mock_set_tracer.assert_called_once()
+ self.assertIsInstance(
+ mock_set_tracer.call_args[0][0], SdkTracerProvider
+ )
diff --git a/opentelemetry-sdk/tests/_configuration/test_tracer_provider.py b/opentelemetry-sdk/tests/_configuration/test_tracer_provider.py
index c3ada36..544e80f 100644
--- a/opentelemetry-sdk/tests/_configuration/test_tracer_provider.py
+++ b/opentelemetry-sdk/tests/_configuration/test_tracer_provider.py
@@ -9,6 +9,7 @@ import sys
import unittest
from unittest.mock import MagicMock, patch
+from opentelemetry import trace as trace_api
from opentelemetry.sdk._configuration._tracer_provider import (
configure_tracer_provider,
create_tracer_provider,
@@ -18,6 +19,27 @@ from opentelemetry.sdk._configuration.models import (
BatchSpanProcessor as BatchSpanProcessorConfig,
)
from opentelemetry.sdk._configuration.models import (
+ ExperimentalComposableProbabilitySampler as ComposableProbabilityConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ ExperimentalComposableRuleBasedSampler as RuleBasedSamplerConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ ExperimentalComposableRuleBasedSamplerRule as RuleBasedSamplerRuleConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ ExperimentalComposableRuleBasedSamplerRuleAttributePatterns as RuleAttributePatternsConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ ExperimentalComposableRuleBasedSamplerRuleAttributeValues as RuleAttributeValuesConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ ExperimentalComposableSampler as ComposableSamplerConfig,
+)
+from opentelemetry.sdk._configuration.models import (
+ ExperimentalSpanParent as SpanParentConfig,
+)
+from opentelemetry.sdk._configuration.models import (
OtlpGrpcExporter as OtlpGrpcExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
@@ -36,6 +58,9 @@ from opentelemetry.sdk._configuration.models import (
SpanExporter as SpanExporterConfig,
)
from opentelemetry.sdk._configuration.models import (
+ SpanKind as SpanKindConfig,
+)
+from opentelemetry.sdk._configuration.models import (
SpanLimits as SpanLimitsConfig,
)
from opentelemetry.sdk._configuration.models import (
@@ -57,10 +82,16 @@ from opentelemetry.sdk.trace.export import (
from opentelemetry.sdk.trace.sampling import (
ALWAYS_OFF,
ALWAYS_ON,
+ Decision,
ParentBased,
Sampler,
TraceIdRatioBased,
)
+from opentelemetry.trace import SpanContext, TraceFlags
+from opentelemetry.trace import SpanKind as TraceSpanKind
+
+TRACE_ID = int("00112233445566778800000000000000", 16)
+SPAN_ID = int("0123456789abcdef", 16)
class TestCreateTracerProviderBasic(unittest.TestCase):
@@ -207,11 +238,11 @@ class TestCreateSampler(unittest.TestCase):
self.assertIs(sampler._local_parent_sampled, ALWAYS_ON)
self.assertIs(sampler._local_parent_not_sampled, ALWAYS_OFF)
- def test_unknown_sampler_raises_configuration_error(self):
+ def test_no_sampler_raises_configuration_error(self):
with self.assertRaises(ConfigurationError):
self._make_provider(SamplerConfig())
- def test_plugin_sampler_loaded_via_entry_point(self):
+ def test_user_defined_sampler_loaded_via_entry_point(self):
mock_sampler = MagicMock(spec=Sampler)
mock_class = MagicMock(return_value=mock_sampler)
with patch(
@@ -222,7 +253,7 @@ class TestCreateSampler(unittest.TestCase):
provider = self._make_provider(SamplerConfig(my_custom_sampler={}))
self.assertIs(provider.sampler, mock_sampler)
- def test_unknown_plugin_raises_configuration_error(self):
+ def test_user_defined_sampler_not_found_raises_configuration_error(self):
with patch(
"opentelemetry.sdk._configuration._common.entry_points",
return_value=[],
@@ -232,6 +263,267 @@ class TestCreateSampler(unittest.TestCase):
self._make_provider(SamplerConfig(no_such_sampler={}))
+class TestCreateCompositeRuleBasedSampler(unittest.TestCase):
+ @staticmethod
+ def _make_provider(rule_based_config):
+ return create_tracer_provider(
+ TracerProviderConfig(
+ processors=[],
+ sampler=SamplerConfig(
+ composite_development=ComposableSamplerConfig(
+ rule_based=rule_based_config
+ )
+ ),
+ )
+ )
+
+ @staticmethod
+ def _rule(sampler, **kwargs):
+ return RuleBasedSamplerRuleConfig(sampler=sampler, **kwargs)
+
+ @staticmethod
+ def _always_on():
+ return ComposableSamplerConfig(always_on={})
+
+ @staticmethod
+ def _always_off():
+ return ComposableSamplerConfig(always_off={})
+
+ @staticmethod
+ def _attribute_values(key, values):
+ return RuleAttributeValuesConfig(key=key, values=values)
+
+ @staticmethod
+ def _attribute_patterns(key, included=None, excluded=None):
+ return RuleAttributePatternsConfig(
+ key=key,
+ included=included,
+ excluded=excluded,
+ )
+
+ def _decision(
+ self,
+ rule_based_config,
+ *,
+ name="span",
+ kind=None,
+ attributes=None,
+ parent_context=None,
+ ):
+ provider = self._make_provider(rule_based_config)
+ return provider.sampler.should_sample(
+ parent_context,
+ TRACE_ID,
+ name,
+ kind,
+ attributes,
+ None,
+ ).decision
+
+ def test_composite_rule_based_no_rules_drops(self):
+ decision = self._decision(RuleBasedSamplerConfig())
+
+ self.assertEqual(decision, Decision.DROP)
+
+ def test_composite_rule_based_no_condition_rule_matches(self):
+ decision = self._decision(
+ RuleBasedSamplerConfig(rules=[self._rule(self._always_on())])
+ )
+
+ self.assertEqual(decision, Decision.RECORD_AND_SAMPLE)
+
+ def test_composite_rule_based_first_match_wins(self):
+ rule_based = RuleBasedSamplerConfig(
+ rules=[
+ self._rule(
+ self._always_off(),
+ attribute_values=self._attribute_values(
+ "http.route", ["/health"]
+ ),
+ ),
+ self._rule(
+ self._always_on(),
+ attribute_values=self._attribute_values(
+ "http.route", ["/health"]
+ ),
+ ),
+ ]
+ )
+
+ decision = self._decision(
+ rule_based, attributes={"http.route": "/health"}
+ )
+
+ self.assertEqual(decision, Decision.DROP)
+
+ def test_composite_rule_based_attribute_values_stringifies_values(self):
+ decision = self._decision(
+ RuleBasedSamplerConfig(
+ rules=[
+ self._rule(
+ self._always_on(),
+ attribute_values=self._attribute_values(
+ "http.response.status_code", ["404"]
+ ),
+ )
+ ]
+ ),
+ attributes={"http.response.status_code": 404},
+ )
+
+ self.assertEqual(decision, Decision.RECORD_AND_SAMPLE)
+
+ def test_composite_rule_based_attribute_values_match_array_item(self):
+ decision = self._decision(
+ RuleBasedSamplerConfig(
+ rules=[
+ self._rule(
+ self._always_on(),
+ attribute_values=self._attribute_values(
+ "http.request.method", ["POST"]
+ ),
+ )
+ ]
+ ),
+ attributes={"http.request.method": ("GET", "POST")},
+ )
+
+ self.assertEqual(decision, Decision.RECORD_AND_SAMPLE)
+
+ def test_composite_rule_based_attribute_patterns_include_exclude(self):
+ rule_based = RuleBasedSamplerConfig(
+ rules=[
+ self._rule(
+ self._always_on(),
+ attribute_patterns=self._attribute_patterns(
+ "http.route",
+ ["/api/*"],
+ ["/api/private/*"],
+ ),
+ )
+ ]
+ )
+
+ included = self._decision(
+ rule_based, attributes={"http.route": "/api/users"}
+ )
+ excluded = self._decision(
+ rule_based, attributes={"http.route": "/api/private/user"}
+ )
+ case_mismatch = self._decision(
+ rule_based, attributes={"http.route": "/API/users"}
+ )
+
+ self.assertEqual(included, Decision.RECORD_AND_SAMPLE)
+ self.assertEqual(excluded, Decision.DROP)
+ self.assertEqual(case_mismatch, Decision.DROP)
+
+ def test_composite_rule_based_span_kind(self):
+ rule_based = RuleBasedSamplerConfig(
+ rules=[
+ self._rule(
+ self._always_on(),
+ span_kinds=[SpanKindConfig.server],
+ )
+ ]
+ )
+
+ server = self._decision(rule_based, kind=TraceSpanKind.SERVER)
+ client = self._decision(rule_based, kind=TraceSpanKind.CLIENT)
+
+ self.assertEqual(server, Decision.RECORD_AND_SAMPLE)
+ self.assertEqual(client, Decision.DROP)
+
+ def test_composite_rule_based_parent(self):
+ local_parent_context = trace_api.set_span_in_context(
+ trace_api.NonRecordingSpan(
+ SpanContext(
+ TRACE_ID,
+ SPAN_ID,
+ is_remote=False,
+ trace_flags=TraceFlags.get_default(),
+ )
+ )
+ )
+ remote_parent_context = trace_api.set_span_in_context(
+ trace_api.NonRecordingSpan(
+ SpanContext(
+ TRACE_ID,
+ SPAN_ID,
+ is_remote=True,
+ trace_flags=TraceFlags.get_default(),
+ )
+ )
+ )
+ rule_based = RuleBasedSamplerConfig(
+ rules=[
+ self._rule(
+ self._always_on(),
+ parent=[SpanParentConfig.local],
+ )
+ ]
+ )
+
+ local = self._decision(rule_based, parent_context=local_parent_context)
+ remote = self._decision(
+ rule_based, parent_context=remote_parent_context
+ )
+ no_parent = self._decision(rule_based)
+
+ self.assertEqual(local, Decision.RECORD_AND_SAMPLE)
+ self.assertEqual(remote, Decision.DROP)
+ self.assertEqual(no_parent, Decision.DROP)
+
+ def test_composite_rule_based_multiple_conditions_are_anded(self):
+ rule_based = RuleBasedSamplerConfig(
+ rules=[
+ self._rule(
+ self._always_on(),
+ attribute_values=self._attribute_values(
+ "http.route", ["/users"]
+ ),
+ span_kinds=[SpanKindConfig.server],
+ )
+ ]
+ )
+
+ matching = self._decision(
+ rule_based,
+ kind=TraceSpanKind.SERVER,
+ attributes={"http.route": "/users"},
+ )
+ wrong_kind = self._decision(
+ rule_based,
+ kind=TraceSpanKind.CLIENT,
+ attributes={"http.route": "/users"},
+ )
+
+ self.assertEqual(matching, Decision.RECORD_AND_SAMPLE)
+ self.assertEqual(wrong_kind, Decision.DROP)
+
+ def test_composite_rule_based_nested_probability_sampler(self):
+ provider = self._make_provider(
+ RuleBasedSamplerConfig(
+ rules=[
+ self._rule(
+ ComposableSamplerConfig(
+ probability=ComposableProbabilityConfig(ratio=0.0)
+ )
+ )
+ ]
+ )
+ )
+
+ expected = (
+ "ComposableRuleBased{[(AlwaysMatch:"
+ "ComposableTraceIDRatioBased{threshold=max, ratio=0.0})]}"
+ )
+ self.assertEqual(
+ provider.sampler.get_description(),
+ expected,
+ )
+
+
class TestCreateSpanExporterAndProcessor(unittest.TestCase):
# pylint: disable=no-self-use
diff --git a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py
index 9f67271..d09368c 100644
--- a/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py
+++ b/opentelemetry-sdk/tests/metrics/test_measurement_consumer.py
@@ -3,6 +3,7 @@
# pylint: disable=invalid-name,no-self-use
+from threading import Event, Thread
from time import sleep
from unittest import TestCase
from unittest.mock import MagicMock, Mock, patch
@@ -23,7 +24,8 @@ from opentelemetry.sdk.metrics._internal.sdk_configuration import (
class TestSynchronousMeasurementConsumer(TestCase):
def test_parent(self, _):
self.assertIsInstance(
- SynchronousMeasurementConsumer(MagicMock()), MeasurementConsumer
+ SynchronousMeasurementConsumer(MagicMock(), metric_readers=()),
+ MeasurementConsumer,
)
def test_creates_metric_reader_storages(self, MockMetricReaderStorage):
@@ -33,9 +35,9 @@ class TestSynchronousMeasurementConsumer(TestCase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=reader_mocks,
views=Mock(),
- )
+ ),
+ metric_readers=reader_mocks,
)
self.assertEqual(len(MockMetricReaderStorage.mock_calls), 5)
@@ -50,9 +52,9 @@ class TestSynchronousMeasurementConsumer(TestCase):
SdkConfiguration(
exemplar_filter=Mock(should_sample=Mock(return_value=False)),
resource=Mock(),
- metric_readers=reader_mocks,
views=Mock(),
- )
+ ),
+ metric_readers=reader_mocks,
)
measurement_mock = Mock()
consumer.consume_measurement(measurement_mock)
@@ -72,9 +74,9 @@ class TestSynchronousMeasurementConsumer(TestCase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=reader_mocks,
views=Mock(),
- )
+ ),
+ metric_readers=reader_mocks,
)
for r_mock, rs_mock in zip(reader_mocks, reader_storage_mocks):
rs_mock.collect.assert_not_called()
@@ -91,9 +93,9 @@ class TestSynchronousMeasurementConsumer(TestCase):
SdkConfiguration(
exemplar_filter=Mock(should_sample=Mock(return_value=False)),
resource=Mock(),
- metric_readers=[reader_mock],
views=Mock(),
- )
+ ),
+ metric_readers=[reader_mock],
)
async_instrument_mocks = [MagicMock() for _ in range(5)]
for i_mock in async_instrument_mocks:
@@ -122,9 +124,9 @@ class TestSynchronousMeasurementConsumer(TestCase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=[reader_mock],
views=Mock(),
- )
+ ),
+ metric_readers=[reader_mock],
)
def sleep_1(*args, **kwargs):
@@ -156,9 +158,9 @@ class TestSynchronousMeasurementConsumer(TestCase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=[reader_mock],
views=Mock(),
- )
+ ),
+ metric_readers=[reader_mock],
)
consumer.register_asynchronous_instrument(
@@ -188,3 +190,123 @@ class TestSynchronousMeasurementConsumer(TestCase):
callback_options_time_call,
10000,
)
+
+
+class TestSynchronousMeasurementConsumerConcurrency(TestCase):
+ def test_consume_measurement_does_not_acquire_lock(self):
+ """consume_measurement must stay lock free on the hot path."""
+ with patch(
+ "opentelemetry.sdk.metrics._internal."
+ "measurement_consumer.MetricReaderStorage"
+ ):
+ consumer = SynchronousMeasurementConsumer(
+ SdkConfiguration(
+ exemplar_filter=Mock(
+ should_sample=Mock(return_value=False)
+ ),
+ resource=Mock(),
+ views=Mock(),
+ ),
+ metric_readers=[Mock()],
+ )
+
+ mock_lock = MagicMock()
+ # pylint: disable-next=protected-access
+ consumer._lock = mock_lock
+
+ consumer.consume_measurement(Mock())
+
+ mock_lock.__enter__.assert_not_called()
+
+ def test_concurrent_changes_to_metric_readers(self):
+ timeout = 1
+ failure = None
+ iteration_started = Event()
+ mutation_done = Event()
+ iteration_timeout_error = "Timed out waiting for iteration to start"
+ mutation_timeout_error = "Timed out waiting for mutation to be done"
+
+ consumer = SynchronousMeasurementConsumer(
+ SdkConfiguration(
+ exemplar_filter=MagicMock(),
+ resource=MagicMock(),
+ views=MagicMock(),
+ ),
+ metric_readers=[MagicMock()],
+ )
+
+ def _hooked_iter(iterable):
+ nonlocal failure
+
+ iterable = iter(iterable)
+ iteration_started.set()
+ if not mutation_done.wait(timeout):
+ failure = mutation_timeout_error
+ yield next(iterable, None)
+ yield from iterable
+
+ class HookedDict(dict):
+ def values(self):
+ return _hooked_iter(super().values())
+
+ with patch.object(
+ consumer,
+ "_reader_storages",
+ # pylint: disable-next=protected-access
+ HookedDict(consumer._reader_storages),
+ ):
+
+ def mutate():
+ """Directly mutate _reader_storages after iteration starts"""
+ nonlocal failure
+ if not iteration_started.wait(timeout):
+ failure = iteration_timeout_error
+ # pylint: disable-next=protected-access
+ consumer._reader_storages.clear()
+ mutation_done.set()
+
+ # Verify that test setup works (direct mutation with no synchronization fails)
+ with self.assertRaises(RuntimeError) as cm:
+ t = Thread(target=mutate)
+ t.start()
+ try:
+ consumer.consume_measurement(MagicMock())
+ finally:
+ t.join()
+ self.assertEqual(
+ "dictionary changed size during iteration", str(cm.exception)
+ )
+ self.assertIsNone(failure)
+
+ # Reset the events for the second scenario
+ iteration_started.clear()
+ mutation_done.clear()
+ failure = None
+
+ with patch.object(
+ consumer,
+ "_reader_storages",
+ # pylint: disable-next=protected-access
+ HookedDict(consumer._reader_storages),
+ ):
+
+ def add_and_remove_readers():
+ """Mutate via the public API while consume_measurement runs"""
+ nonlocal failure
+ if not iteration_started.wait(timeout):
+ failure = iteration_timeout_error
+ reader = MagicMock()
+ consumer.add_metric_reader(reader)
+ consumer.remove_metric_reader(reader)
+ mutation_done.set()
+
+ # The copy-on-write API never mutates the mapping being
+ # iterated, so `consume_measurement` completes without raising even
+ # though add/remove run concurrently mid-iteration.
+ t = Thread(target=add_and_remove_readers)
+ t.start()
+ try:
+ consumer.consume_measurement(MagicMock())
+ finally:
+ t.join()
+ self.assertIsNone(failure)
diff --git a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py
index ec70ff1..281aae9 100644
--- a/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py
+++ b/opentelemetry-sdk/tests/metrics/test_metric_reader_storage.py
@@ -68,7 +68,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(view1, view2),
),
MagicMock(
@@ -135,7 +134,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(view1, view2),
),
MagicMock(
@@ -245,7 +243,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(view1,),
),
MagicMock(
@@ -276,7 +273,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(View(instrument_name="test"),),
),
MagicMock(
@@ -322,7 +318,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(),
),
MagicMock(
@@ -358,7 +353,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(
instrument_name="name", aggregation=DropAggregation()
@@ -386,7 +380,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(View(instrument_name="name"),),
),
MagicMock(
@@ -433,7 +426,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(
instrument_name="observable_counter",
@@ -482,7 +474,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(instrument_name="observable_counter_0", name="foo"),
View(instrument_name="observable_counter_1", name="foo"),
@@ -540,7 +531,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(instrument_name="bar", name="foo"),
View(instrument_name="baz", name="foo"),
@@ -609,7 +599,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(instrument_name="foo"),
View(instrument_name="bar"),
@@ -662,7 +651,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(instrument_name="bar", name="foo"),
View(instrument_name="baz", name="foo"),
@@ -713,7 +701,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(instrument_name="bar", name="foo"),
View(instrument_name="baz", name="foo"),
@@ -760,7 +747,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(instrument_name="observable_counter_0", name="foo"),
View(instrument_name="observable_counter_1", name="foo"),
@@ -815,7 +801,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(instrument_name="observable_counter", name="foo"),
View(instrument_name="histogram", name="foo"),
@@ -870,7 +855,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(instrument_name="observable_counter_0", name="foo"),
View(instrument_name="observable_counter_1", name="foo"),
@@ -925,7 +909,6 @@ class TestMetricReaderStorage(ConcurrencyTestBase):
SdkConfiguration(
exemplar_filter=Mock(),
resource=Mock(),
- metric_readers=(),
views=(
View(instrument_name="up_down_counter", name="foo"),
View(
diff --git a/opentelemetry-sdk/tests/metrics/test_metrics.py b/opentelemetry-sdk/tests/metrics/test_metrics.py
index beb8dbb..433ad05 100644
--- a/opentelemetry-sdk/tests/metrics/test_metrics.py
+++ b/opentelemetry-sdk/tests/metrics/test_metrics.py
@@ -2,10 +2,9 @@
# SPDX-License-Identifier: Apache-2.0
# pylint: disable=protected-access,no-self-use,too-many-lines
-
import weakref
from collections.abc import Callable, Iterable, Sequence
-from logging import WARNING
+from logging import DEBUG, WARNING
from threading import Lock
from time import sleep
from typing import Any
@@ -34,6 +33,7 @@ from opentelemetry.sdk.metrics._internal import (
_RuleBasedMeterConfigurator,
)
from opentelemetry.sdk.metrics.export import (
+ InMemoryMetricReader,
Metric,
MetricExporter,
MetricExportResult,
@@ -488,6 +488,36 @@ class TestMeterProvider(ConcurrencyTestBase, TestCase):
sync_consumer_instance.consume_measurement.assert_called()
+ def test_addition_of_metric_reader(self):
+ internal_logger = "opentelemetry.sdk.metrics._internal"
+ export_logger = "opentelemetry.sdk.metrics._internal.export"
+
+ reader = InMemoryMetricReader()
+ meter_provider = MeterProvider()
+ meter = meter_provider.get_meter(__name__)
+ counter = meter.create_counter("counter")
+ counter.add(1)
+ # Suppress warnings for calling collect on an unregistered metric reader
+ with self.assertLogs(export_logger, DEBUG):
+ self.assertIsNone(reader.get_metrics_data())
+
+ meter_provider.add_metric_reader(reader)
+ counter.add(1)
+ self.assertIsNotNone(reader.get_metrics_data())
+
+ with self.assertLogs(internal_logger, DEBUG) as cm:
+ meter_provider.add_metric_reader(reader)
+ self.assertIn("has been registered already!", cm.output[0])
+
+ meter_provider.remove_metric_reader(reader)
+ counter.add(1)
+ with self.assertLogs(export_logger, DEBUG):
+ self.assertIsNone(reader.get_metrics_data())
+
+ with self.assertLogs(internal_logger, DEBUG) as cm:
+ meter_provider.remove_metric_reader(reader)
+ self.assertIn("has not been registered!", cm.output[0])
+
class TestMeterConcurrency(ConcurrencyTestBase, TestCase):
def test_create_instrument_concurrency(self):
@@ -548,9 +578,7 @@ class TestMeter(TestCase):
def setUp(self):
self.meter = Meter(Mock(), Mock())
- # TODO: convert to assertNoLogs instead of mocking logger when 3.10 is baseline
- @patch("opentelemetry.sdk.metrics._internal._logger")
- def test_repeated_instrument_names(self, logger_mock):
+ def test_repeated_instrument_names(self):
with self.assertNotRaises(Exception):
self.meter.create_counter("counter")
self.meter.create_up_down_counter("up_down_counter")
@@ -572,18 +600,24 @@ class TestMeter(TestCase):
"histogram",
"gauge",
]:
- getattr(self.meter, f"create_{instrument_name}")(instrument_name)
- logger_mock.warning.assert_not_called()
+ with self.assertNoLogs(
+ "opentelemetry.sdk.metrics._internal", level="WARNING"
+ ):
+ getattr(self.meter, f"create_{instrument_name}")(
+ instrument_name
+ )
for instrument_name in [
"observable_counter",
"observable_gauge",
"observable_up_down_counter",
]:
- getattr(self.meter, f"create_{instrument_name}")(
- instrument_name, callbacks=[Mock()]
- )
- logger_mock.warning.assert_not_called()
+ with self.assertNoLogs(
+ "opentelemetry.sdk.metrics._internal", level="WARNING"
+ ):
+ getattr(self.meter, f"create_{instrument_name}")(
+ instrument_name, callbacks=[Mock()]
+ )
def test_repeated_instrument_names_with_different_advisory(self):
with self.assertNotRaises(Exception):
diff --git a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py
index 1de1af3..73b0e6e 100644
--- a/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py
+++ b/opentelemetry-sdk/tests/metrics/test_view_instrument_match.py
@@ -71,7 +71,6 @@ class Test_ViewInstrumentMatch(TestCase): # pylint: disable=invalid-name
cls.sdk_configuration = SdkConfiguration(
exemplar_filter=Mock(),
resource=cls.mock_resource,
- metric_readers=[],
views=[],
)
diff --git a/opentelemetry-sdk/tests/resources/test_resources.py b/opentelemetry-sdk/tests/resources/test_resources.py
index 8c6ff46..2b945ea 100644
--- a/opentelemetry-sdk/tests/resources/test_resources.py
+++ b/opentelemetry-sdk/tests/resources/test_resources.py
@@ -1,6 +1,10 @@
# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0
+
+# pylint: disable=too-many-lines
+
import os
+import subprocess
import sys
import unittest
import uuid
@@ -10,6 +14,7 @@ from os import environ
from unittest.mock import Mock, patch
from urllib import parse
+import opentelemetry.sdk.resources as _resources_module
from opentelemetry.sdk.environment_variables import (
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS,
)
@@ -34,6 +39,7 @@ from opentelemetry.sdk.resources import (
PROCESS_RUNTIME_DESCRIPTION,
PROCESS_RUNTIME_NAME,
PROCESS_RUNTIME_VERSION,
+ SERVICE_INSTANCE_ID,
SERVICE_NAME,
TELEMETRY_SDK_LANGUAGE,
TELEMETRY_SDK_NAME,
@@ -43,6 +49,7 @@ from opentelemetry.sdk.resources import (
ProcessResourceDetector,
Resource,
ResourceDetector,
+ ServiceInstanceIdResourceDetector,
_HostResourceDetector,
get_aggregated_resources,
)
@@ -59,6 +66,11 @@ except ImportError:
class TestResources(unittest.TestCase):
def setUp(self) -> None:
environ[OTEL_RESOURCE_ATTRIBUTES] = ""
+ self._service_instance_id = (
+ ServiceInstanceIdResourceDetector()
+ .detect()
+ .attributes[SERVICE_INSTANCE_ID]
+ )
def tearDown(self) -> None:
environ.pop(OTEL_RESOURCE_ATTRIBUTES)
@@ -79,6 +91,7 @@ class TestResources(unittest.TestCase):
TELEMETRY_SDK_NAME: "opentelemetry",
TELEMETRY_SDK_LANGUAGE: "python",
TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION,
+ SERVICE_INSTANCE_ID: self._service_instance_id,
SERVICE_NAME: "unknown_service",
}
@@ -105,40 +118,30 @@ class TestResources(unittest.TestCase):
resource = Resource.get_empty()
self.assertEqual(resource, _EMPTY_RESOURCE)
- resource = Resource.create(None)
- self.assertEqual(
- resource,
- _DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
- ),
+ expected_default = _DEFAULT_RESOURCE.merge(
+ Resource(
+ {
+ SERVICE_INSTANCE_ID: self._service_instance_id,
+ SERVICE_NAME: "unknown_service",
+ },
+ "",
+ )
)
+
+ resource = Resource.create(None)
+ self.assertEqual(resource, expected_default)
self.assertEqual(resource.schema_url, "")
resource = Resource.create(None, None)
- self.assertEqual(
- resource,
- _DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
- ),
- )
+ self.assertEqual(resource, expected_default)
self.assertEqual(resource.schema_url, "")
resource = Resource.create({})
- self.assertEqual(
- resource,
- _DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
- ),
- )
+ self.assertEqual(resource, expected_default)
self.assertEqual(resource.schema_url, "")
resource = Resource.create({}, None)
- self.assertEqual(
- resource,
- _DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
- ),
- )
+ self.assertEqual(resource, expected_default)
self.assertEqual(resource.schema_url, "")
def test_resource_merge(self):
@@ -202,6 +205,7 @@ class TestResources(unittest.TestCase):
TELEMETRY_SDK_NAME: "opentelemetry",
TELEMETRY_SDK_LANGUAGE: "python",
TELEMETRY_SDK_VERSION: _OPENTELEMETRY_SDK_VERSION,
+ SERVICE_INSTANCE_ID: self._service_instance_id,
SERVICE_NAME: "unknown_service",
}
@@ -257,7 +261,13 @@ class TestResources(unittest.TestCase):
self.assertEqual(
aggregated_resources,
_DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
+ Resource(
+ {
+ SERVICE_INSTANCE_ID: self._service_instance_id,
+ SERVICE_NAME: "unknown_service",
+ },
+ "",
+ )
),
)
@@ -308,7 +318,13 @@ class TestResources(unittest.TestCase):
[resource_detector1, resource_detector2, resource_detector3]
),
_DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
+ Resource(
+ {
+ SERVICE_INSTANCE_ID: self._service_instance_id,
+ SERVICE_NAME: "unknown_service",
+ },
+ "",
+ )
).merge(
Resource(
{
@@ -351,7 +367,13 @@ class TestResources(unittest.TestCase):
self.assertEqual(
get_aggregated_resources([resource_detector1, resource_detector2]),
_DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
+ Resource(
+ {
+ SERVICE_INSTANCE_ID: self._service_instance_id,
+ SERVICE_NAME: "unknown_service",
+ },
+ "",
+ )
).merge(
Resource(
{"key1": "value1", "key2": "value2", "key3": "value3"},
@@ -365,7 +387,13 @@ class TestResources(unittest.TestCase):
[resource_detector2, resource_detector3]
),
_DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
+ Resource(
+ {
+ SERVICE_INSTANCE_ID: self._service_instance_id,
+ SERVICE_NAME: "unknown_service",
+ },
+ "",
+ )
).merge(
Resource({"key2": "value2", "key3": "value3"}, "url1")
),
@@ -383,7 +411,13 @@ class TestResources(unittest.TestCase):
]
),
_DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
+ Resource(
+ {
+ SERVICE_INSTANCE_ID: self._service_instance_id,
+ SERVICE_NAME: "unknown_service",
+ },
+ "",
+ )
).merge(
Resource(
{
@@ -407,7 +441,13 @@ class TestResources(unittest.TestCase):
self.assertEqual(
get_aggregated_resources([resource_detector]),
_DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
+ Resource(
+ {
+ SERVICE_INSTANCE_ID: self._service_instance_id,
+ SERVICE_NAME: "unknown_service",
+ },
+ "",
+ )
),
)
@@ -427,7 +467,13 @@ class TestResources(unittest.TestCase):
self.assertEqual(
get_aggregated_resources([resource_detector]),
_DEFAULT_RESOURCE.merge(
- Resource({SERVICE_NAME: "unknown_service"}, "")
+ Resource(
+ {
+ SERVICE_INSTANCE_ID: self._service_instance_id,
+ SERVICE_NAME: "unknown_service",
+ },
+ "",
+ )
),
)
mock_logger.warning.assert_called_with(
@@ -912,3 +958,113 @@ class TestHostResourceDetector(unittest.TestCase):
resource.attributes["telemetry.sdk.language"], "python"
)
self.assertIn(HOST_NAME, resource.attributes)
+
+
+# pylint: disable=protected-access
+class TestServiceInstanceIdResourceDetector(unittest.TestCase):
+ def setUp(self) -> None:
+ self._orig_instance_id = _resources_module._service_instance_id
+ self._orig_instance_pid = _resources_module._service_instance_id_pid
+
+ def tearDown(self) -> None:
+ _resources_module._service_instance_id = self._orig_instance_id
+ _resources_module._service_instance_id_pid = self._orig_instance_pid
+
+ def test_detect_value_is_valid_uuid4(self):
+ _resources_module._service_instance_id = None
+ _resources_module._service_instance_id_pid = None
+ detector = ServiceInstanceIdResourceDetector()
+ value = detector.detect().attributes[SERVICE_INSTANCE_ID]
+ parsed = uuid.UUID(value)
+ self.assertEqual(parsed.version, 4)
+
+ def test_detect_stable_within_instance(self):
+ _resources_module._service_instance_id = None
+ _resources_module._service_instance_id_pid = None
+ detector = ServiceInstanceIdResourceDetector()
+ id1 = detector.detect().attributes[SERVICE_INSTANCE_ID]
+ id2 = detector.detect().attributes[SERVICE_INSTANCE_ID]
+ self.assertEqual(id1, id2)
+
+ def test_detect_shared_across_instances(self):
+ _resources_module._service_instance_id = None
+ _resources_module._service_instance_id_pid = None
+ id1 = (
+ ServiceInstanceIdResourceDetector()
+ .detect()
+ .attributes[SERVICE_INSTANCE_ID]
+ )
+ id2 = (
+ ServiceInstanceIdResourceDetector()
+ .detect()
+ .attributes[SERVICE_INSTANCE_ID]
+ )
+ self.assertEqual(id1, id2)
+
+ def test_detect_pid_change_generates_new_id(self):
+ _resources_module._service_instance_id = "old-id"
+ _resources_module._service_instance_id_pid = os.getpid() - 1
+ new_id = (
+ ServiceInstanceIdResourceDetector()
+ .detect()
+ .attributes[SERVICE_INSTANCE_ID]
+ )
+ self.assertNotEqual(new_id, "old-id")
+ self.assertEqual(
+ _resources_module._service_instance_id_pid, os.getpid()
+ )
+ uuid.UUID(new_id)
+
+ def test_detect_pid_unchanged_returns_same_id(self):
+ known_id = "known-stable-id"
+ _resources_module._service_instance_id = known_id
+ _resources_module._service_instance_id_pid = os.getpid()
+ result = (
+ ServiceInstanceIdResourceDetector()
+ .detect()
+ .attributes[SERVICE_INSTANCE_ID]
+ )
+ self.assertEqual(result, known_id)
+
+ @unittest.skipUnless(hasattr(os, "fork"), "requires os.fork")
+ def test_detect_fork_generates_new_id(self):
+ script = """\
+import os
+import sys
+
+from opentelemetry.sdk.resources import ServiceInstanceIdResourceDetector, SERVICE_INSTANCE_ID
+
+parent_id = ServiceInstanceIdResourceDetector().detect().attributes[SERVICE_INSTANCE_ID]
+
+pid = os.fork()
+if not pid:
+ child_id = ServiceInstanceIdResourceDetector().detect().attributes[SERVICE_INSTANCE_ID]
+ print(f"child:{child_id}", flush=True)
+ os._exit(0)
+else:
+ os.waitpid(pid, 0)
+ print(f"parent:{parent_id}", flush=True)
+"""
+ result = subprocess.run(
+ [sys.executable, "-c", script],
+ capture_output=True,
+ text=True,
+ check=True,
+ )
+ ids = dict(
+ line.split(":", 1) for line in result.stdout.strip().splitlines()
+ )
+ parent_id, child_id = ids["parent"], ids["child"]
+ self.assertNotEqual(parent_id, child_id)
+ self.assertEqual(uuid.UUID(parent_id).version, 4)
+ self.assertEqual(uuid.UUID(child_id).version, 4)
+
+ @patch.dict(
+ environ,
+ {OTEL_EXPERIMENTAL_RESOURCE_DETECTORS: "service_instance"},
+ clear=True,
+ )
+ def test_resource_detector_entry_points_service_instance(self):
+ resource = Resource.create()
+ self.assertIn(SERVICE_INSTANCE_ID, resource.attributes)
+ uuid.UUID(resource.attributes[SERVICE_INSTANCE_ID])
diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py
index fbf4bd3..67c82f4 100644
--- a/opentelemetry-sdk/tests/test_configurator.py
+++ b/opentelemetry-sdk/tests/test_configurator.py
@@ -1255,7 +1255,7 @@ class TestMetricsInit(TestCase):
provider._sdk_config.resource.attributes.get("service.name"),
"otlp-service",
)
- reader = provider._sdk_config.metric_readers[0]
+ reader = provider._metric_readers[0]
self.assertIsInstance(reader, DummyMetricReader)
self.assertIsInstance(reader.exporter, DummyOTLPMetricExporter)
@@ -1268,7 +1268,7 @@ class TestMetricsInit(TestCase):
self.assertEqual(self.set_provider_mock.call_count, 1)
provider = self.set_provider_mock.call_args[0][0]
self.assertIsInstance(provider, DummyMeterProvider)
- reader = provider._sdk_config.metric_readers[0]
+ reader = provider._metric_readers[0]
self.assertIsInstance(reader, DummyMetricReaderPullExporter)
def test_metrics_init_exporter_uses_exporter_args_map(self):
@@ -1282,7 +1282,7 @@ class TestMetricsInit(TestCase):
},
)
provider = self.set_provider_mock.call_args[0][0]
- reader = provider._sdk_config.metric_readers[0]
+ reader = provider._metric_readers[0]
self.assertEqual(reader.exporter.compression, "gzip")
def test_metrics_init_meter_configurator_none_by_default(self):
@@ -1565,3 +1565,118 @@ class TestClearLoggingHandlers(TestCase):
self.assertEqual(logging.config.dictConfig.__name__, "dictConfig")
self.assertEqual(logging.basicConfig.__name__, "basicConfig")
self.assertEqual(logging.config.fileConfig.__name__, "fileConfig")
+
+
+class TestOpAMPInit(TestCase):
+ @patch("opentelemetry.sdk._configuration._init_metrics")
+ @patch("opentelemetry.sdk._configuration._init_tracing")
+ @patch("opentelemetry.sdk._configuration._init_logging")
+ @patch("opentelemetry.sdk._configuration.entry_points")
+ @patch("opentelemetry.sdk._configuration.Resource")
+ def test_pre_sdk_init_function_found(
+ self,
+ mock_resource,
+ mock_entry_points,
+ mock_logging,
+ mock_tracing,
+ mock_metrics,
+ ):
+ init_function = mock.Mock()
+ mock_entry_points.side_effect = [
+ [IterEntryPoint("pre_sdk_init_function", init_function)],
+ [],
+ ]
+
+ _initialize_components(id_generator=1)
+
+ mock_entry_points.assert_has_calls(
+ [
+ mock.call(
+ group="_opentelemetry_opamp", name="pre_sdk_init_function"
+ )
+ ]
+ )
+ init_function.assert_called_once_with(
+ mock_resource.create.return_value
+ )
+
+ @patch("opentelemetry.sdk._configuration._init_metrics")
+ @patch("opentelemetry.sdk._configuration._init_tracing")
+ @patch("opentelemetry.sdk._configuration._init_logging")
+ @patch("opentelemetry.sdk._configuration.entry_points")
+ @patch("opentelemetry.sdk._configuration.Resource")
+ def test_post_sdk_init_function_found(
+ self,
+ mock_resource,
+ mock_entry_points,
+ mock_logging,
+ mock_tracing,
+ mock_metrics,
+ ):
+ init_function = mock.Mock()
+ mock_entry_points.side_effect = [
+ [],
+ [IterEntryPoint("post_sdk_init_function", init_function)],
+ ]
+
+ _initialize_components(id_generator=1)
+
+ mock_entry_points.assert_has_calls(
+ [
+ mock.call(
+ group="_opentelemetry_opamp", name="post_sdk_init_function"
+ )
+ ]
+ )
+ init_function.assert_called_once_with(
+ mock_resource.create.return_value
+ )
+
+ @patch("opentelemetry.sdk._configuration._init_metrics")
+ @patch("opentelemetry.sdk._configuration._init_tracing")
+ @patch("opentelemetry.sdk._configuration._init_logging")
+ @patch("opentelemetry.sdk._configuration.entry_points")
+ def test_init_function_load_failure(
+ self, mock_entry_points, mock_logging, mock_tracing, mock_metrics
+ ):
+ entry_point_mock = mock.Mock()
+ entry_point_mock.load.side_effect = AttributeError(
+ "module 'foo' has no attribute 'OpampInit'"
+ )
+ mock_entry_points.configure_mock(
+ return_value=[entry_point_mock],
+ )
+ entry_point_mock.__str__ = lambda x: "<EntryPoint>"
+
+ with self.assertLogs(level="WARNING") as cm:
+ _initialize_components(id_generator=1)
+
+ mock_entry_points.assert_has_calls(
+ [
+ mock.call(
+ group="_opentelemetry_opamp", name="pre_sdk_init_function"
+ )
+ ]
+ )
+
+ self.assertIn(
+ "WARNING:opentelemetry.sdk._configuration:Failed to load OpAMP init function from entry point,"
+ " <EntryPoint>: module 'foo' has no attribute 'OpampInit'",
+ cm.output,
+ )
+
+ @patch("opentelemetry.sdk._configuration._init_metrics")
+ @patch("opentelemetry.sdk._configuration._init_tracing")
+ @patch("opentelemetry.sdk._configuration._init_logging")
+ @patch("opentelemetry.sdk._configuration.entry_points")
+ def test_init_function_not_found(
+ self, mock_entry_points, mock_logging, mock_tracing, mock_metrics
+ ):
+ mock_entry_points.configure_mock(return_value=[])
+
+ with self.assertLogs(level="DEBUG") as cm:
+ _initialize_components(id_generator=1)
+ self.assertIn(
+ "DEBUG:opentelemetry.sdk._configuration:No OpAMP init function found",
+ cm.output,
+ )
diff --git a/opentelemetry-sdk/tests/trace/composite_sampler/test_rule_based.py b/opentelemetry-sdk/tests/trace/composite_sampler/test_rule_based.py
index a6bc11c..0a398f2 100644
--- a/opentelemetry-sdk/tests/trace/composite_sampler/test_rule_based.py
+++ b/opentelemetry-sdk/tests/trace/composite_sampler/test_rule_based.py
@@ -8,10 +8,26 @@ from opentelemetry.sdk.trace._sampling_experimental import (
composite_sampler,
)
from opentelemetry.sdk.trace._sampling_experimental._rule_based import (
+ AllPredicate,
+ AlwaysMatchPredicate,
+ AttributePatternsPredicate,
AttributePredicate,
+ AttributeValuesPredicate,
+ ParentPredicate,
+ SpanKindPredicate,
)
from opentelemetry.sdk.trace.id_generator import RandomIdGenerator
from opentelemetry.sdk.trace.sampling import Decision
+from opentelemetry.trace import (
+ NonRecordingSpan,
+ SpanContext,
+ SpanKind,
+ TraceFlags,
+ set_span_in_context,
+)
+
+TRACE_ID = int("00112233445566778800000000000000", 16)
+SPAN_ID = int("0123456789abcdef", 16)
class NameIsFooPredicate:
@@ -30,6 +46,36 @@ class NameIsFooPredicate:
return "NameIsFooPredicate"
+def _predicate_result(
+ predicate,
+ *,
+ parent_ctx=None,
+ span_kind=None,
+ attributes=None,
+):
+ return predicate(
+ parent_ctx=parent_ctx,
+ name="span",
+ span_kind=span_kind,
+ attributes=attributes,
+ links=None,
+ trace_state=None,
+ )
+
+
+def _parent_context(is_remote: bool):
+ return set_span_in_context(
+ NonRecordingSpan(
+ SpanContext(
+ TRACE_ID,
+ SPAN_ID,
+ is_remote=is_remote,
+ trace_flags=TraceFlags.get_default(),
+ )
+ )
+ )
+
+
def test_description_with_no_rules():
assert (
composable_rule_based(rules=[]).get_description()
@@ -37,14 +83,188 @@ def test_description_with_no_rules():
)
+def test_always_match_predicate():
+ assert _predicate_result(AlwaysMatchPredicate()) is True
+ assert str(AlwaysMatchPredicate()) == "AlwaysMatch"
+
+
+def test_all_predicate_matches_when_all_predicates_match():
+ predicate = AllPredicate(
+ [
+ AttributeValuesPredicate("http.route", ["/users"]),
+ SpanKindPredicate([SpanKind.SERVER]),
+ ]
+ )
+
+ assert (
+ _predicate_result(
+ predicate,
+ span_kind=SpanKind.SERVER,
+ attributes={"http.route": "/users"},
+ )
+ is True
+ )
+
+
+def test_all_predicate_does_not_match_when_any_predicate_does_not_match():
+ predicate = AllPredicate(
+ [
+ AttributeValuesPredicate("http.route", ["/users"]),
+ SpanKindPredicate([SpanKind.SERVER]),
+ ]
+ )
+
+ assert (
+ _predicate_result(
+ predicate,
+ span_kind=SpanKind.CLIENT,
+ attributes={"http.route": "/users"},
+ )
+ is False
+ )
+
+
+def test_attribute_values_predicate_no_attributes():
+ assert (
+ _predicate_result(AttributeValuesPredicate("http.route", ["/users"]))
+ is False
+ )
+
+
+def test_attribute_values_predicate_stringifies_values():
+ assert (
+ _predicate_result(
+ AttributeValuesPredicate("http.response.status_code", ["404"]),
+ attributes={"http.response.status_code": 404},
+ )
+ is True
+ )
+
+
+def test_attribute_values_predicate_matches_array_item():
+ assert (
+ _predicate_result(
+ AttributeValuesPredicate("http.request.method", ["POST"]),
+ attributes={"http.request.method": ("GET", "POST")},
+ )
+ is True
+ )
+
+
+def test_attribute_values_predicate_no_match():
+ assert (
+ _predicate_result(
+ AttributeValuesPredicate("http.route", ["/users"]),
+ attributes={"http.route": "/health"},
+ )
+ is False
+ )
+
+
+def test_attribute_patterns_predicate_includes_all_by_default():
+ assert (
+ _predicate_result(
+ AttributePatternsPredicate("http.route"),
+ attributes={"http.route": "/users"},
+ )
+ is True
+ )
+
+
+def test_attribute_patterns_predicate_include_exclude_precedence():
+ predicate = AttributePatternsPredicate(
+ "http.route",
+ included=["/api/*"],
+ excluded=["/api/private/*"],
+ )
+
+ assert (
+ _predicate_result(predicate, attributes={"http.route": "/api/users"})
+ is True
+ )
+ assert (
+ _predicate_result(
+ predicate, attributes={"http.route": "/api/private/user"}
+ )
+ is False
+ )
+
+
+def test_attribute_patterns_predicate_is_case_sensitive():
+ assert (
+ _predicate_result(
+ AttributePatternsPredicate("http.route", included=["/api/*"]),
+ attributes={"http.route": "/API/users"},
+ )
+ is False
+ )
+
+
+def test_attribute_patterns_predicate_matches_array_item():
+ assert (
+ _predicate_result(
+ AttributePatternsPredicate("http.request.method", ["POST"]),
+ attributes={"http.request.method": ("GET", "POST")},
+ )
+ is True
+ )
+
+
+def test_span_kind_predicate():
+ predicate = SpanKindPredicate([SpanKind.SERVER])
+
+ assert _predicate_result(predicate, span_kind=SpanKind.SERVER) is True
+ assert _predicate_result(predicate, span_kind=SpanKind.CLIENT) is False
+ assert _predicate_result(predicate) is False
+
+
+def test_parent_predicate_matches_no_parent():
+ assert _predicate_result(ParentPredicate(["none"])) is True
+ assert _predicate_result(ParentPredicate(["local"])) is False
+
+
+def test_parent_predicate_matches_local_parent():
+ local_parent_ctx = _parent_context(is_remote=False)
+
+ assert (
+ _predicate_result(
+ ParentPredicate(["local"]), parent_ctx=local_parent_ctx
+ )
+ is True
+ )
+ assert (
+ _predicate_result(
+ ParentPredicate(["remote"]), parent_ctx=local_parent_ctx
+ )
+ is False
+ )
+
+
+def test_parent_predicate_matches_remote_parent():
+ remote_parent_ctx = _parent_context(is_remote=True)
+
+ assert (
+ _predicate_result(
+ ParentPredicate(["remote"]), parent_ctx=remote_parent_ctx
+ )
+ is True
+ )
+ assert (
+ _predicate_result(
+ ParentPredicate(["local"]), parent_ctx=remote_parent_ctx
+ )
+ is False
+ )
+
+
def test_description_with_rules():
rules = [
- (AttributePredicate("foo", "bar"), composable_always_on()),
+ (AttributeValuesPredicate("foo", ["bar"]), composable_always_on()),
(NameIsFooPredicate(), composable_always_off()),
]
assert (
composable_rule_based(rules=rules).get_description()
- == "ComposableRuleBased{[(foo=bar:ComposableAlwaysOn),(NameIsFooPredicate:ComposableAlwaysOff)]}"
+ == "ComposableRuleBased{[(foo in [bar]:ComposableAlwaysOn),(NameIsFooPredicate:ComposableAlwaysOff)]}"
)
@@ -95,7 +315,7 @@ def test_should_sample_match():
def test_should_sample_match_multiple_rules():
rules = [
- (AttributePredicate("foo", "bar"), composable_always_off()),
+ (AttributeValuesPredicate("foo", ["bar"]), composable_always_off()),
(NameIsFooPredicate(), composable_always_on()),
]
sampler = composite_sampler(composable_rule_based(rules=rules))