From cc828407d8a798518229e9fa480241b351045ab4 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 18:05:32 -0700 Subject: [PATCH 01/19] Add is-deprecated helper --- solarwinds_apm/apm_config.py | 46 +++++++++++++++---- ...pm_config_service_key_format_deprecated.py | 38 +++++++++++++++ 2 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index eee99dc31..a9105d21a 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -128,16 +128,23 @@ def __init__( # TODO Implement in-code config with kwargs after alpha # self.update_with_kwargs(kwargs) + self.legacy_service_key = False + if not self.is_lambda: + self.legacy_service_key = calculate_service_key_format_deprecated( + self.__config["service_key"] + ) + self.agent_enabled = self._calculate_agent_enabled() self.service_name = self._calculate_service_name( self.agent_enabled, otel_resource, ) - self.__config["service_key"] = self._update_service_key_name( - self.agent_enabled, - self.__config["service_key"], - self.service_name, - ) + if self.legacy_service_key: + self.__config["service_key"] = self._update_service_key_name( + self.agent_enabled, + self.__config["service_key"], + self.service_name, + ) # Create final APM resource with SolarWinds attributes # and calculated service name @@ -380,12 +387,13 @@ def _calculate_service_name_apm_proto( # 1. OTEL_SERVICE_NAME # 2. service.name in OTEL_RESOURCE_ATTRIBUTES # 3. service.name in OTel Resource set by any Resource Detectors - # 4. service name component of SW_APM_SERVICE_KEY + # 4. (deprecated) service name component of SW_APM_SERVICE_KEY # 5. empty string # # Note: 1-4 require that SW_APM_SERVICE_KEY exists and is in the correct - # format of ":". Otherwise agent_enabled: False - # and service.name is empty string. + # format of "". The legacy format of ":" + # is deprecated and will no longer be supported in a future release. + # Otherwise, agent_enabled: False and service.name is empty string. # # The passed in OTel Resource likely has a `service.name` already calculated # by merging OTEL_SERVICE_NAME / OTEL_RESOURCE_ATTRIBUTES with defaults. @@ -936,3 +944,25 @@ def to_configuration(cls, apm_config) -> Configuration: transaction_name=apm_config.get("transaction_name"), transaction_settings=transaction_settings, ) + + +def calculate_service_key_format_deprecated( + service_key: str, +) -> bool: + """Check whether a service key uses the deprecated token-and-name format. + + Parameters: + service_key (str): The configured service key. + + Returns: + bool: True if the service key uses the deprecated format, False otherwise. + """ + if service_key and ":" in service_key: + logger.warning( + "The service name component of SW_APM_SERVICE_KEY is deprecated " + "and will be removed in a future release. Set the service name " + "using OTEL_SERVICE_NAME or OTEL_RESOURCE_ATTRIBUTES instead." + ) + return True + + return False diff --git a/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py new file mode 100644 index 000000000..119bb9593 --- /dev/null +++ b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py @@ -0,0 +1,38 @@ +# © 2026 SolarWinds Worldwide, LLC. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +"""Tests for service-key format deprecation detection.""" + +import logging + +import pytest + +from solarwinds_apm.apm_config import calculate_service_key_format_deprecated + + +@pytest.mark.parametrize("service_key", ["a", "token", ""]) +def test_token_only_service_key_is_not_deprecated( + service_key: str, + caplog: pytest.LogCaptureFixture, +) -> None: + """Token-only and empty service keys do not emit deprecation warnings.""" + caplog.set_level(logging.WARNING, logger="solarwinds_apm.apm_config") + + assert calculate_service_key_format_deprecated(service_key) is False + assert not caplog.records + + +def test_legacy_service_key_is_deprecated_and_logs_warning( + caplog: pytest.LogCaptureFixture, +) -> None: + """Legacy token-and-name keys return true and log a warning.""" + caplog.set_level(logging.WARNING, logger="solarwinds_apm.apm_config") + + assert calculate_service_key_format_deprecated("token:service") is True + + assert len(caplog.records) == 1 + assert caplog.records[0].levelno == logging.WARNING + assert "SW_APM_SERVICE_KEY is deprecated" in caplog.records[0].message + assert "token" not in caplog.records[0].message \ No newline at end of file From 3a3307589cb36993b2c867b6cb95f756b7a9acda Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 18:14:37 -0700 Subject: [PATCH 02/19] agent_enabled if service_key present; no more format requirement --- solarwinds_apm/apm_config.py | 9 --------- tests/unit/test_apm_config/test_apm_config.py | 14 ++++++++------ .../test_apm_config_agent_enabled.py | 12 +++++++++--- ...est_apm_config_service_key_format_deprecated.py | 2 +- 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index a9105d21a..717bcdcd1 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -265,15 +265,6 @@ def _calculate_agent_enabled_config(self) -> bool: if not self.__config.get("service_key"): logger.error("Missing service key. Tracing disabled.") return False - # Key must be at least one char + ":" + at least one other char - key_parts = [ - p - for p in self.__config.get("service_key", "").split(":") - if len(p) > 0 - ] - if len(key_parts) != 2: - logger.error("Incorrect service key format. Tracing disabled.") - return False # (2) SW_APM_AGENT_ENABLED # At this point it might be false after reading env var or cnf file diff --git a/tests/unit/test_apm_config/test_apm_config.py b/tests/unit/test_apm_config/test_apm_config.py index 2d2472133..471f6f6bd 100644 --- a/tests/unit/test_apm_config/test_apm_config.py +++ b/tests/unit/test_apm_config/test_apm_config.py @@ -101,7 +101,7 @@ def _mock_service_key(self, mocker, service_key): }, ) - def test__init_invalid_service_key_format(self, mocker): + def test__init_token_only_service_key_format(self, mocker, mock_env_vars): mocker.patch.dict( os.environ, { @@ -109,12 +109,14 @@ def test__init_invalid_service_key_format(self, mocker): }, ) test_config = apm_config.SolarWindsApmConfig() - assert not test_config.agent_enabled + assert test_config.agent_enabled assert test_config.service_name == "" assert test_config.get("service_key") == "incorrect_format" - def test__init_invalid_service_key_format_otel_service_name_and_resource_attrs( - self, mocker + def test__init_token_only_service_key_format_otel_service_name_and_resource_attrs( + self, + mocker, + mock_env_vars, ): mocker.patch.dict( os.environ, @@ -125,8 +127,8 @@ def test__init_invalid_service_key_format_otel_service_name_and_resource_attrs( }, ) test_config = apm_config.SolarWindsApmConfig() - assert not test_config.agent_enabled - assert test_config.service_name == "" + assert test_config.agent_enabled + assert test_config.service_name == "wont_be_used" assert test_config.get("service_key") == "incorrect_format" def test__init_valid_service_key_format_agent_enabled_false(self, mocker): diff --git a/tests/unit/test_apm_config/test_apm_config_agent_enabled.py b/tests/unit/test_apm_config/test_apm_config_agent_enabled.py index d38243b01..ffaf080d9 100644 --- a/tests/unit/test_apm_config/test_apm_config_agent_enabled.py +++ b/tests/unit/test_apm_config/test_apm_config_agent_enabled.py @@ -135,11 +135,17 @@ def test_calculate_agent_enabled_service_key_env_var_not_set_cnf_file_used( if old_service_key: os.environ["SW_APM_SERVICE_KEY"] = old_service_key - def test_calculate_agent_enabled_service_key_bad_format(self, mocker): + @pytest.mark.parametrize("service_key", ["x", "invalidkey"]) + def test_calculate_agent_enabled_token_only_service_key( + self, + mocker, + mock_env_vars, + service_key, + ): mocker.patch.dict( os.environ, { - "SW_APM_SERVICE_KEY": "invalidkey", + "SW_APM_SERVICE_KEY": service_key, "SW_APM_AGENT_ENABLED": "true", }, ) @@ -153,7 +159,7 @@ def test_calculate_agent_enabled_service_key_bad_format(self, mocker): } ) resulting_config = apm_config.SolarWindsApmConfig() - assert not resulting_config._calculate_agent_enabled() + assert resulting_config._calculate_agent_enabled() assert resulting_config.service_name == "" def test_calculate_agent_enabled_service_key_ok( diff --git a/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py index 119bb9593..f3d98b9e5 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py +++ b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py @@ -35,4 +35,4 @@ def test_legacy_service_key_is_deprecated_and_logs_warning( assert len(caplog.records) == 1 assert caplog.records[0].levelno == logging.WARNING assert "SW_APM_SERVICE_KEY is deprecated" in caplog.records[0].message - assert "token" not in caplog.records[0].message \ No newline at end of file + assert "token" not in caplog.records[0].message From 6602a89cd8ddca4ef873e0555b97813d30552215 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 18:24:38 -0700 Subject: [PATCH 03/19] Update _calculate_service_name_apm_proto --- solarwinds_apm/apm_config.py | 20 ++++++--------- .../test_apm_config_service_name.py | 25 +++++++++++++++++-- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index 717bcdcd1..b412e4f23 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -404,19 +404,13 @@ def _calculate_service_name_apm_proto( if otel_service_name and otel_service_name.startswith( "unknown_service" ): - # When agent_enabled, assume service_key exists and is formatted correctly. - # This is a precaution. Validate defensively instead of relying on - # exception handling so unexpected non-string values cannot crash - # the instrumented application. - service_key = self.__config.get("service_key", ":") - if isinstance(service_key, str): - service_key_parts = service_key.split(":", 1) - if len(service_key_parts) > 1: - service_name = service_key_parts[1] - else: - service_name = "" - else: - service_name = "" + # When agent_enabled, assume service_key exists. + # This only sets service_name if service_key contains a colon + service_key = self.__config.get("service_key") + if isinstance(service_key, str) and service_key: + _, separator, key_service_name = service_key.partition(":") + if separator: + service_name = key_service_name else: service_name = otel_service_name return service_name diff --git a/tests/unit/test_apm_config/test_apm_config_service_name.py b/tests/unit/test_apm_config/test_apm_config_service_name.py index 0dc1616f9..bda033f93 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_name.py +++ b/tests/unit/test_apm_config/test_apm_config_service_name.py @@ -134,23 +134,44 @@ def test__calculate_service_name_apm_proto_use_otel_service_name( ) assert result == "foobar" - def test__calculate_service_name_apm_proto_malformed_service_key_only_token( + def test__calculate_service_name_apm_proto_token_only_service_key( self, mocker, + mock_env_vars, ): mocker.patch.dict( os.environ, { - "SW_APM_SERVICE_KEY": "token:", + "SW_APM_SERVICE_KEY": "x", }, ) test_config = apm_config.SolarWindsApmConfig() + assert test_config.agent_enabled result = test_config._calculate_service_name_apm_proto( True, Resource.create(), # default is unknown_service ) assert result == "" + def test__calculate_service_name_apm_proto_token_only_preserves_otel_name( + self, + mocker, + mock_env_vars, + ): + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "x", + }, + ) + test_config = apm_config.SolarWindsApmConfig() + result = test_config._calculate_service_name_apm_proto( + True, + Resource.create({"service.name": "otel-service"}), + ) + assert test_config.agent_enabled + assert result == "otel-service" + def test__calculate_service_name_apm_proto_non_string_service_key( self, mocker, From b77f647c14ddbed042e0b603c6cac4baeea44819 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 18:29:01 -0700 Subject: [PATCH 04/19] Update to_configuration --- solarwinds_apm/apm_config.py | 10 ++++++---- tests/unit/test_apm_config/test_apm_config.py | 6 +++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index b412e4f23..d9ff65218 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -905,10 +905,12 @@ def to_configuration(cls, apm_config) -> Configuration: Returns: Configuration: Configuration object for sampler initialization. """ - try: - token = apm_config.get("service_key").split(":")[0] - except (AttributeError, IndexError): - token = "" + service_key = apm_config.get("service_key") + token = ( + service_key.partition(":")[0] + if isinstance(service_key, str) + else "" + ) filters = apm_config.get("transaction_filters") transaction_settings = [] for transaction_filter in filters: diff --git a/tests/unit/test_apm_config/test_apm_config.py b/tests/unit/test_apm_config/test_apm_config.py index 471f6f6bd..1de2baec0 100644 --- a/tests/unit/test_apm_config/test_apm_config.py +++ b/tests/unit/test_apm_config/test_apm_config.py @@ -892,10 +892,10 @@ def test_to_configuration_with_multiple_transaction_filters(apm): assert config.transaction_settings[1].tracing is False -def test_to_configuration_with_invalid_service_key(apm): - apm._set_config_value("service_key", "invalid_format") +def test_to_configuration_with_token_only_service_key(apm): + apm._set_config_value("service_key", "x") config = apm_config.SolarWindsApmConfig.to_configuration(apm_config=apm) - assert config.headers["Authorization"] == "Bearer invalid_format" + assert config.headers["Authorization"] == "Bearer x" def test_to_configuration_with_empty_service_key(apm): From 0e38f4723104ab2fa68057adf6921d5444ab685d Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 18:34:19 -0700 Subject: [PATCH 05/19] UPdate mask_service_key --- solarwinds_apm/apm_config.py | 17 ++++++------- tests/unit/test_apm_config/test_apm_config.py | 24 +++++-------------- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index d9ff65218..39989ff14 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -85,8 +85,7 @@ class SolarWindsApmConfig: _CONFIG_FILE_DEFAULT = "./solarwinds-apm-config.json" _DELIMITER = "." _KEY_MASK = "{}...{}:{}" - _KEY_MASK_BAD_FORMAT = "{}..." - _KEY_MASK_BAD_FORMAT_SHORT = "{}" + _KEY_MASK_TOKEN_ONLY = "{}...{}" _SW_PREFIX = "sw_apm_" _logged_no_config_file = False @@ -488,20 +487,18 @@ def mask_service_key(self) -> str: try: if service_key.strip() == "": return service_key - key_parts = service_key.split(":") + key_parts = service_key.split(":", 1) except AttributeError: return "" - if len(key_parts) < 2: - bad_format_key = key_parts[0] if key_parts else "" - if len(bad_format_key) < 5: - return self._KEY_MASK_BAD_FORMAT_SHORT.format(bad_format_key) - return self._KEY_MASK_BAD_FORMAT.format( - bad_format_key[0:4], - ) api_token = key_parts[0] if len(api_token) < 9: return service_key + if len(key_parts) == 1: + return self._KEY_MASK_TOKEN_ONLY.format( + api_token[0:4], + api_token[-4:], + ) return self._KEY_MASK.format( api_token[0:4], api_token[-4:], diff --git a/tests/unit/test_apm_config/test_apm_config.py b/tests/unit/test_apm_config/test_apm_config.py index 1de2baec0..cf55cb4bc 100644 --- a/tests/unit/test_apm_config/test_apm_config.py +++ b/tests/unit/test_apm_config/test_apm_config.py @@ -336,31 +336,19 @@ def test_mask_service_key_whitespace_key(self, mocker): self._mock_service_key(mocker, " ") assert apm_config.SolarWindsApmConfig().mask_service_key() == " " - def test_mask_service_key_invalid_format_no_colon(self, mocker): + def test_mask_service_key_token_only_format(self, mocker): self._mock_service_key(mocker, "a") - assert ( - apm_config.SolarWindsApmConfig().mask_service_key() - == "a" - ) + assert apm_config.SolarWindsApmConfig().mask_service_key() == "a" self._mock_service_key(mocker, "abcd") - assert ( - apm_config.SolarWindsApmConfig().mask_service_key() - == "abcd" - ) - self._mock_service_key(mocker, "abcde") - assert ( - apm_config.SolarWindsApmConfig().mask_service_key() - == "abcd..." - ) + assert apm_config.SolarWindsApmConfig().mask_service_key() == "abcd" self._mock_service_key(mocker, "abcdefgh") assert ( - apm_config.SolarWindsApmConfig().mask_service_key() - == "abcd..." + apm_config.SolarWindsApmConfig().mask_service_key() == "abcdefgh" ) self._mock_service_key(mocker, "abcd1efgh") assert ( apm_config.SolarWindsApmConfig().mask_service_key() - == "abcd..." + == "abcd...efgh" ) self._mock_service_key( mocker, @@ -368,7 +356,7 @@ def test_mask_service_key_invalid_format_no_colon(self, mocker): ) assert ( apm_config.SolarWindsApmConfig().mask_service_key() - == "CyUu..." + == "CyUu...HK6I" ) def test_mask_service_key_less_than_9_char_token( From 62f131d69e58fddb0d079003e9ae6a2bb0c840d5 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 18:43:05 -0700 Subject: [PATCH 06/19] Update distro headers from service_key --- solarwinds_apm/distro.py | 11 +++-------- tests/unit/test_distro.py | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/solarwinds_apm/distro.py b/solarwinds_apm/distro.py index 637f8ed43..f1535c8d5 100644 --- a/solarwinds_apm/distro.py +++ b/solarwinds_apm/distro.py @@ -114,10 +114,10 @@ def _log_runtime(self) -> None: def _get_token_from_service_key(self) -> str | None: """ - Extract token portion from service_key environment variable. + Extract the token from a legacy or token-only service key. Returns: - str | None: The API token if service_key is valid, None otherwise. + str | None: The API token if service_key is present, None otherwise. """ service_key = environ.get("SW_APM_SERVICE_KEY") if not service_key: @@ -125,16 +125,11 @@ def _get_token_from_service_key(self) -> str | None: if not self._is_lambda: logger.debug("Missing service key") return None - # Key must be at least one char + ":" + at least one other char try: - key_parts = [p for p in service_key.split(":") if len(p) > 0] + return service_key.partition(":")[0] except AttributeError: logger.debug("Invalid service key format") return None - if len(key_parts) != 2: - logger.debug("Incorrect service key format") - return None - return key_parts[0] def _configure(self, **kwargs: dict) -> None: """ diff --git a/tests/unit/test_distro.py b/tests/unit/test_distro.py index 5ccd0b79f..16c08258c 100644 --- a/tests/unit/test_distro.py +++ b/tests/unit/test_distro.py @@ -132,11 +132,9 @@ def test__get_token_from_service_key_missing(self, mocker): os.environ.pop("SW_APM_SERVICE_KEY", None) assert distro.SolarWindsDistro()._get_token_from_service_key() is None - def test__get_token_from_service_key_bad_format(self, mocker): - mocker.patch.dict( - os.environ, {"SW_APM_SERVICE_KEY": "missing-service-name"} - ) - assert distro.SolarWindsDistro()._get_token_from_service_key() is None + def test__get_token_from_service_key_token_only(self, mocker): + mocker.patch.dict(os.environ, {"SW_APM_SERVICE_KEY": "x"}) + assert distro.SolarWindsDistro()._get_token_from_service_key() == "x" def test__get_token_from_service_key_ok(self, mocker): mocker.patch.dict( @@ -648,6 +646,36 @@ def test_configure_env_headers_otel_endpoint_default(self, mocker): == "authorization=Bearer%20foo-token" ) + def test_configure_env_headers_legacy_service_key(self, mocker): + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "foo-token:bar-name", + OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.collector.na-01.cloud.solarwinds.com:443", + }, + clear=True, + ) + distro.SolarWindsDistro()._configure() + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) + + def test_configure_env_headers_token_only_service_key(self, mocker): + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "foo-token", + OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.collector.na-01.cloud.solarwinds.com:443", + }, + clear=True, + ) + distro.SolarWindsDistro()._configure() + assert ( + os.environ[OTEL_EXPORTER_OTLP_HEADERS] + == "authorization=Bearer%20foo-token" + ) + def test_configure_env_headers_otel_endpoint_resolved(self, mocker): mocker.patch.dict( os.environ, From b3dff47744767a4a09d6c8aa7dc7f2dfcee1d5f9 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 19:06:11 -0700 Subject: [PATCH 07/19] Safer lambda fallback? --- solarwinds_apm/apm_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index 39989ff14..442752804 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -352,7 +352,8 @@ def _calculate_service_name_lambda( if not otel_service_name: return self.lambda_function_name - if otel_service_name == "unknown_service": + # e.g. unknown_service:python + if otel_service_name.startswith("unknown_service"): # OTEL_SERVICE_NAME was not set # and exec wrapper did not wrap instrumentation return self.lambda_function_name From 3b7ad67cbd69d53d4066ca67547b0d4230ab00d1 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 19:11:58 -0700 Subject: [PATCH 08/19] Add HttpSampler token-only test --- tests/integration/test_service_name.py | 4 ++ tests/unit/test_oboe/test_http_sampler.py | 76 +++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/tests/integration/test_service_name.py b/tests/integration/test_service_name.py index f1983b950..8ec2fcc3f 100644 --- a/tests/integration/test_service_name.py +++ b/tests/integration/test_service_name.py @@ -212,6 +212,10 @@ def test_sw_key_fallback(self): assert isinstance(resource_attrs["host.name"], str) +# class TestServiceNamePrecedence4TokenOnlySwKeyNotUsed(TestBaseSwHeadersAndAttributes): +# # TODO - use otel default "unknown_service" because service_key is only a token + + class TestServiceNameAzureDetectorFullEnvironment( TestBaseSwHeadersAndAttributes ): diff --git a/tests/unit/test_oboe/test_http_sampler.py b/tests/unit/test_oboe/test_http_sampler.py index 236f45460..f9408b95b 100644 --- a/tests/unit/test_oboe/test_http_sampler.py +++ b/tests/unit/test_oboe/test_http_sampler.py @@ -6,12 +6,14 @@ import json import os import socket +import time from unittest.mock import MagicMock, patch import pytest from opentelemetry import trace from opentelemetry.sdk.metrics import AlwaysOnExemplarFilter, MeterProvider from opentelemetry.sdk.metrics._internal.export import InMemoryMetricReader +from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import SimpleSpanProcessor from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( @@ -70,6 +72,80 @@ def test_valid_service_key_samples_created_spans(): assert "BucketRate" in spans[0].attributes +@patch("requests.get") +def test_valid_token_only_service_key_samples_created_spans( + mock_get, + mocker, +): + mocker.patch.dict( + os.environ, + { + "SW_APM_SERVICE_KEY": "token-only-test-key", + "OTEL_SERVICE_NAME": "token-only-service", + }, + clear=True, + ) + mock_response = MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = { + "value": 1000000, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "timestamp": int(time.time()), + "ttl": 120, + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "TriggerRelaxedBucketCapacity": 20, + "TriggerRelaxedBucketRate": 1, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 0.1, + "SignatureKey": "signature", + }, + } + mock_get.return_value = mock_response + + service_key = os.environ["SW_APM_SERVICE_KEY"] + service_name = Resource.create().attributes["service.name"] + meter_provider = MeterProvider( + metric_readers=[InMemoryMetricReader()], + exemplar_filter=AlwaysOnExemplarFilter(), + ) + sampler = HttpSampler( + meter_provider=meter_provider, + config=Configuration( + collector="https://apm.collector.na-01.cloud.solarwinds.com", + service=service_name, + headers={"Authorization": f"Bearer {service_key}"}, + enabled=True, + trigger_trace_enabled=True, + tracing_mode=None, + transaction_settings=[], + transaction_name=None, + ), + initial=None, + ) + memory_exporter = InMemorySpanExporter() + tracer_provider = TracerProvider(sampler=sampler) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer("test", tracer_provider=tracer_provider) + sampler.wait_until_ready(1) + with tracer.start_as_current_span("test") as span: + assert span.is_recording() + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + assert "SampleRate" in spans[0].attributes + assert "SampleSource" in spans[0].attributes + assert "BucketCapacity" in spans[0].attributes + assert "BucketRate" in spans[0].attributes + mock_get.assert_any_call( + f"https://apm.collector.na-01.cloud.solarwinds.com/v1/settings/{service_name}/{socket.gethostname()}", + headers={"Authorization": "Bearer token-only-test-key"}, + timeout=10, + ) + + def test_invalid_service_key_does_not_sample_created_spans(): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()], From 8f0c7f30d827f7fd303e04f20436ebee31394182 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 19:30:55 -0700 Subject: [PATCH 09/19] Test better for default otel service name --- solarwinds_apm/apm_config.py | 5 +++-- tests/unit/test_apm_config/test_apm_config.py | 5 ++++- tests/unit/test_apm_config/test_apm_config_agent_enabled.py | 6 +++++- tests/unit/test_apm_config/test_apm_config_service_name.py | 5 +++-- 4 files changed, 15 insertions(+), 6 deletions(-) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index 442752804..7f938b7b2 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -409,8 +409,9 @@ def _calculate_service_name_apm_proto( service_key = self.__config.get("service_key") if isinstance(service_key, str) and service_key: _, separator, key_service_name = service_key.partition(":") - if separator: - service_name = key_service_name + service_name = ( + key_service_name if separator else otel_service_name + ) else: service_name = otel_service_name return service_name diff --git a/tests/unit/test_apm_config/test_apm_config.py b/tests/unit/test_apm_config/test_apm_config.py index cf55cb4bc..f98a4a0c1 100644 --- a/tests/unit/test_apm_config/test_apm_config.py +++ b/tests/unit/test_apm_config/test_apm_config.py @@ -110,7 +110,10 @@ def test__init_token_only_service_key_format(self, mocker, mock_env_vars): ) test_config = apm_config.SolarWindsApmConfig() assert test_config.agent_enabled - assert test_config.service_name == "" + assert ( + test_config.service_name + == Resource.create().attributes["service.name"] + ) assert test_config.get("service_key") == "incorrect_format" def test__init_token_only_service_key_format_otel_service_name_and_resource_attrs( diff --git a/tests/unit/test_apm_config/test_apm_config_agent_enabled.py b/tests/unit/test_apm_config/test_apm_config_agent_enabled.py index ffaf080d9..ddb39f9c0 100644 --- a/tests/unit/test_apm_config/test_apm_config_agent_enabled.py +++ b/tests/unit/test_apm_config/test_apm_config_agent_enabled.py @@ -8,6 +8,7 @@ import os import pytest +from opentelemetry.sdk.resources import Resource from solarwinds_apm import apm_config @@ -160,7 +161,10 @@ def test_calculate_agent_enabled_token_only_service_key( ) resulting_config = apm_config.SolarWindsApmConfig() assert resulting_config._calculate_agent_enabled() - assert resulting_config.service_name == "" + assert ( + resulting_config.service_name + == Resource.create().attributes["service.name"] + ) def test_calculate_agent_enabled_service_key_ok( self, diff --git a/tests/unit/test_apm_config/test_apm_config_service_name.py b/tests/unit/test_apm_config/test_apm_config_service_name.py index bda033f93..9831b638e 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_name.py +++ b/tests/unit/test_apm_config/test_apm_config_service_name.py @@ -147,11 +147,12 @@ def test__calculate_service_name_apm_proto_token_only_service_key( ) test_config = apm_config.SolarWindsApmConfig() assert test_config.agent_enabled + otel_resource = Resource.create() # default is unknown_service result = test_config._calculate_service_name_apm_proto( True, - Resource.create(), # default is unknown_service + otel_resource, ) - assert result == "" + assert result == otel_resource.attributes["service.name"] def test__calculate_service_name_apm_proto_token_only_preserves_otel_name( self, From 80b4b5f16f60bd816dbf3a3a707c005d5e45de21 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 14 Oct 2026 19:32:48 -0700 Subject: [PATCH 10/19] Add integration tests TokenOnlySwKeyNotUsed --- .../integration/test_base_sw_headers_attrs.py | 3 +- tests/integration/test_service_name.py | 56 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_base_sw_headers_attrs.py b/tests/integration/test_base_sw_headers_attrs.py index 57eb177ce..bfd129904 100644 --- a/tests/integration/test_base_sw_headers_attrs.py +++ b/tests/integration/test_base_sw_headers_attrs.py @@ -49,6 +49,7 @@ class TestBaseSwHeadersAndAttributes(TestBase): "SampleRate", "SampleSource", ] + SERVICE_KEY = "foo:bar" @staticmethod def assert_valid_trace_flags(trace_flags: str): @@ -105,7 +106,7 @@ def setUp(self): argument_otel_environment_variable[argument] = attribute # Set APM service key - not valid, but we mock anyway - os.environ["SW_APM_SERVICE_KEY"] = "foo:bar" + os.environ["SW_APM_SERVICE_KEY"] = self.SERVICE_KEY # Load Distro SolarWindsDistro().configure() diff --git a/tests/integration/test_service_name.py b/tests/integration/test_service_name.py index 8ec2fcc3f..4a6dcd2ff 100644 --- a/tests/integration/test_service_name.py +++ b/tests/integration/test_service_name.py @@ -212,8 +212,60 @@ def test_sw_key_fallback(self): assert isinstance(resource_attrs["host.name"], str) -# class TestServiceNamePrecedence4TokenOnlySwKeyNotUsed(TestBaseSwHeadersAndAttributes): -# # TODO - use otel default "unknown_service" because service_key is only a token +class TestServiceNamePrecedence4TokenOnlySwKeyNotUsed( + TestBaseSwHeadersAndAttributes +): + SERVICE_KEY = "token-only-test-key" + + def setUp(self): + self._otel_service_name = os.environ.pop("OTEL_SERVICE_NAME", None) + self._otel_resource_attributes = os.environ.pop( + "OTEL_RESOURCE_ATTRIBUTES", None + ) + super().setUp() + + def tearDown(self): + super().tearDown() + if self._otel_service_name is not None: + os.environ["OTEL_SERVICE_NAME"] = self._otel_service_name + if self._otel_resource_attributes is not None: + os.environ["OTEL_RESOURCE_ATTRIBUTES"] = ( + self._otel_resource_attributes + ) + + def test_token_only_key_does_not_set_service_name(self): + timestamp = int(time.time()) + with mock.patch( + target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", + return_value=[ + { + "arguments": { + "BucketCapacity": 2, + "BucketRate": 1, + "MetricsFlushInterval": 60, + "SignatureKey": "", + "TriggerRelaxedBucketCapacity": 4, + "TriggerRelaxedBucketRate": 3, + "TriggerStrictBucketCapacity": 6, + "TriggerStrictBucketRate": 5, + }, + "flags": "SAMPLE_START,SAMPLE_THROUGH_ALWAYS,SAMPLE_BUCKET_ENABLED,TRIGGER_TRACE", + "layer": "", + "timestamp": timestamp, + "ttl": 120, + "type": 0, + "value": 1000000, + } + ], + ): + resp = self.client.get("/test_trace/") + assert resp.status_code == 200 + spans = self.memory_exporter.get_finished_spans() + assert len(spans) > 0 + + service_name = spans[0].resource.attributes["service.name"] + assert service_name.startswith("unknown_service") + assert service_name != self.SERVICE_KEY class TestServiceNameAzureDetectorFullEnvironment( From 1dc62a6b7fc7aea5f6f164774ab7043859d28674 Mon Sep 17 00:00:00 2001 From: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:21:36 -0700 Subject: [PATCH 11/19] Refactor span exporter and tracer setup in tests Refactor test setup for span exporter and tracer provider, ensuring proper resource cleanup with a try-finally block. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/unit/test_oboe/test_http_sampler.py | 43 ++++++++++++----------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/tests/unit/test_oboe/test_http_sampler.py b/tests/unit/test_oboe/test_http_sampler.py index f9408b95b..561bffcfe 100644 --- a/tests/unit/test_oboe/test_http_sampler.py +++ b/tests/unit/test_oboe/test_http_sampler.py @@ -124,26 +124,29 @@ def test_valid_token_only_service_key_samples_created_spans( ), initial=None, ) - memory_exporter = InMemorySpanExporter() - tracer_provider = TracerProvider(sampler=sampler) - tracer_provider.add_span_processor( - span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) - ) - tracer = trace.get_tracer("test", tracer_provider=tracer_provider) - sampler.wait_until_ready(1) - with tracer.start_as_current_span("test") as span: - assert span.is_recording() - spans = memory_exporter.get_finished_spans() - assert len(spans) == 1 - assert "SampleRate" in spans[0].attributes - assert "SampleSource" in spans[0].attributes - assert "BucketCapacity" in spans[0].attributes - assert "BucketRate" in spans[0].attributes - mock_get.assert_any_call( - f"https://apm.collector.na-01.cloud.solarwinds.com/v1/settings/{service_name}/{socket.gethostname()}", - headers={"Authorization": "Bearer token-only-test-key"}, - timeout=10, - ) + try: + memory_exporter = InMemorySpanExporter() + tracer_provider = TracerProvider(sampler=sampler) + tracer_provider.add_span_processor( + span_processor=SimpleSpanProcessor(span_exporter=memory_exporter) + ) + tracer = trace.get_tracer("test", tracer_provider=tracer_provider) + sampler.wait_until_ready(1) + with tracer.start_as_current_span("test") as span: + assert span.is_recording() + spans = memory_exporter.get_finished_spans() + assert len(spans) == 1 + assert "SampleRate" in spans[0].attributes + assert "SampleSource" in spans[0].attributes + assert "BucketCapacity" in spans[0].attributes + assert "BucketRate" in spans[0].attributes + mock_get.assert_any_call( + f"https://apm.collector.na-01.cloud.solarwinds.com/v1/settings/{service_name}/{socket.gethostname()}", + headers={"Authorization": "Bearer token-only-test-key"}, + timeout=10, + ) + finally: + sampler.shutdown() def test_invalid_service_key_does_not_sample_created_spans(): From fd89b06570670295ce45eccf08431b28be9f1565 Mon Sep 17 00:00:00 2001 From: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:22:22 -0700 Subject: [PATCH 12/19] Test legacy service key deprecation warning Add logging warning for deprecated legacy service key format. Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../test_apm_config_service_key_format_deprecated.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py index f3d98b9e5..0b563424b 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py +++ b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py @@ -26,8 +26,12 @@ def test_token_only_service_key_is_not_deprecated( def test_legacy_service_key_is_deprecated_and_logs_warning( caplog: pytest.LogCaptureFixture, + monkeypatch: pytest.MonkeyPatch, ) -> None: """Legacy token-and-name keys return true and log a warning.""" + monkeypatch.setattr( + logging.getLogger("solarwinds_apm"), "propagate", True + ) caplog.set_level(logging.WARNING, logger="solarwinds_apm.apm_config") assert calculate_service_key_format_deprecated("token:service") is True From 1b729e811fe3ebf4cfcca6f5f3d51861c083af33 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 15 Oct 2026 10:22:43 -0700 Subject: [PATCH 13/19] ruff --- .../test_apm_config_service_key_format_deprecated.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py index 0b563424b..0ca7079dc 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py +++ b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py @@ -29,9 +29,7 @@ def test_legacy_service_key_is_deprecated_and_logs_warning( monkeypatch: pytest.MonkeyPatch, ) -> None: """Legacy token-and-name keys return true and log a warning.""" - monkeypatch.setattr( - logging.getLogger("solarwinds_apm"), "propagate", True - ) + monkeypatch.setattr(logging.getLogger("solarwinds_apm"), "propagate", True) caplog.set_level(logging.WARNING, logger="solarwinds_apm.apm_config") assert calculate_service_key_format_deprecated("token:service") is True From 14f92e9b63abda0c1a3259a868b5b5dce4a6a69c Mon Sep 17 00:00:00 2001 From: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com> Date: Thu, 15 Oct 2026 10:31:09 -0700 Subject: [PATCH 14/19] Update service name check for unknown_service Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- solarwinds_apm/apm_config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index 7f938b7b2..898acd66c 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -353,7 +353,9 @@ def _calculate_service_name_lambda( return self.lambda_function_name # e.g. unknown_service:python - if otel_service_name.startswith("unknown_service"): + if otel_service_name == "unknown_service" or otel_service_name.startswith( + "unknown_service:" + ): # OTEL_SERVICE_NAME was not set # and exec wrapper did not wrap instrumentation return self.lambda_function_name From d9f9d69a78fdd0cbe4de01886f97a2193bacde9c Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 15 Oct 2026 10:31:24 -0700 Subject: [PATCH 15/19] ruff --- solarwinds_apm/apm_config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index 898acd66c..880f184b6 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -353,8 +353,9 @@ def _calculate_service_name_lambda( return self.lambda_function_name # e.g. unknown_service:python - if otel_service_name == "unknown_service" or otel_service_name.startswith( - "unknown_service:" + if ( + otel_service_name == "unknown_service" + or otel_service_name.startswith("unknown_service:") ): # OTEL_SERVICE_NAME was not set # and exec wrapper did not wrap instrumentation From 7f3b09fa13bb9567aae0c8699eb0dd7092542908 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 15 Oct 2026 12:14:33 -0700 Subject: [PATCH 16/19] add check_service_name_migration --- solarwinds_apm/apm_config.py | 22 +++++++++ .../test_apm_config_service_name.py | 47 +++++++++++++++++++ 2 files changed, 69 insertions(+) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index 880f184b6..6ea7575b4 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -144,6 +144,8 @@ def __init__( self.__config["service_key"], self.service_name, ) + else: + _ = self.check_service_name_migration() # Create final APM resource with SolarWinds attributes # and calculated service name @@ -443,6 +445,26 @@ def _calculate_service_name( otel_resource, ) + def check_service_name_migration(self) -> bool: + """Warn when the OTel default service name is still in use. + + Returns: + bool: True if the resolved name uses the OTel unknown default. + """ + if not isinstance( + self.service_name, str + ) or not self.service_name.startswith("unknown_service"): + return False + + logger.warning( + "APM is using the service name %s because neither " + "OTEL_SERVICE_NAME nor service.name in " + "OTEL_RESOURCE_ATTRIBUTES were set. Configure one to set " + "the service name.", + self.service_name, + ) + return True + def _update_service_key_name( self, agent_enabled: bool, diff --git a/tests/unit/test_apm_config/test_apm_config_service_name.py b/tests/unit/test_apm_config/test_apm_config_service_name.py index 9831b638e..23f18a0be 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_name.py +++ b/tests/unit/test_apm_config/test_apm_config_service_name.py @@ -4,6 +4,7 @@ # # Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +import logging import os from opentelemetry.sdk.resources import Resource @@ -75,6 +76,52 @@ def test__calculate_service_name_not_is_lambda(self, mocker): mock_calc_lambda.assert_not_called() +class TestSolarWindsApmConfigServiceNameMigration: + def test_unknown_default_service_name_warns( + self, mocker, caplog, mock_env_vars + ): + mocker.patch.dict( + os.environ, + {"SW_APM_SERVICE_KEY": "token-only"}, + ) + caplog.set_level( + logging.WARNING, + logger="solarwinds_apm.apm_config", + ) + test_config = apm_config.SolarWindsApmConfig( + Resource({"service.name": "unknown_service:python"}) + ) + + assert test_config.check_service_name_migration() is True + assert any( + "APM is using the service name unknown_service:python" + in record.message + for record in caplog.records + ) + + def test_configured_service_name_does_not_warn( + self, + mocker, + caplog, + mock_env_vars, + ): + mocker.patch.dict( + os.environ, + {"SW_APM_SERVICE_KEY": "token-only"}, + ) + caplog.set_level( + logging.WARNING, + logger="solarwinds_apm.apm_config", + ) + test_config = apm_config.SolarWindsApmConfig( + Resource({"service.name": "configured-service"}) + ) + caplog.clear() + + assert test_config.check_service_name_migration() is False + assert not caplog.records + + class TestSolarWindsApmConfigServiceNameApmProto: def test__calculate_service_name_apm_proto_agent_disabled(self): test_config = apm_config.SolarWindsApmConfig() From d5fa8d092b5a393f2a7d03716d2f9fef67e8072b Mon Sep 17 00:00:00 2001 From: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:21:11 -0700 Subject: [PATCH 17/19] Patch logging propagation in APM config tests Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tests/unit/test_apm_config/test_apm_config_service_name.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_apm_config/test_apm_config_service_name.py b/tests/unit/test_apm_config/test_apm_config_service_name.py index 23f18a0be..ec4274a04 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_name.py +++ b/tests/unit/test_apm_config/test_apm_config_service_name.py @@ -84,6 +84,9 @@ def test_unknown_default_service_name_warns( os.environ, {"SW_APM_SERVICE_KEY": "token-only"}, ) + mocker.patch.object( + logging.getLogger("solarwinds_apm"), "propagate", True + ) caplog.set_level( logging.WARNING, logger="solarwinds_apm.apm_config", From f85156574943554f19dbc7c15bf677d6042a6f8c Mon Sep 17 00:00:00 2001 From: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:23:51 -0700 Subject: [PATCH 18/19] Refactor service_name assignment logic Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- solarwinds_apm/apm_config.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index 6ea7575b4..9c2bd90be 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -413,10 +413,8 @@ def _calculate_service_name_apm_proto( # This only sets service_name if service_key contains a colon service_key = self.__config.get("service_key") if isinstance(service_key, str) and service_key: - _, separator, key_service_name = service_key.partition(":") - service_name = ( - key_service_name if separator else otel_service_name - ) + key_service_name = service_key.partition(":")[2] + service_name = key_service_name or otel_service_name else: service_name = otel_service_name return service_name From 70c452b4e07aadc5c065e13619a70334a677744f Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Mon, 19 Oct 2026 11:44:42 -0700 Subject: [PATCH 19/19] Fix header --- .../test_apm_config_service_key_format_deprecated.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py index 0ca7079dc..600b522a8 100644 --- a/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py +++ b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py @@ -1,8 +1,5 @@ -# © 2026 SolarWinds Worldwide, LLC. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at:http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +# Copyright SolarWinds Worldwide, LLC. +# SPDX-License-Identifier: Apache-2.0 """Tests for service-key format deprecation detection.""" import logging