From 13fd0aed40c92558577d009703b74227e3713c95 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 29 Jul 2026 16:47:32 -0700 Subject: [PATCH 1/9] Add basic,edge test cases --- .../integration/test_set_transaction_name.py | 277 ++++++++++++++++++ 1 file changed, 277 insertions(+) create mode 100644 tests/integration/test_set_transaction_name.py diff --git a/tests/integration/test_set_transaction_name.py b/tests/integration/test_set_transaction_name.py new file mode 100644 index 000000000..41fdba753 --- /dev/null +++ b/tests/integration/test_set_transaction_name.py @@ -0,0 +1,277 @@ +# © 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. + +import time +from unittest import mock + +from solarwinds_apm.api import set_transaction_name +from solarwinds_apm.apm_constants import INTL_SWO_TRANSACTION_ATTR_KEY +from solarwinds_apm.trace.serviceentry_processor import ServiceEntrySpanProcessor + +from .test_base_sw_headers_attrs import TestBaseSwHeadersAndAttributes + + +class TestSetTransactionNameBasic(TestBaseSwHeadersAndAttributes): + """Basic functionality tests for set_transaction_name()""" + + def setUp(self): + super().setUp() + self.tracer_provider.add_span_processor(ServiceEntrySpanProcessor()) + + def _setup_endpoints(self): + """Set up test routes before Flask instrumentation""" + super()._setup_endpoints() + + def route_single_call(): + set_transaction_name("custom-name") + return "ok" + + def route_multiple_calls(): + set_transaction_name("first") + set_transaction_name("second") + return "ok" + + # pylint: disable=no-member + self.app.route("/test_single_call/")(route_single_call) + self.app.route("/test_multiple_calls/")(route_multiple_calls) + + def test_single_call_sets_attribute(self): + """Test that single call to set_transaction_name sets sw.transaction attribute""" + 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_single_call/") + assert resp.status_code == 200 + spans = self.memory_exporter.get_finished_spans() + assert len(spans) > 0 + entry_spans = [ + s for s in spans + if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + ] + assert len(entry_spans) == 1 + entry_span = entry_spans[0] + assert entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-name" + + def test_multiple_calls_last_wins(self): + """Test that multiple calls to set_transaction_name, last one wins""" + 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_multiple_calls/") + assert resp.status_code == 200 + spans = self.memory_exporter.get_finished_spans() + assert len(spans) > 0 + entry_spans = [ + s for s in spans + if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + ] + assert len(entry_spans) == 1 + entry_span = entry_spans[0] + assert entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "second" + + +class TestSetTransactionNameEdgeCases(TestBaseSwHeadersAndAttributes): + """Edge case tests for set_transaction_name()""" + + def setUp(self): + super().setUp() + self.tracer_provider.add_span_processor(ServiceEntrySpanProcessor()) + + def _setup_endpoints(self): + """Set up test routes before Flask instrumentation""" + super()._setup_endpoints() + + def route_empty_string(): + result = set_transaction_name("") + assert result is False + return "ok" + + def route_none_value(): + result = set_transaction_name(None) + assert result is False + return "ok" + + def route_long_name(): + long_name = "a" * 300 + set_transaction_name(long_name) + return "ok" + + # pylint: disable=no-member + self.app.route("/test_empty_string/")(route_empty_string) + self.app.route("/test_none_value/")(route_none_value) + self.app.route("/test_long_name/")(route_long_name) + + def test_empty_string_rejected(self): + """Test that empty string is rejected and original name preserved""" + 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_empty_string/") + assert resp.status_code == 200 + spans = self.memory_exporter.get_finished_spans() + assert len(spans) > 0 + entry_spans = [ + s for s in spans + if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + ] + assert len(entry_spans) == 1 + + entry_span = entry_spans[0] + txn_name = entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + assert txn_name is not None + assert txn_name != "" + assert "/test_empty_string/" == txn_name + + def test_none_value_rejected(self): + """Test that None value is rejected and original name preserved""" + 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_none_value/") + assert resp.status_code == 200 + spans = self.memory_exporter.get_finished_spans() + assert len(spans) > 0 + entry_spans = [ + s for s in spans + if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + ] + assert len(entry_spans) == 1 + + entry_span = entry_spans[0] + txn_name = entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + assert txn_name is not None + assert "test_none_value" in txn_name or "/test_none_value/" == txn_name + + def test_no_active_span_returns_false(self): + """Test that calling set_transaction_name outside request context returns False""" + result = set_transaction_name("test") + assert result is False + + def test_long_name_truncated(self): + """Test that long transaction names are truncated to 256 characters""" + 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_long_name/") + assert resp.status_code == 200 + spans = self.memory_exporter.get_finished_spans() + assert len(spans) > 0 + entry_spans = [ + s for s in spans + if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + ] + assert len(entry_spans) == 1 + + entry_span = entry_spans[0] + txn_name = entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + assert txn_name is not None + assert len(txn_name) == 256 + assert txn_name == "a" * 256 From db7fe1fb4737811ea57bf90202f690ff058604aa Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 29 Jul 2026 17:10:03 -0700 Subject: [PATCH 2/9] Add distributed set_txn_name integration test --- .../integration/test_set_transaction_name.py | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/tests/integration/test_set_transaction_name.py b/tests/integration/test_set_transaction_name.py index 41fdba753..b841b6586 100644 --- a/tests/integration/test_set_transaction_name.py +++ b/tests/integration/test_set_transaction_name.py @@ -4,9 +4,14 @@ # # 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 threading import time from unittest import mock +import flask +import requests +from werkzeug.serving import make_server + from solarwinds_apm.api import set_transaction_name from solarwinds_apm.apm_constants import INTL_SWO_TRANSACTION_ATTR_KEY from solarwinds_apm.trace.serviceentry_processor import ServiceEntrySpanProcessor @@ -275,3 +280,90 @@ def test_long_name_truncated(self): assert txn_name is not None assert len(txn_name) == 256 assert txn_name == "a" * 256 + +class TestSetTransactionNameDistributed(TestBaseSwHeadersAndAttributes): + """Distributed trace tests for set_transaction_name() + + Tests true distributed tracing by setting up two Flask apps where service A + makes an HTTP request to service B, propagating trace context between them. + """ + + def setUp(self): + super().setUp() + self.tracer_provider.add_span_processor(ServiceEntrySpanProcessor()) + + # Set up a second app in addition to self.app + # Should be done before service_a set up with call to this one + self.app_b = flask.Flask("service_b") + self.flask_inst.instrument_app(self.app_b) + + def service_b_endpoint(): + set_transaction_name("custom-service-b") + return "service-b-response" + + self.app_b.route("/service_b/")(service_b_endpoint) + + self.server = make_server("127.0.0.1", 5001, self.app_b, threaded=True) + self.server_thread = threading.Thread(target=self.server.serve_forever) + self.server_thread.daemon = True + self.server_thread.start() + + def tearDown(self): + self.server.shutdown() + self.server_thread.join(timeout=1) + super().tearDown() + + def _setup_endpoints(self): + """Set up test routes before Flask instrumentation""" + super()._setup_endpoints() + + def service_a_endpoint(): + set_transaction_name("custom-service-a") + resp = requests.get("http://127.0.0.1:5001/service_b/") + return f"service-a-response: {resp.text}" + + # pylint: disable=no-member + self.app.route("/service_a/")(service_a_endpoint) + + def test_custom_names_at_all_entry_spans(self): + """Test that custom names are set independently for each service entry span + + Service A calls service B via HTTP, creating a distributed trace where each + service sets its own custom transaction name on its respective entry span. + """ + 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_a = self.client.get("/service_a/") + assert resp_a.status_code == 200 + spans = self.memory_exporter.get_finished_spans() + assert len(spans) > 0 + entry_spans = [ + s for s in spans + if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + ] + assert len(entry_spans) == 2 + # leaf-most entry span will be first + assert entry_spans[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-b" + assert entry_spans[1].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-a" From 9ce612392c1c8332577dba088f041f5c54067c15 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 29 Jul 2026 17:21:12 -0700 Subject: [PATCH 3/9] add more_complex_traces --- .../integration/test_set_transaction_name.py | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/tests/integration/test_set_transaction_name.py b/tests/integration/test_set_transaction_name.py index b841b6586..b190fda8e 100644 --- a/tests/integration/test_set_transaction_name.py +++ b/tests/integration/test_set_transaction_name.py @@ -10,6 +10,7 @@ import flask import requests +from opentelemetry import trace from werkzeug.serving import make_server from solarwinds_apm.api import set_transaction_name @@ -367,3 +368,82 @@ def test_custom_names_at_all_entry_spans(self): # leaf-most entry span will be first assert entry_spans[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-b" assert entry_spans[1].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-a" + + def test_custom_names_across_more_complex_traces(self): + """Test custom names work correctly when manual spans are created with OTel SDK + + Each service creates manual child spans using start_as_current_span before calling + set_transaction_name. The entry spans should still get the custom names, and the + trace should contain additional manual spans. + """ + 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, + } + ], + ): + tracer = trace.get_tracer(__name__) + + def service_b_with_manual_spans(): + with tracer.start_as_current_span("manual-outer-b"): + current_span = trace.get_current_span() + current_span.set_attribute("test.custom_attribute", "outer-b") + with tracer.start_as_current_span("manual-inner-b"): + current_span = trace.get_current_span() + current_span.set_attribute("test.custom_attribute", "inner-b") + set_transaction_name("custom-service-b") + return "service-b-response" + + def service_a_with_manual_spans(): + with tracer.start_as_current_span("manual-outer-a"): + current_span = trace.get_current_span() + current_span.set_attribute("test.custom_attribute", "outer-a") + with tracer.start_as_current_span("manual-inner-a"): + current_span = trace.get_current_span() + current_span.set_attribute("test.custom_attribute", "inner-a") + set_transaction_name("custom-service-a") + resp = requests.get("http://127.0.0.1:5001/service_b_manual/") + return f"service-a-response: {resp.text}" + + self.app_b.route("/service_b_manual/")(service_b_with_manual_spans) + # pylint: disable=no-member + self.app.route("/service_a_manual/")(service_a_with_manual_spans) + + resp_a = self.client.get("/service_a_manual/") + assert resp_a.status_code == 200 + spans = self.memory_exporter.get_finished_spans() + assert len(spans) > 0 + + entry_spans = [ + s for s in spans + if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + ] + assert len(entry_spans) == 2 + # leaf-most entry span will be first + assert entry_spans[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-b" + assert entry_spans[1].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-a" + + manual_spans = [s for s in spans if s.name.startswith("manual-")] + assert len(manual_spans) == 4 + assert manual_spans[0].name == "manual-inner-b" + assert manual_spans[1].name == "manual-outer-b" + assert manual_spans[2].name == "manual-inner-a" + assert manual_spans[3].name == "manual-outer-a" From 088421ef8efccf64e334d4ca7bd1d32062b692ef Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 29 Jul 2026 18:05:09 -0700 Subject: [PATCH 4/9] lint --- .../integration/test_set_transaction_name.py | 190 ++++++++++++------ 1 file changed, 129 insertions(+), 61 deletions(-) diff --git a/tests/integration/test_set_transaction_name.py b/tests/integration/test_set_transaction_name.py index b190fda8e..a00b39508 100644 --- a/tests/integration/test_set_transaction_name.py +++ b/tests/integration/test_set_transaction_name.py @@ -1,8 +1,15 @@ # © 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 +# 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. +# 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 threading import time @@ -15,9 +22,13 @@ from solarwinds_apm.api import set_transaction_name from solarwinds_apm.apm_constants import INTL_SWO_TRANSACTION_ATTR_KEY -from solarwinds_apm.trace.serviceentry_processor import ServiceEntrySpanProcessor +from solarwinds_apm.trace.serviceentry_processor import ( + ServiceEntrySpanProcessor, +) -from .test_base_sw_headers_attrs import TestBaseSwHeadersAndAttributes +from .test_base_sw_headers_attrs import ( + TestBaseSwHeadersAndAttributes, +) class TestSetTransactionNameBasic(TestBaseSwHeadersAndAttributes): @@ -30,22 +41,24 @@ def setUp(self): def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" super()._setup_endpoints() - + def route_single_call(): set_transaction_name("custom-name") return "ok" - + def route_multiple_calls(): set_transaction_name("first") set_transaction_name("second") return "ok" - + # pylint: disable=no-member self.app.route("/test_single_call/")(route_single_call) self.app.route("/test_multiple_calls/")(route_multiple_calls) def test_single_call_sets_attribute(self): - """Test that single call to set_transaction_name sets sw.transaction attribute""" + """Test single call to set_transaction_name sets + sw.transaction attribute + """ timestamp = int(time.time()) with mock.patch( target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", @@ -75,15 +88,21 @@ def test_single_call_sets_attribute(self): spans = self.memory_exporter.get_finished_spans() assert len(spans) > 0 entry_spans = [ - s for s in spans - if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + s + for s in spans + if not ( + s.parent and s.parent.is_valid and not s.parent.is_remote + ) ] assert len(entry_spans) == 1 entry_span = entry_spans[0] - assert entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-name" + assert ( + entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-name" + ) def test_multiple_calls_last_wins(self): - """Test that multiple calls to set_transaction_name, last one wins""" + """Test multiple calls to set_transaction_name, last one wins""" timestamp = int(time.time()) with mock.patch( target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", @@ -113,12 +132,18 @@ def test_multiple_calls_last_wins(self): spans = self.memory_exporter.get_finished_spans() assert len(spans) > 0 entry_spans = [ - s for s in spans - if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + s + for s in spans + if not ( + s.parent and s.parent.is_valid and not s.parent.is_remote + ) ] assert len(entry_spans) == 1 entry_span = entry_spans[0] - assert entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "second" + assert ( + entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "second" + ) class TestSetTransactionNameEdgeCases(TestBaseSwHeadersAndAttributes): @@ -131,29 +156,29 @@ def setUp(self): def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" super()._setup_endpoints() - + def route_empty_string(): result = set_transaction_name("") assert result is False return "ok" - + def route_none_value(): result = set_transaction_name(None) assert result is False return "ok" - + def route_long_name(): long_name = "a" * 300 set_transaction_name(long_name) return "ok" - + # pylint: disable=no-member self.app.route("/test_empty_string/")(route_empty_string) self.app.route("/test_none_value/")(route_none_value) self.app.route("/test_long_name/")(route_long_name) def test_empty_string_rejected(self): - """Test that empty string is rejected and original name preserved""" + """Test empty string is rejected and original name preserved""" timestamp = int(time.time()) with mock.patch( target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", @@ -183,19 +208,22 @@ def test_empty_string_rejected(self): spans = self.memory_exporter.get_finished_spans() assert len(spans) > 0 entry_spans = [ - s for s in spans - if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + s + for s in spans + if not ( + s.parent and s.parent.is_valid and not s.parent.is_remote + ) ] assert len(entry_spans) == 1 - + entry_span = entry_spans[0] txn_name = entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) assert txn_name is not None assert txn_name != "" - assert "/test_empty_string/" == txn_name + assert txn_name == "/test_empty_string/" def test_none_value_rejected(self): - """Test that None value is rejected and original name preserved""" + """Test None value is rejected and original name preserved""" timestamp = int(time.time()) with mock.patch( target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", @@ -225,23 +253,31 @@ def test_none_value_rejected(self): spans = self.memory_exporter.get_finished_spans() assert len(spans) > 0 entry_spans = [ - s for s in spans - if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + s + for s in spans + if not ( + s.parent and s.parent.is_valid and not s.parent.is_remote + ) ] assert len(entry_spans) == 1 - + entry_span = entry_spans[0] txn_name = entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) assert txn_name is not None - assert "test_none_value" in txn_name or "/test_none_value/" == txn_name + assert ( + "test_none_value" in txn_name + or txn_name == "/test_none_value/" + ) def test_no_active_span_returns_false(self): - """Test that calling set_transaction_name outside request context returns False""" + """Test calling set_transaction_name outside request context + returns False + """ result = set_transaction_name("test") assert result is False def test_long_name_truncated(self): - """Test that long transaction names are truncated to 256 characters""" + """Test long transaction names are truncated to 256 characters""" timestamp = int(time.time()) with mock.patch( target="solarwinds_apm.oboe.json_sampler.JsonSampler._read", @@ -271,20 +307,24 @@ def test_long_name_truncated(self): spans = self.memory_exporter.get_finished_spans() assert len(spans) > 0 entry_spans = [ - s for s in spans - if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + s + for s in spans + if not ( + s.parent and s.parent.is_valid and not s.parent.is_remote + ) ] assert len(entry_spans) == 1 - + entry_span = entry_spans[0] txn_name = entry_span.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) assert txn_name is not None assert len(txn_name) == 256 assert txn_name == "a" * 256 + class TestSetTransactionNameDistributed(TestBaseSwHeadersAndAttributes): """Distributed trace tests for set_transaction_name() - + Tests true distributed tracing by setting up two Flask apps where service A makes an HTTP request to service B, propagating trace context between them. """ @@ -292,18 +332,18 @@ class TestSetTransactionNameDistributed(TestBaseSwHeadersAndAttributes): def setUp(self): super().setUp() self.tracer_provider.add_span_processor(ServiceEntrySpanProcessor()) - + # Set up a second app in addition to self.app # Should be done before service_a set up with call to this one self.app_b = flask.Flask("service_b") self.flask_inst.instrument_app(self.app_b) - + def service_b_endpoint(): set_transaction_name("custom-service-b") return "service-b-response" - + self.app_b.route("/service_b/")(service_b_endpoint) - + self.server = make_server("127.0.0.1", 5001, self.app_b, threaded=True) self.server_thread = threading.Thread(target=self.server.serve_forever) self.server_thread.daemon = True @@ -317,18 +357,18 @@ def tearDown(self): def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" super()._setup_endpoints() - + def service_a_endpoint(): set_transaction_name("custom-service-a") resp = requests.get("http://127.0.0.1:5001/service_b/") return f"service-a-response: {resp.text}" - + # pylint: disable=no-member self.app.route("/service_a/")(service_a_endpoint) def test_custom_names_at_all_entry_spans(self): """Test that custom names are set independently for each service entry span - + Service A calls service B via HTTP, creating a distributed trace where each service sets its own custom transaction name on its respective entry span. """ @@ -361,17 +401,26 @@ def test_custom_names_at_all_entry_spans(self): spans = self.memory_exporter.get_finished_spans() assert len(spans) > 0 entry_spans = [ - s for s in spans - if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + s + for s in spans + if not ( + s.parent and s.parent.is_valid and not s.parent.is_remote + ) ] assert len(entry_spans) == 2 # leaf-most entry span will be first - assert entry_spans[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-b" - assert entry_spans[1].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-a" + assert ( + entry_spans[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-service-b" + ) + assert ( + entry_spans[1].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-service-a" + ) def test_custom_names_across_more_complex_traces(self): """Test custom names work correctly when manual spans are created with OTel SDK - + Each service creates manual child spans using start_as_current_span before calling set_transaction_name. The entry spans should still get the custom names, and the trace should contain additional manual spans. @@ -401,46 +450,65 @@ def test_custom_names_across_more_complex_traces(self): ], ): tracer = trace.get_tracer(__name__) - + def service_b_with_manual_spans(): with tracer.start_as_current_span("manual-outer-b"): current_span = trace.get_current_span() - current_span.set_attribute("test.custom_attribute", "outer-b") + current_span.set_attribute( + "test.custom_attribute", "outer-b" + ) with tracer.start_as_current_span("manual-inner-b"): current_span = trace.get_current_span() - current_span.set_attribute("test.custom_attribute", "inner-b") + current_span.set_attribute( + "test.custom_attribute", "inner-b" + ) set_transaction_name("custom-service-b") return "service-b-response" - + def service_a_with_manual_spans(): with tracer.start_as_current_span("manual-outer-a"): current_span = trace.get_current_span() - current_span.set_attribute("test.custom_attribute", "outer-a") + current_span.set_attribute( + "test.custom_attribute", "outer-a" + ) with tracer.start_as_current_span("manual-inner-a"): current_span = trace.get_current_span() - current_span.set_attribute("test.custom_attribute", "inner-a") + current_span.set_attribute( + "test.custom_attribute", "inner-a" + ) set_transaction_name("custom-service-a") - resp = requests.get("http://127.0.0.1:5001/service_b_manual/") + resp = requests.get( + "http://127.0.0.1:5001/service_b_manual/" + ) return f"service-a-response: {resp.text}" - + self.app_b.route("/service_b_manual/")(service_b_with_manual_spans) # pylint: disable=no-member self.app.route("/service_a_manual/")(service_a_with_manual_spans) - + resp_a = self.client.get("/service_a_manual/") assert resp_a.status_code == 200 spans = self.memory_exporter.get_finished_spans() assert len(spans) > 0 entry_spans = [ - s for s in spans - if not (s.parent and s.parent.is_valid and not s.parent.is_remote) + s + for s in spans + if not ( + s.parent and s.parent.is_valid and not s.parent.is_remote + ) ] assert len(entry_spans) == 2 # leaf-most entry span will be first - assert entry_spans[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-b" - assert entry_spans[1].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-a" - + assert ( + entry_spans[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-service-b" + ) + assert ( + entry_spans[1].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-service-a" + ) + manual_spans = [s for s in spans if s.name.startswith("manual-")] assert len(manual_spans) == 4 assert manual_spans[0].name == "manual-inner-b" From 6cec5929ceaee8945289ee10e602839ddb9da2ed Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 29 Jul 2026 18:09:50 -0700 Subject: [PATCH 5/9] Register before serving --- .../integration/test_set_transaction_name.py | 73 +++++++++---------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/tests/integration/test_set_transaction_name.py b/tests/integration/test_set_transaction_name.py index a00b39508..ce5f83d6f 100644 --- a/tests/integration/test_set_transaction_name.py +++ b/tests/integration/test_set_transaction_name.py @@ -338,11 +338,28 @@ def setUp(self): self.app_b = flask.Flask("service_b") self.flask_inst.instrument_app(self.app_b) + # Get tracer for manual span tests + tracer = trace.get_tracer(__name__) + def service_b_endpoint(): set_transaction_name("custom-service-b") return "service-b-response" + def service_b_with_manual_spans(): + with tracer.start_as_current_span("manual-outer-b"): + current_span = trace.get_current_span() + current_span.set_attribute("test.custom_attribute", "outer-b") + with tracer.start_as_current_span("manual-inner-b"): + current_span = trace.get_current_span() + current_span.set_attribute( + "test.custom_attribute", "inner-b" + ) + set_transaction_name("custom-service-b") + return "service-b-response" + + # Register all routes before starting server self.app_b.route("/service_b/")(service_b_endpoint) + self.app_b.route("/service_b_manual/")(service_b_with_manual_spans) self.server = make_server("127.0.0.1", 5001, self.app_b, threaded=True) self.server_thread = threading.Thread(target=self.server.serve_forever) @@ -358,13 +375,32 @@ def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" super()._setup_endpoints() + # Get tracer for manual span tests + tracer = trace.get_tracer(__name__) + def service_a_endpoint(): set_transaction_name("custom-service-a") resp = requests.get("http://127.0.0.1:5001/service_b/") return f"service-a-response: {resp.text}" + def service_a_with_manual_spans(): + with tracer.start_as_current_span("manual-outer-a"): + current_span = trace.get_current_span() + current_span.set_attribute("test.custom_attribute", "outer-a") + with tracer.start_as_current_span("manual-inner-a"): + current_span = trace.get_current_span() + current_span.set_attribute( + "test.custom_attribute", "inner-a" + ) + set_transaction_name("custom-service-a") + resp = requests.get( + "http://127.0.0.1:5001/service_b_manual/" + ) + return f"service-a-response: {resp.text}" + # pylint: disable=no-member self.app.route("/service_a/")(service_a_endpoint) + self.app.route("/service_a_manual/")(service_a_with_manual_spans) def test_custom_names_at_all_entry_spans(self): """Test that custom names are set independently for each service entry span @@ -449,43 +485,6 @@ def test_custom_names_across_more_complex_traces(self): } ], ): - tracer = trace.get_tracer(__name__) - - def service_b_with_manual_spans(): - with tracer.start_as_current_span("manual-outer-b"): - current_span = trace.get_current_span() - current_span.set_attribute( - "test.custom_attribute", "outer-b" - ) - with tracer.start_as_current_span("manual-inner-b"): - current_span = trace.get_current_span() - current_span.set_attribute( - "test.custom_attribute", "inner-b" - ) - set_transaction_name("custom-service-b") - return "service-b-response" - - def service_a_with_manual_spans(): - with tracer.start_as_current_span("manual-outer-a"): - current_span = trace.get_current_span() - current_span.set_attribute( - "test.custom_attribute", "outer-a" - ) - with tracer.start_as_current_span("manual-inner-a"): - current_span = trace.get_current_span() - current_span.set_attribute( - "test.custom_attribute", "inner-a" - ) - set_transaction_name("custom-service-a") - resp = requests.get( - "http://127.0.0.1:5001/service_b_manual/" - ) - return f"service-a-response: {resp.text}" - - self.app_b.route("/service_b_manual/")(service_b_with_manual_spans) - # pylint: disable=no-member - self.app.route("/service_a_manual/")(service_a_with_manual_spans) - resp_a = self.client.get("/service_a_manual/") assert resp_a.status_code == 200 spans = self.memory_exporter.get_finished_spans() From e1aa8f637cc9d0d3048a9e581d00a3708fae30e1 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Wed, 29 Jul 2026 18:15:47 -0700 Subject: [PATCH 6/9] Add timeout 5 --- tests/integration/test_set_transaction_name.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/test_set_transaction_name.py b/tests/integration/test_set_transaction_name.py index ce5f83d6f..d0560d9df 100644 --- a/tests/integration/test_set_transaction_name.py +++ b/tests/integration/test_set_transaction_name.py @@ -380,7 +380,7 @@ def _setup_endpoints(self): def service_a_endpoint(): set_transaction_name("custom-service-a") - resp = requests.get("http://127.0.0.1:5001/service_b/") + resp = requests.get("http://127.0.0.1:5001/service_b/", timeout=5) return f"service-a-response: {resp.text}" def service_a_with_manual_spans(): @@ -394,7 +394,8 @@ def service_a_with_manual_spans(): ) set_transaction_name("custom-service-a") resp = requests.get( - "http://127.0.0.1:5001/service_b_manual/" + "http://127.0.0.1:5001/service_b_manual/", + timeout=5, ) return f"service-a-response: {resp.text}" From d874038f7a3efedbafaf3ad0627bca79b8c7a9da Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 30 Jul 2026 09:50:24 -0700 Subject: [PATCH 7/9] Add response_time metrics check --- .../integration/test_base_sw_headers_attrs.py | 6 +- .../integration/test_set_transaction_name.py | 109 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_base_sw_headers_attrs.py b/tests/integration/test_base_sw_headers_attrs.py index c311a05d3..0dc73a9ad 100644 --- a/tests/integration/test_base_sw_headers_attrs.py +++ b/tests/integration/test_base_sw_headers_attrs.py @@ -15,8 +15,10 @@ from opentelemetry import trace as trace_api from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.requests import RequestsInstrumentor +from opentelemetry.metrics import set_meter_provider from opentelemetry.propagate import get_global_textmap from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import InMemoryMetricReader from opentelemetry.sdk.trace import TracerProvider, export from opentelemetry.sdk.trace.export.in_memory_span_exporter import ( InMemorySpanExporter, @@ -124,7 +126,9 @@ def setUp(self): reset_trace_globals() reset_metrics_globals() # Init parent-based with JsonSampler to guarantee sampling decision for tests - self.meter_provider = MeterProvider() + self.metric_reader = InMemoryMetricReader() + self.meter_provider = MeterProvider(metric_readers=[self.metric_reader]) + set_meter_provider(self.meter_provider) sampler_configuration = SolarWindsApmConfig.to_configuration(apm_config) json_sampler = JsonSampler( self.meter_provider, diff --git a/tests/integration/test_set_transaction_name.py b/tests/integration/test_set_transaction_name.py index d0560d9df..0af89bc6f 100644 --- a/tests/integration/test_set_transaction_name.py +++ b/tests/integration/test_set_transaction_name.py @@ -22,6 +22,9 @@ from solarwinds_apm.api import set_transaction_name from solarwinds_apm.apm_constants import INTL_SWO_TRANSACTION_ATTR_KEY +from solarwinds_apm.trace.response_time_processor import ( + ResponseTimeProcessor, +) from solarwinds_apm.trace.serviceentry_processor import ( ServiceEntrySpanProcessor, ) @@ -37,6 +40,27 @@ class TestSetTransactionNameBasic(TestBaseSwHeadersAndAttributes): def setUp(self): super().setUp() self.tracer_provider.add_span_processor(ServiceEntrySpanProcessor()) + self.tracer_provider.add_span_processor( + ResponseTimeProcessor(self.configurator.apm_config) + ) + + def _get_metrics_for_transaction(self, transaction_name): + """Helper to get metrics data filtered by transaction name""" + self.metric_reader.collect() + metrics_data = self.metric_reader.get_metrics_data() + if not metrics_data or not metrics_data.resource_metrics: + return [] + + matching_data_points = [] + for resource_metric in metrics_data.resource_metrics: + for scope_metric in resource_metric.scope_metrics: + for metric in scope_metric.metrics: + if metric.name == "trace.service.response_time": + for data_point in metric.data.data_points: + txn = data_point.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + if txn == transaction_name: + matching_data_points.append(data_point) + return matching_data_points def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" @@ -101,6 +125,11 @@ def test_single_call_sets_attribute(self): == "custom-name" ) + # Verify metrics also have correct transaction name + metrics = self._get_metrics_for_transaction("custom-name") + assert len(metrics) == 1 + assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-name" + def test_multiple_calls_last_wins(self): """Test multiple calls to set_transaction_name, last one wins""" timestamp = int(time.time()) @@ -145,6 +174,11 @@ def test_multiple_calls_last_wins(self): == "second" ) + # Verify metrics also have correct transaction name + metrics = self._get_metrics_for_transaction("second") + assert len(metrics) == 1 + assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "second" + class TestSetTransactionNameEdgeCases(TestBaseSwHeadersAndAttributes): """Edge case tests for set_transaction_name()""" @@ -152,6 +186,27 @@ class TestSetTransactionNameEdgeCases(TestBaseSwHeadersAndAttributes): def setUp(self): super().setUp() self.tracer_provider.add_span_processor(ServiceEntrySpanProcessor()) + self.tracer_provider.add_span_processor( + ResponseTimeProcessor(self.configurator.apm_config) + ) + + def _get_metrics_for_transaction(self, transaction_name): + """Helper to get metrics data filtered by transaction name""" + self.metric_reader.collect() + metrics_data = self.metric_reader.get_metrics_data() + if not metrics_data or not metrics_data.resource_metrics: + return [] + + matching_data_points = [] + for resource_metric in metrics_data.resource_metrics: + for scope_metric in resource_metric.scope_metrics: + for metric in scope_metric.metrics: + if metric.name == "trace.service.response_time": + for data_point in metric.data.data_points: + txn = data_point.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + if txn == transaction_name: + matching_data_points.append(data_point) + return matching_data_points def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" @@ -222,6 +277,11 @@ def test_empty_string_rejected(self): assert txn_name != "" assert txn_name == "/test_empty_string/" + # Verify metrics also have correct transaction name + metrics = self._get_metrics_for_transaction("/test_empty_string/") + assert len(metrics) == 1 + assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "/test_empty_string/" + def test_none_value_rejected(self): """Test None value is rejected and original name preserved""" timestamp = int(time.time()) @@ -269,6 +329,11 @@ def test_none_value_rejected(self): or txn_name == "/test_none_value/" ) + # Verify metrics also have correct transaction name + metrics = self._get_metrics_for_transaction(txn_name) + assert len(metrics) == 1 + assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == txn_name + def test_no_active_span_returns_false(self): """Test calling set_transaction_name outside request context returns False @@ -321,6 +386,11 @@ def test_long_name_truncated(self): assert len(txn_name) == 256 assert txn_name == "a" * 256 + # Verify metrics also have correct transaction name + metrics = self._get_metrics_for_transaction(txn_name) + assert len(metrics) == 1 + assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == txn_name + class TestSetTransactionNameDistributed(TestBaseSwHeadersAndAttributes): """Distributed trace tests for set_transaction_name() @@ -332,6 +402,9 @@ class TestSetTransactionNameDistributed(TestBaseSwHeadersAndAttributes): def setUp(self): super().setUp() self.tracer_provider.add_span_processor(ServiceEntrySpanProcessor()) + self.tracer_provider.add_span_processor( + ResponseTimeProcessor(self.configurator.apm_config) + ) # Set up a second app in addition to self.app # Should be done before service_a set up with call to this one @@ -371,6 +444,24 @@ def tearDown(self): self.server_thread.join(timeout=1) super().tearDown() + def _get_metrics_for_transaction(self, transaction_name): + """Helper to get metrics data filtered by transaction name""" + self.metric_reader.collect() + metrics_data = self.metric_reader.get_metrics_data() + if not metrics_data or not metrics_data.resource_metrics: + return [] + + matching_data_points = [] + for resource_metric in metrics_data.resource_metrics: + for scope_metric in resource_metric.scope_metrics: + for metric in scope_metric.metrics: + if metric.name == "trace.service.response_time": + for data_point in metric.data.data_points: + txn = data_point.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + if txn == transaction_name: + matching_data_points.append(data_point) + return matching_data_points + def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" super()._setup_endpoints() @@ -455,6 +546,15 @@ def test_custom_names_at_all_entry_spans(self): == "custom-service-a" ) + # Verify metrics also have correct transaction names + metrics_a = self._get_metrics_for_transaction("custom-service-a") + assert len(metrics_a) == 1 + assert metrics_a[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-a" + + metrics_b = self._get_metrics_for_transaction("custom-service-b") + assert len(metrics_b) == 1 + assert metrics_b[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-b" + def test_custom_names_across_more_complex_traces(self): """Test custom names work correctly when manual spans are created with OTel SDK @@ -515,3 +615,12 @@ def test_custom_names_across_more_complex_traces(self): assert manual_spans[1].name == "manual-outer-b" assert manual_spans[2].name == "manual-inner-a" assert manual_spans[3].name == "manual-outer-a" + + # Verify metrics also have correct transaction names + metrics_a = self._get_metrics_for_transaction("custom-service-a") + assert len(metrics_a) == 1 + assert metrics_a[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-a" + + metrics_b = self._get_metrics_for_transaction("custom-service-b") + assert len(metrics_b) == 1 + assert metrics_b[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-b" From 7d44cdf3697946b88ea19082e3160dc1c5c3db9e Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 30 Jul 2026 09:56:30 -0700 Subject: [PATCH 8/9] Refactor new test base class --- .../integration/test_set_transaction_name.py | 59 +++---------------- 1 file changed, 8 insertions(+), 51 deletions(-) diff --git a/tests/integration/test_set_transaction_name.py b/tests/integration/test_set_transaction_name.py index 0af89bc6f..84e175016 100644 --- a/tests/integration/test_set_transaction_name.py +++ b/tests/integration/test_set_transaction_name.py @@ -34,8 +34,8 @@ ) -class TestSetTransactionNameBasic(TestBaseSwHeadersAndAttributes): - """Basic functionality tests for set_transaction_name()""" +class TestBaseTransactionName(TestBaseSwHeadersAndAttributes): + """Base class for set_transaction_name() tests with common setup and helpers""" def setUp(self): super().setUp() @@ -62,6 +62,10 @@ def _get_metrics_for_transaction(self, transaction_name): matching_data_points.append(data_point) return matching_data_points + +class TestSetTransactionNameBasic(TestBaseTransactionName): + """Basic functionality tests for set_transaction_name()""" + def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" super()._setup_endpoints() @@ -180,34 +184,9 @@ def test_multiple_calls_last_wins(self): assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "second" -class TestSetTransactionNameEdgeCases(TestBaseSwHeadersAndAttributes): +class TestSetTransactionNameEdgeCases(TestBaseTransactionName): """Edge case tests for set_transaction_name()""" - def setUp(self): - super().setUp() - self.tracer_provider.add_span_processor(ServiceEntrySpanProcessor()) - self.tracer_provider.add_span_processor( - ResponseTimeProcessor(self.configurator.apm_config) - ) - - def _get_metrics_for_transaction(self, transaction_name): - """Helper to get metrics data filtered by transaction name""" - self.metric_reader.collect() - metrics_data = self.metric_reader.get_metrics_data() - if not metrics_data or not metrics_data.resource_metrics: - return [] - - matching_data_points = [] - for resource_metric in metrics_data.resource_metrics: - for scope_metric in resource_metric.scope_metrics: - for metric in scope_metric.metrics: - if metric.name == "trace.service.response_time": - for data_point in metric.data.data_points: - txn = data_point.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) - if txn == transaction_name: - matching_data_points.append(data_point) - return matching_data_points - def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" super()._setup_endpoints() @@ -392,7 +371,7 @@ def test_long_name_truncated(self): assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == txn_name -class TestSetTransactionNameDistributed(TestBaseSwHeadersAndAttributes): +class TestSetTransactionNameDistributed(TestBaseTransactionName): """Distributed trace tests for set_transaction_name() Tests true distributed tracing by setting up two Flask apps where service A @@ -401,10 +380,6 @@ class TestSetTransactionNameDistributed(TestBaseSwHeadersAndAttributes): def setUp(self): super().setUp() - self.tracer_provider.add_span_processor(ServiceEntrySpanProcessor()) - self.tracer_provider.add_span_processor( - ResponseTimeProcessor(self.configurator.apm_config) - ) # Set up a second app in addition to self.app # Should be done before service_a set up with call to this one @@ -444,24 +419,6 @@ def tearDown(self): self.server_thread.join(timeout=1) super().tearDown() - def _get_metrics_for_transaction(self, transaction_name): - """Helper to get metrics data filtered by transaction name""" - self.metric_reader.collect() - metrics_data = self.metric_reader.get_metrics_data() - if not metrics_data or not metrics_data.resource_metrics: - return [] - - matching_data_points = [] - for resource_metric in metrics_data.resource_metrics: - for scope_metric in resource_metric.scope_metrics: - for metric in scope_metric.metrics: - if metric.name == "trace.service.response_time": - for data_point in metric.data.data_points: - txn = data_point.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) - if txn == transaction_name: - matching_data_points.append(data_point) - return matching_data_points - def _setup_endpoints(self): """Set up test routes before Flask instrumentation""" super()._setup_endpoints() From 36d1b187e2cc399f92232bedd06226da9ef46da7 Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 30 Jul 2026 09:56:58 -0700 Subject: [PATCH 9/9] ruff --- .../integration/test_set_transaction_name.py | 55 ++++++++++++++----- 1 file changed, 42 insertions(+), 13 deletions(-) diff --git a/tests/integration/test_set_transaction_name.py b/tests/integration/test_set_transaction_name.py index 84e175016..98865068b 100644 --- a/tests/integration/test_set_transaction_name.py +++ b/tests/integration/test_set_transaction_name.py @@ -50,14 +50,16 @@ def _get_metrics_for_transaction(self, transaction_name): metrics_data = self.metric_reader.get_metrics_data() if not metrics_data or not metrics_data.resource_metrics: return [] - + matching_data_points = [] for resource_metric in metrics_data.resource_metrics: for scope_metric in resource_metric.scope_metrics: for metric in scope_metric.metrics: if metric.name == "trace.service.response_time": for data_point in metric.data.data_points: - txn = data_point.attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + txn = data_point.attributes.get( + INTL_SWO_TRANSACTION_ATTR_KEY + ) if txn == transaction_name: matching_data_points.append(data_point) return matching_data_points @@ -132,7 +134,10 @@ def test_single_call_sets_attribute(self): # Verify metrics also have correct transaction name metrics = self._get_metrics_for_transaction("custom-name") assert len(metrics) == 1 - assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-name" + assert ( + metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-name" + ) def test_multiple_calls_last_wins(self): """Test multiple calls to set_transaction_name, last one wins""" @@ -181,7 +186,10 @@ def test_multiple_calls_last_wins(self): # Verify metrics also have correct transaction name metrics = self._get_metrics_for_transaction("second") assert len(metrics) == 1 - assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "second" + assert ( + metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "second" + ) class TestSetTransactionNameEdgeCases(TestBaseTransactionName): @@ -259,7 +267,10 @@ def test_empty_string_rejected(self): # Verify metrics also have correct transaction name metrics = self._get_metrics_for_transaction("/test_empty_string/") assert len(metrics) == 1 - assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "/test_empty_string/" + assert ( + metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "/test_empty_string/" + ) def test_none_value_rejected(self): """Test None value is rejected and original name preserved""" @@ -311,7 +322,10 @@ def test_none_value_rejected(self): # Verify metrics also have correct transaction name metrics = self._get_metrics_for_transaction(txn_name) assert len(metrics) == 1 - assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == txn_name + assert ( + metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == txn_name + ) def test_no_active_span_returns_false(self): """Test calling set_transaction_name outside request context @@ -368,7 +382,10 @@ def test_long_name_truncated(self): # Verify metrics also have correct transaction name metrics = self._get_metrics_for_transaction(txn_name) assert len(metrics) == 1 - assert metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == txn_name + assert ( + metrics[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == txn_name + ) class TestSetTransactionNameDistributed(TestBaseTransactionName): @@ -506,11 +523,17 @@ def test_custom_names_at_all_entry_spans(self): # Verify metrics also have correct transaction names metrics_a = self._get_metrics_for_transaction("custom-service-a") assert len(metrics_a) == 1 - assert metrics_a[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-a" - + assert ( + metrics_a[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-service-a" + ) + metrics_b = self._get_metrics_for_transaction("custom-service-b") assert len(metrics_b) == 1 - assert metrics_b[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-b" + assert ( + metrics_b[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-service-b" + ) def test_custom_names_across_more_complex_traces(self): """Test custom names work correctly when manual spans are created with OTel SDK @@ -576,8 +599,14 @@ def test_custom_names_across_more_complex_traces(self): # Verify metrics also have correct transaction names metrics_a = self._get_metrics_for_transaction("custom-service-a") assert len(metrics_a) == 1 - assert metrics_a[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-a" - + assert ( + metrics_a[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-service-a" + ) + metrics_b = self._get_metrics_for_transaction("custom-service-b") assert len(metrics_b) == 1 - assert metrics_b[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) == "custom-service-b" + assert ( + metrics_b[0].attributes.get(INTL_SWO_TRANSACTION_ATTR_KEY) + == "custom-service-b" + )