diff --git a/awx/main/analytics/core.py b/awx/main/analytics/core.py index 4407ab6257..7245297e11 100644 --- a/awx/main/analytics/core.py +++ b/awx/main/analytics/core.py @@ -420,6 +420,18 @@ def gather(dest=None, module=None, subset=None, since=None, until=None, collecti return tarfiles +def _log_shipping_response(response, path): + filename = os.path.basename(path) + try: + data = response.json() + request_id = data.get('request_id', 'unknown') + account_number = data.get('account_number', 'unknown') + org_id = data.get('org_id', 'unknown') + logger.info(f"Analytics upload successful: file={filename} request_id={request_id} account_number={account_number} org_id={org_id}") + except Exception: + logger.info(f"Analytics upload successful: file={filename} status={response.status_code}") + + def ship(path): """ Ship gathered metrics to the Insights API @@ -468,6 +480,7 @@ def ship(path): cert_url, files=files, cert=(cert_path, key_path), verify=settings.INSIGHTS_CERT_PATH, headers=s.headers, timeout=(31, 31) ) if response.status_code < 300: + _log_shipping_response(response, path) return True else: logger.warning( @@ -484,6 +497,7 @@ def ship(path): response = client.make_request("POST", url, headers=s.headers, files=files, verify=settings.INSIGHTS_CERT_PATH, timeout=(31, 31)) if response.status_code < 300: + _log_shipping_response(response, path) return True else: logger.error(f'OIDC authentication failed with status {response.status_code}, {response.text}') diff --git a/awx/main/tests/unit/analytics/test_core_ship.py b/awx/main/tests/unit/analytics/test_core_ship.py index a544860b35..14e5f78b9f 100644 --- a/awx/main/tests/unit/analytics/test_core_ship.py +++ b/awx/main/tests/unit/analytics/test_core_ship.py @@ -9,7 +9,7 @@ from unittest import mock from django.test.utils import override_settings -from awx.main.analytics.core import ship, _get_cert_upload_url +from awx.main.analytics.core import ship, _get_cert_upload_url, _log_shipping_response class TestGetCertUploadUrl: @@ -70,6 +70,7 @@ class TestShipMTLS: # Mock successful mTLS response mock_response = mock.Mock() mock_response.status_code = 200 + mock_response.json.return_value = {'request_id': 'abc-123', 'account_number': '12345', 'org_id': '67890'} mock_session = mock.Mock() mock_session.headers = {} mock_session.post.return_value = mock_response @@ -81,6 +82,7 @@ class TestShipMTLS: mock_get_cert.assert_called_once() mock_temp_files.assert_called_once_with('cert-pem-data', 'key-pem-data') mock_session.post.assert_called_once() + mock_response.json.assert_called_once() # Verify cert URL is used (cert. subdomain added) call_args = mock_session.post.call_args @@ -126,6 +128,7 @@ class TestShipMTLS: # Mock successful OIDC response mock_oidc_response = mock.Mock() mock_oidc_response.status_code = 200 + mock_oidc_response.json.return_value = {'request_id': 'oidc-456', 'account_number': '12345', 'org_id': '67890'} mock_oidc_instance = mock.Mock() mock_oidc_instance.make_request.return_value = mock_oidc_response mock_oidc_client.return_value = mock_oidc_instance @@ -172,6 +175,7 @@ class TestShipMTLS: # Mock successful OIDC response mock_oidc_response = mock.Mock() mock_oidc_response.status_code = 200 + mock_oidc_response.json.return_value = {'request_id': 'oidc-789', 'account_number': '12345', 'org_id': '67890'} mock_oidc_instance = mock.Mock() mock_oidc_instance.make_request.return_value = mock_oidc_response mock_oidc_client.return_value = mock_oidc_instance @@ -209,6 +213,7 @@ class TestShipMTLS: # Mock successful OIDC response mock_oidc_response = mock.Mock() mock_oidc_response.status_code = 200 + mock_oidc_response.json.return_value = {'request_id': 'oidc-no-cert', 'account_number': '12345', 'org_id': '67890'} mock_oidc_instance = mock.Mock() mock_oidc_instance.make_request.return_value = mock_oidc_response mock_oidc_client.return_value = mock_oidc_instance @@ -269,3 +274,34 @@ class TestShipMTLS: assert result is False mock_session.post.assert_called_once() mock_oidc_instance.make_request.assert_called_once() + + +class TestLogShippingResponse: + """Test _log_shipping_response() helper function.""" + + def test_logs_response_fields(self): + """Test that request_id, account_number, and org_id are logged.""" + response = mock.Mock() + response.json.return_value = {'request_id': 'req-abc', 'account_number': '99999', 'org_id': '11111'} + with mock.patch('awx.main.analytics.core.logger') as mock_logger: + _log_shipping_response(response, '/tmp/analytics.tar.gz') + mock_logger.info.assert_called_once_with("Analytics upload successful: file=analytics.tar.gz request_id=req-abc account_number=99999 org_id=11111") + + def test_logs_unknown_for_missing_fields(self): + """Test fallback to 'unknown' when response fields are absent.""" + response = mock.Mock() + response.json.return_value = {} + with mock.patch('awx.main.analytics.core.logger') as mock_logger: + _log_shipping_response(response, '/tmp/analytics.tar.gz') + mock_logger.info.assert_called_once_with( + "Analytics upload successful: file=analytics.tar.gz request_id=unknown account_number=unknown org_id=unknown" + ) + + def test_graceful_fallback_on_json_error(self): + """Test fallback log when response body is not valid JSON.""" + response = mock.Mock() + response.json.side_effect = ValueError("No JSON") + response.status_code = 202 + with mock.patch('awx.main.analytics.core.logger') as mock_logger: + _log_shipping_response(response, '/tmp/analytics.tar.gz') + mock_logger.info.assert_called_once_with("Analytics upload successful: file=analytics.tar.gz status=202")