From 3b482180f0c12824bba4617b4bd3b5fe221de65b Mon Sep 17 00:00:00 2001 From: tammy-baylis-swi Date: Thu, 16 Jul 2026 09:51:23 -0700 Subject: [PATCH 1/3] ResponseTime defensive if status_code non-compliant string --- .../trace/response_time_processor.py | 49 +++++-- .../test_response_time_processor.py | 125 ++++++++++++++++++ 2 files changed, 162 insertions(+), 12 deletions(-) diff --git a/solarwinds_apm/trace/response_time_processor.py b/solarwinds_apm/trace/response_time_processor.py index df9cac047..8781b32e3 100644 --- a/solarwinds_apm/trace/response_time_processor.py +++ b/solarwinds_apm/trace/response_time_processor.py @@ -185,17 +185,38 @@ def enhance_meter_attrs_with_http_span_attrs( self._HTTP_RESPONSE_STATUS_CODE, None ) status_code_old = span.attributes.get(self._HTTP_STATUS_CODE, None) - if status_code_new and status_code_new > 0: - meter_attrs.update( - {self._HTTP_RESPONSE_STATUS_CODE: status_code_new} - ) - elif status_code_old and status_code_old > 0: - meter_attrs.update( - {self._HTTP_RESPONSE_STATUS_CODE: status_code_old} - ) - # Something went wrong in OTel or instrumented service crashed early - # if no status_code, current nor deprecated, in attributes of HTTP span + + # Convert to int (compliant) if str (non-compliant but can happen) + # RFC 9110: status-code should be 3-digit (integer) + status_code = None + if status_code_new is not None: + try: + status_code = int(status_code_new) + if status_code <= 0: + status_code = None + except (ValueError, TypeError): + logger.debug( + "Expected HTTP status code as int (RFC 9110), but got %s. Skipping response time attribute.", + type(status_code_new).__name__, + ) + + # Fall back to deprecated attribute if new attribute was invalid/missing + if status_code is None and status_code_old is not None: + try: + status_code = int(status_code_old) + if status_code <= 0: + status_code = None + except (ValueError, TypeError): + logger.debug( + "Expected HTTP status code as int (RFC 9110), but got %s. Skipping response time attribute.", + type(status_code_old).__name__, + ) + + if status_code is not None and status_code > 0: + meter_attrs.update({self._HTTP_RESPONSE_STATUS_CODE: status_code}) else: + # Something went wrong in OTel or instrumented service crashed early + # if no status_code, current nor deprecated, in attributes of HTTP span meter_attrs.update( { self._HTTP_RESPONSE_STATUS_CODE: self._HTTP_SPAN_STATUS_UNAVAILABLE @@ -207,9 +228,13 @@ def enhance_meter_attrs_with_http_span_attrs( ) request_method_old = span.attributes.get(self._HTTP_METHOD, None) if request_method_new: - meter_attrs.update({self._HTTP_REQUEST_METHOD: request_method_new}) + meter_attrs.update( + {self._HTTP_REQUEST_METHOD: str(request_method_new)} + ) elif request_method_old: - meter_attrs.update({self._HTTP_REQUEST_METHOD: request_method_old}) + meter_attrs.update( + {self._HTTP_REQUEST_METHOD: str(request_method_old)} + ) return meter_attrs diff --git a/tests/unit/test_processors/test_response_time_processor.py b/tests/unit/test_processors/test_response_time_processor.py index bac67b9a7..bbb66a22c 100644 --- a/tests/unit/test_processors/test_response_time_processor.py +++ b/tests/unit/test_processors/test_response_time_processor.py @@ -468,6 +468,131 @@ def test_enhance_meter_attrs_with_http_span_attrs_no_http_attrs(self, mocker): assert "http.request.method" not in result assert result["existing"] == "value" + def test_enhance_meter_attrs_with_http_span_attrs_string_status_code_new_attr(self, mocker): + """Test that string status codes are converted to int (new attr)""" + mock_apm_config = self.get_mock_apm_config(mocker) + processor = ResponseTimeProcessor(mock_apm_config) + + mock_span = mocker.Mock() + mock_span.configure_mock(**{ + "attributes": { + "http.request.method": "GET", + "http.response.status_code": "200" # String instead of int + } + }) + + meter_attrs = {} + result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) + + assert result["http.response.status_code"] == 200 + assert result["http.request.method"] == "GET" + + def test_enhance_meter_attrs_with_http_span_attrs_string_status_code_old_attr(self, mocker): + """Test that string status codes are converted to int (deprecated attr)""" + mock_apm_config = self.get_mock_apm_config(mocker) + processor = ResponseTimeProcessor(mock_apm_config) + + mock_span = mocker.Mock() + mock_span.configure_mock(**{ + "attributes": { + "http.method": "POST", + "http.status_code": "404" # String instead of int + } + }) + + meter_attrs = {} + result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) + + assert result["http.response.status_code"] == 404 + assert result["http.request.method"] == "POST" + + def test_enhance_meter_attrs_with_http_span_attrs_invalid_string_status_code(self, mocker): + """Test that invalid string status codes fall back to unavailable (0)""" + mock_apm_config = self.get_mock_apm_config(mocker) + processor = ResponseTimeProcessor(mock_apm_config) + + mock_span = mocker.Mock() + mock_span.configure_mock(**{ + "attributes": { + "http.request.method": "GET", + "http.response.status_code": "invalid" # Cannot convert to int + } + }) + + meter_attrs = {} + mock_logger = mocker.patch("solarwinds_apm.trace.response_time_processor.logger") + + result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) + + assert result["http.response.status_code"] == 0 # Fallback to unavailable + assert result["http.request.method"] == "GET" + mock_logger.debug.assert_called_once() + assert "Expected HTTP status code as int" in mock_logger.debug.call_args[0][0] + + def test_enhance_meter_attrs_with_http_span_attrs_invalid_type_status_code(self, mocker): + """Test that non-string/non-int status codes fall back to unavailable (0)""" + mock_apm_config = self.get_mock_apm_config(mocker) + processor = ResponseTimeProcessor(mock_apm_config) + + mock_span = mocker.Mock() + mock_span.configure_mock(**{ + "attributes": { + "http.request.method": "GET", + "http.response.status_code": ["200"] # List instead of int + } + }) + + meter_attrs = {} + mock_logger = mocker.patch("solarwinds_apm.trace.response_time_processor.logger") + + result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) + + assert result["http.response.status_code"] == 0 # Fallback to unavailable + assert result["http.request.method"] == "GET" + mock_logger.debug.assert_called_once() + assert "Expected HTTP status code as int" in mock_logger.debug.call_args[0][0] + + def test_enhance_meter_attrs_with_http_span_attrs_string_zero_status_code(self, mocker): + """Test that string '0' is converted but treated as invalid (fallback to old attr)""" + mock_apm_config = self.get_mock_apm_config(mocker) + processor = ResponseTimeProcessor(mock_apm_config) + + mock_span = mocker.Mock() + mock_span.configure_mock(**{ + "attributes": { + "http.request.method": "GET", + "http.response.status_code": "0", + "http.status_code": 200 + } + }) + + meter_attrs = {} + result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) + + # String "0" converts to int 0, which is <= 0, so falls back to old attr + assert result["http.response.status_code"] == 200 + assert result["http.request.method"] == "GET" + + def test_enhance_meter_attrs_with_http_span_attrs_negative_string_status_code(self, mocker): + """Test that negative string status codes are treated as invalid""" + mock_apm_config = self.get_mock_apm_config(mocker) + processor = ResponseTimeProcessor(mock_apm_config) + + mock_span = mocker.Mock() + mock_span.configure_mock(**{ + "attributes": { + "http.request.method": "GET", + "http.response.status_code": "-1" + } + }) + + meter_attrs = {} + result = processor.enhance_meter_attrs_with_http_span_attrs(mock_span, meter_attrs) + + # Negative values are treated as invalid, fallback to unavailable + assert result["http.response.status_code"] == 0 + assert result["http.request.method"] == "GET" + def test_on_end_valid_local_parent_span(self, mocker): """Only scenario to skip OTLP metrics generation (not entry span)""" mock_txname_manager, \ From ff3b3de9165a3b63202cb739562cd59593935ebb Mon Sep 17 00:00:00 2001 From: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:02:07 -0700 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- solarwinds_apm/trace/response_time_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarwinds_apm/trace/response_time_processor.py b/solarwinds_apm/trace/response_time_processor.py index 8781b32e3..1504ab6e3 100644 --- a/solarwinds_apm/trace/response_time_processor.py +++ b/solarwinds_apm/trace/response_time_processor.py @@ -196,7 +196,7 @@ def enhance_meter_attrs_with_http_span_attrs( status_code = None except (ValueError, TypeError): logger.debug( - "Expected HTTP status code as int (RFC 9110), but got %s. Skipping response time attribute.", + "Expected HTTP status code as int (RFC 9110), but got %s. Ignoring invalid value.", type(status_code_new).__name__, ) From 8e4821a70f259f0b2a86ae1087ab932082de0a3c Mon Sep 17 00:00:00 2001 From: Tammy Baylis <96076570+tammy-baylis-swi@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:02:38 -0700 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- solarwinds_apm/trace/response_time_processor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarwinds_apm/trace/response_time_processor.py b/solarwinds_apm/trace/response_time_processor.py index 1504ab6e3..a608c55c7 100644 --- a/solarwinds_apm/trace/response_time_processor.py +++ b/solarwinds_apm/trace/response_time_processor.py @@ -208,7 +208,7 @@ def enhance_meter_attrs_with_http_span_attrs( status_code = None except (ValueError, TypeError): logger.debug( - "Expected HTTP status code as int (RFC 9110), but got %s. Skipping response time attribute.", + "Expected HTTP status code as int (RFC 9110), but got %s. Ignoring invalid value.", type(status_code_old).__name__, )