From 8734dfaab3c14395c6dc0fe08c68964a5813b4b9 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 19 Jun 2025 15:02:40 +0200 Subject: [PATCH 1/8] opentelemetry-sdk: sketch of an OpAMP integration --- .../sdk/_configuration/__init__.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index a982b9de71..2de5d957e4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -523,6 +523,34 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: raise RuntimeError(f"{id_generator_name} is not an IdGenerator") +def _import_opamp() -> callable[[...], None]: + # this in development, at the moment we are looking for a callable that takes + # the resource and instantiate an OpAMP agent. + # Since configuration is not specified every implementors may have its own. + # OpAMPAgent and OpAMPClient will be contributed once I finish writing tests :) + # def opamp_init(resource: Resource): + # endpoint = os.environ.get("OTEL_PYTHON_OPAMP_ENDPOINT") + # if endpoint: + # opamp_client = OpAMPClient( + # endpoint=endpoint, + # agent_identifying_attributes={ + # "service.name": resource.get("service.name"), + # "deployment.environment.name": resource.get("deployment.environment.name"), + # }, + # ) + # opamp_agent = OpAMPAgent( + # interval=30, + # handler=opamp_handler, # this is an handler that gets called to process each OpAMP message + # client=opamp_client, + # ) + # opamp_agent.start() + _, opamp_init_func = _import_config_components( + ["_init_func"], "_opentelemetry_opamp" + )[0] + + return opamp_init_func + + def _initialize_components( auto_instrumentation_version: str | None = None, trace_exporter_names: list[str] | None = None, @@ -581,6 +609,18 @@ def _initialize_components( # from the env variable else defaults to "unknown_service" resource = Resource.create(resource_attributes) + # OpAMP is a system created to configure OpenTelemetry SDKs with a remote config. + # This is different than other init helpers because setting up OpAMP requires distro + # provided code as it's not strictly specified. We call OpAMP init before other code + # because people may want to have it blocking to get an updated config before setting + # up the rest. Content of OpAMP config depends on the implementor and vendors will + # have their own. OpAMP to be fully integrated will need to introduce the concept of a + # config so we can track the difference between current config and a newly provided remote + # config. The goal is to have configuration updated dynamically while our SDK config is + # currently static. + _init_opamp = _import_opamp() + _init_opamp(resource=resource) + _init_tracing( exporters=span_exporters, id_generator=id_generator, From c53c4c3ba37d088fbc92e06eaa605e2c141256fd Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 19 Jun 2025 15:07:54 +0200 Subject: [PATCH 2/8] OpAMP is optional --- .../src/opentelemetry/sdk/_configuration/__init__.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 2de5d957e4..177bce834b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -618,8 +618,11 @@ def _initialize_components( # config so we can track the difference between current config and a newly provided remote # config. The goal is to have configuration updated dynamically while our SDK config is # currently static. - _init_opamp = _import_opamp() - _init_opamp(resource=resource) + try: + _init_opamp = _import_opamp() + _init_opamp(resource=resource) + except RuntimeError: + _logger.debug("No OpAMP init function found") _init_tracing( exporters=span_exporters, From 99730fd42ae06b1ffa2e0f62765a9a5ae84f646f Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 20 Mar 2026 12:19:40 +0100 Subject: [PATCH 3/8] Reword and add tests --- .../sdk/_configuration/__init__.py | 26 ++-------------- opentelemetry-sdk/tests/test_configurator.py | 30 +++++++++++++++++++ 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 177bce834b..e2b1d0b7b6 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -527,25 +527,9 @@ def _import_opamp() -> callable[[...], None]: # this in development, at the moment we are looking for a callable that takes # the resource and instantiate an OpAMP agent. # Since configuration is not specified every implementors may have its own. - # OpAMPAgent and OpAMPClient will be contributed once I finish writing tests :) - # def opamp_init(resource: Resource): - # endpoint = os.environ.get("OTEL_PYTHON_OPAMP_ENDPOINT") - # if endpoint: - # opamp_client = OpAMPClient( - # endpoint=endpoint, - # agent_identifying_attributes={ - # "service.name": resource.get("service.name"), - # "deployment.environment.name": resource.get("deployment.environment.name"), - # }, - # ) - # opamp_agent = OpAMPAgent( - # interval=30, - # handler=opamp_handler, # this is an handler that gets called to process each OpAMP message - # client=opamp_client, - # ) - # opamp_agent.start() + # Refer to opentelemetry-opamp-client package on how to setup the OpAMP agent. _, opamp_init_func = _import_config_components( - ["_init_func"], "_opentelemetry_opamp" + ["init_function"], "_opentelemetry_opamp" )[0] return opamp_init_func @@ -613,11 +597,7 @@ def _initialize_components( # This is different than other init helpers because setting up OpAMP requires distro # provided code as it's not strictly specified. We call OpAMP init before other code # because people may want to have it blocking to get an updated config before setting - # up the rest. Content of OpAMP config depends on the implementor and vendors will - # have their own. OpAMP to be fully integrated will need to introduce the concept of a - # config so we can track the difference between current config and a newly provided remote - # config. The goal is to have configuration updated dynamically while our SDK config is - # currently static. + # up the rest. try: _init_opamp = _import_opamp() _init_opamp(resource=resource) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 720469c71f..41f99d0789 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -1508,3 +1508,33 @@ def f(x): 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.entry_points") + @patch("opentelemetry.sdk._configuration.Resource") + def test_init_function_found(self, mock_resource, mock_entry_points): + init_function = mock.Mock() + mock_entry_points.configure_mock( + return_value=[IterEntryPoint("init_function", init_function)] + ) + + _initialize_components(id_generator=1) + + mock_entry_points.assert_has_calls( + [mock.call(group="_opentelemetry_opamp", name="init_function")] + ) + init_function.assert_called_once_with( + resource=mock_resource.create.return_value + ) + + @patch("opentelemetry.sdk._configuration.entry_points") + def test_init_function_not_found(self, mock_entry_points): + 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, + ) From c0269faa0a5eaffd2e103bf981fc63cb603a4f28 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Fri, 20 Mar 2026 12:41:16 +0100 Subject: [PATCH 4/8] Reword --- .../src/opentelemetry/sdk/_configuration/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index e2b1d0b7b6..c9bf478d60 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -597,7 +597,7 @@ def _initialize_components( # This is different than other init helpers because setting up OpAMP requires distro # provided code as it's not strictly specified. We call OpAMP init before other code # because people may want to have it blocking to get an updated config before setting - # up the rest. + # up the rest of the SDK. try: _init_opamp = _import_opamp() _init_opamp(resource=resource) From dd7cef03ac3a8e75e1424feddd8ce541038874f0 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 9 Apr 2026 09:51:21 +0200 Subject: [PATCH 5/8] Fix typing --- .../src/opentelemetry/sdk/_configuration/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index c9bf478d60..145b0d218d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -523,7 +523,7 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: raise RuntimeError(f"{id_generator_name} is not an IdGenerator") -def _import_opamp() -> callable[[...], None]: +def _import_opamp() -> Callable[[...], None]: # this in development, at the moment we are looking for a callable that takes # the resource and instantiate an OpAMP agent. # Since configuration is not specified every implementors may have its own. From 830fd1bf384d94f4d00906b4bdaaabcd1ae902ff Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 9 Apr 2026 11:18:43 +0200 Subject: [PATCH 6/8] Rework error handling after comments --- .../sdk/_configuration/__init__.py | 28 +++++++++++++------ opentelemetry-sdk/tests/test_configurator.py | 24 ++++++++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 145b0d218d..1057bd2c7d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -528,11 +528,25 @@ def _import_opamp() -> Callable[[...], None]: # the resource and instantiate an OpAMP agent. # Since configuration is not specified every implementors may have its own. # Refer to opentelemetry-opamp-client package on how to setup the OpAMP agent. - _, opamp_init_func = _import_config_components( - ["init_function"], "_opentelemetry_opamp" - )[0] + try: + entry_point = next( + iter( + entry_points( + group="_opentelemetry_opamp", name="init_function" + ) + ) + ) + return entry_point.load() + except StopIteration: + _logger.debug("No OpAMP init function found") + except AttributeError as exc: + _logger.warning( + "Failed to load OpAMP init function from entry point, %s: %s", + entry_point, + exc, + ) - return opamp_init_func + return None def _initialize_components( @@ -598,11 +612,9 @@ def _initialize_components( # provided code as it's not strictly specified. We call OpAMP init before other code # because people may want to have it blocking to get an updated config before setting # up the rest of the SDK. - try: - _init_opamp = _import_opamp() + _init_opamp = _import_opamp() + if _init_opamp is not None: _init_opamp(resource=resource) - except RuntimeError: - _logger.debug("No OpAMP init function found") _init_tracing( exporters=span_exporters, diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 41f99d0789..a31f588b9b 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -1528,6 +1528,30 @@ def test_init_function_found(self, mock_resource, mock_entry_points): resource=mock_resource.create.return_value ) + @patch("opentelemetry.sdk._configuration.entry_points") + def test_init_function_load_failure(self, mock_entry_points): + 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: "" + + with self.assertLogs(level="WARNING") as cm: + _initialize_components(id_generator=1) + + mock_entry_points.assert_has_calls( + [mock.call(group="_opentelemetry_opamp", name="init_function")] + ) + + self.assertIn( + "WARNING:opentelemetry.sdk._configuration:Failed to load OpAMP init function from entry point," + " : module 'foo' has no attribute 'OpampInit'", + cm.output, + ) + @patch("opentelemetry.sdk._configuration.entry_points") def test_init_function_not_found(self, mock_entry_points): mock_entry_points.configure_mock(return_value=[]) From 2298f14ae1698f2d916de5ef903569ec326cb28a Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 9 Apr 2026 11:30:27 +0200 Subject: [PATCH 7/8] Fix typecheck --- .../src/opentelemetry/sdk/_configuration/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 1057bd2c7d..4cb8a8c067 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -523,11 +523,12 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: raise RuntimeError(f"{id_generator_name} is not an IdGenerator") -def _import_opamp() -> Callable[[...], None]: +def _import_opamp() -> Callable[[Resource], None] | None: # this in development, at the moment we are looking for a callable that takes # the resource and instantiate an OpAMP agent. # Since configuration is not specified every implementors may have its own. # Refer to opentelemetry-opamp-client package on how to setup the OpAMP agent. + entry_point = None try: entry_point = next( iter( @@ -614,7 +615,7 @@ def _initialize_components( # up the rest of the SDK. _init_opamp = _import_opamp() if _init_opamp is not None: - _init_opamp(resource=resource) + _init_opamp(resource) _init_tracing( exporters=span_exporters, From f7e07b30641b08b7a0d242da3349a252ae42c006 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Thu, 9 Apr 2026 11:33:57 +0200 Subject: [PATCH 8/8] Fix tests --- opentelemetry-sdk/tests/test_configurator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index a31f588b9b..1935db48b1 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -1525,7 +1525,7 @@ def test_init_function_found(self, mock_resource, mock_entry_points): [mock.call(group="_opentelemetry_opamp", name="init_function")] ) init_function.assert_called_once_with( - resource=mock_resource.create.return_value + mock_resource.create.return_value ) @patch("opentelemetry.sdk._configuration.entry_points")