diff --git a/solarwinds_apm/apm_config.py b/solarwinds_apm/apm_config.py index 37d656a7d..2188e3643 100644 --- a/solarwinds_apm/apm_config.py +++ b/solarwinds_apm/apm_config.py @@ -82,8 +82,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 @@ -125,16 +124,25 @@ 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, + ) + else: + _ = self.check_service_name_migration() # Create final APM resource with SolarWinds attributes # and calculated service name @@ -255,15 +263,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 @@ -381,12 +380,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. @@ -406,19 +406,12 @@ 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: + 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 @@ -447,6 +440,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, @@ -496,20 +509,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:], @@ -913,10 +924,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: @@ -937,3 +950,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/solarwinds_apm/distro.py b/solarwinds_apm/distro.py index 9f69fd1ae..b2ba16f7f 100644 --- a/solarwinds_apm/distro.py +++ b/solarwinds_apm/distro.py @@ -111,10 +111,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: @@ -122,16 +122,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/integration/test_base_sw_headers_attrs.py b/tests/integration/test_base_sw_headers_attrs.py index 6eda9b835..87d75297d 100644 --- a/tests/integration/test_base_sw_headers_attrs.py +++ b/tests/integration/test_base_sw_headers_attrs.py @@ -46,6 +46,7 @@ class TestBaseSwHeadersAndAttributes(TestBase): "SampleRate", "SampleSource", ] + SERVICE_KEY = "foo:bar" @staticmethod def assert_valid_trace_flags(trace_flags: str): @@ -102,7 +103,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 35b55d43e..171ad4177 100644 --- a/tests/integration/test_service_name.py +++ b/tests/integration/test_service_name.py @@ -209,6 +209,62 @@ def test_sw_key_fallback(self): assert isinstance(resource_attrs["host.name"], str) +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( TestBaseSwHeadersAndAttributes ): diff --git a/tests/unit/test_apm_config/test_apm_config.py b/tests/unit/test_apm_config/test_apm_config.py index d251d6ac9..598fd49e9 100644 --- a/tests/unit/test_apm_config/test_apm_config.py +++ b/tests/unit/test_apm_config/test_apm_config.py @@ -98,7 +98,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, { @@ -106,12 +106,17 @@ def test__init_invalid_service_key_format(self, mocker): }, ) 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 + == Resource.create().attributes["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, @@ -122,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): @@ -331,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, @@ -363,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( @@ -887,10 +880,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): 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 f47e0357f..931bfd8a1 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 @@ -5,6 +5,7 @@ import os import pytest +from opentelemetry.sdk.resources import Resource from solarwinds_apm import apm_config @@ -132,11 +133,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", }, ) @@ -150,8 +157,11 @@ 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.service_name == "" + assert resulting_config._calculate_agent_enabled() + 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_key_format_deprecated.py b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py new file mode 100644 index 000000000..600b522a8 --- /dev/null +++ b/tests/unit/test_apm_config/test_apm_config_service_key_format_deprecated.py @@ -0,0 +1,37 @@ +# Copyright SolarWinds Worldwide, LLC. +# SPDX-License-Identifier: Apache-2.0 +"""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, + 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 + + 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 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 d0b86fee5..cf5383d38 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 @@ -1,6 +1,7 @@ # Copyright SolarWinds Worldwide, LLC. # SPDX-License-Identifier: Apache-2.0 +import logging import os from opentelemetry.sdk.resources import Resource @@ -72,6 +73,55 @@ 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"}, + ) + mocker.patch.object( + logging.getLogger("solarwinds_apm"), "propagate", True + ) + 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() @@ -131,22 +181,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 + 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, + 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, diff --git a/tests/unit/test_distro.py b/tests/unit/test_distro.py index 1a5edae0f..16bfb30b5 100644 --- a/tests/unit/test_distro.py +++ b/tests/unit/test_distro.py @@ -129,11 +129,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( @@ -645,6 +643,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, diff --git a/tests/unit/test_oboe/test_http_sampler.py b/tests/unit/test_oboe/test_http_sampler.py index 50278595b..9e9c73703 100644 --- a/tests/unit/test_oboe/test_http_sampler.py +++ b/tests/unit/test_oboe/test_http_sampler.py @@ -3,12 +3,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 ( @@ -67,6 +69,83 @@ 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, + ) + 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(): meter_provider = MeterProvider( metric_readers=[InMemoryMetricReader()],