Skip to content

Commit a80e98d

Browse files
committed
improve coverage
1 parent cae3f4b commit a80e98d

File tree

3 files changed

+378
-0
lines changed

3 files changed

+378
-0
lines changed

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/exporter/otlp/aws/logs/test_aws_cw_otlp_batch_log_record_processor.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,86 @@ def test_force_flush_exports_only_one_batch(self, _, __, ___):
272272
exported_batch = args[0]
273273
self.assertEqual(len(exported_batch), 5)
274274

275+
@patch(
276+
"amazon.opentelemetry.distro.exporter.otlp.aws.logs._aws_cw_otlp_batch_log_record_processor.attach",
277+
return_value=MagicMock(),
278+
)
279+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.logs._aws_cw_otlp_batch_log_record_processor.detach")
280+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.logs._aws_cw_otlp_batch_log_record_processor.set_value")
281+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.logs._aws_cw_otlp_batch_log_record_processor._logger")
282+
def test_export_handles_exception_gracefully(self, mock_logger, _, __, ___):
283+
"""Tests that exceptions during export are caught and logged"""
284+
# Setup exporter to raise an exception
285+
self.mock_exporter.export.side_effect = Exception("Export failed")
286+
287+
# Add logs to queue
288+
test_logs = self.generate_test_log_data(log_body="test message", count=2)
289+
for log in test_logs:
290+
self.processor._queue.appendleft(log)
291+
292+
# Call _export - should not raise exception
293+
self.processor._export(batch_strategy=BatchLogExportStrategy.EXPORT_ALL)
294+
295+
# Verify exception was logged
296+
mock_logger.exception.assert_called_once()
297+
call_args = mock_logger.exception.call_args[0]
298+
self.assertIn("Exception while exporting logs:", call_args[0])
299+
300+
# Queue should be empty even though export failed
301+
self.assertEqual(len(self.processor._queue), 0)
302+
303+
@patch("amazon.opentelemetry.distro.exporter.otlp.aws.logs._aws_cw_otlp_batch_log_record_processor._logger")
304+
def test_estimate_log_size_debug_logging_on_depth_exceeded(self, mock_logger):
305+
"""Tests that debug logging occurs when depth limit is exceeded"""
306+
# Create deeply nested structure that exceeds depth limit
307+
depth_limit = 1
308+
log_body = {"level1": {"level2": {"level3": {"level4": "this should trigger debug log"}}}}
309+
310+
test_logs = self.generate_test_log_data(log_body=log_body, count=1)
311+
312+
# Call with limited depth that will be exceeded
313+
self.processor._estimate_log_size(log=test_logs[0], depth=depth_limit)
314+
315+
# Verify debug logging was called
316+
mock_logger.debug.assert_called_once()
317+
call_args = mock_logger.debug.call_args[0]
318+
self.assertIn("Max log depth of", call_args[0])
319+
self.assertIn("exceeded", call_args[0])
320+
321+
def test_estimate_utf8_size_static_method(self):
322+
"""Tests the _estimate_utf8_size static method with various strings"""
323+
# Test ASCII only string
324+
ascii_result = AwsCloudWatchOtlpBatchLogRecordProcessor._estimate_utf8_size("hello")
325+
self.assertEqual(ascii_result, 5) # 5 ASCII chars = 5 bytes
326+
327+
# Test mixed ASCII and non-ASCII
328+
mixed_result = AwsCloudWatchOtlpBatchLogRecordProcessor._estimate_utf8_size("café")
329+
self.assertEqual(mixed_result, 7) # 3 ASCII + 1 non-ASCII (4 bytes) = 7 bytes
330+
331+
# Test non-ASCII only
332+
non_ascii_result = AwsCloudWatchOtlpBatchLogRecordProcessor._estimate_utf8_size("深入")
333+
self.assertEqual(non_ascii_result, 8) # 2 non-ASCII chars * 4 bytes = 8 bytes
334+
335+
# Test empty string
336+
empty_result = AwsCloudWatchOtlpBatchLogRecordProcessor._estimate_utf8_size("")
337+
self.assertEqual(empty_result, 0)
338+
339+
def test_constructor_with_custom_parameters(self):
340+
"""Tests constructor with custom parameters"""
341+
custom_processor = AwsCloudWatchOtlpBatchLogRecordProcessor(
342+
exporter=self.mock_exporter,
343+
schedule_delay_millis=5000,
344+
max_export_batch_size=100,
345+
export_timeout_millis=10000,
346+
max_queue_size=2000,
347+
)
348+
349+
# Verify exporter is stored
350+
self.assertEqual(custom_processor._exporter, self.mock_exporter)
351+
352+
# Verify parameters are passed to parent constructor
353+
self.assertEqual(custom_processor._max_export_batch_size, 100)
354+
275355
@staticmethod
276356
def generate_test_log_data(
277357
log_body,

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/sampler/test_aws_xray_remote_sampler.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,143 @@ def test_non_parent_based_xray_sampler_updates_statistics_thrice_for_one_parent_
304304

305305
non_parent_based_xray_sampler._rules_timer.cancel()
306306
non_parent_based_xray_sampler._targets_timer.cancel()
307+
308+
def test_create_remote_sampler_with_none_resource(self):
309+
"""Tests creating remote sampler with None resource"""
310+
with patch("amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler._logger") as mock_logger:
311+
self.rs = AwsXRayRemoteSampler(resource=None)
312+
313+
# Verify warning was logged for None resource
314+
mock_logger.warning.assert_called_once_with(
315+
"OTel Resource provided is `None`. Defaulting to empty resource"
316+
)
317+
318+
# Verify empty resource was set
319+
self.assertIsNotNone(self.rs._root._root._AwsXRayRemoteSampler__resource)
320+
self.assertEqual(len(self.rs._root._root._AwsXRayRemoteSampler__resource.attributes), 0)
321+
322+
def test_create_remote_sampler_with_small_polling_interval(self):
323+
"""Tests creating remote sampler with polling interval < 10"""
324+
with patch("amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler._logger") as mock_logger:
325+
self.rs = AwsXRayRemoteSampler(resource=Resource.get_empty(), polling_interval=5) # Less than 10
326+
327+
# Verify info log was called for small polling interval
328+
mock_logger.info.assert_any_call("`polling_interval` is `None` or too small. Defaulting to %s", 300)
329+
330+
# Verify default polling interval was set
331+
self.assertEqual(self.rs._root._root._AwsXRayRemoteSampler__polling_interval, 300)
332+
333+
def test_create_remote_sampler_with_none_endpoint(self):
334+
"""Tests creating remote sampler with None endpoint"""
335+
with patch("amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler._logger") as mock_logger:
336+
self.rs = AwsXRayRemoteSampler(resource=Resource.get_empty(), endpoint=None)
337+
338+
# Verify info log was called for None endpoint
339+
mock_logger.info.assert_any_call("`endpoint` is `None`. Defaulting to %s", "http://127.0.0.1:2000")
340+
341+
@patch("requests.Session.post", side_effect=mocked_requests_get)
342+
def test_should_sample_with_expired_rule_cache(self, mock_post=None):
343+
"""Tests should_sample behavior when rule cache is expired"""
344+
self.rs = AwsXRayRemoteSampler(resource=Resource.get_empty())
345+
346+
# Mock rule cache to be expired
347+
with patch.object(self.rs._root._root._AwsXRayRemoteSampler__rule_cache, "expired", return_value=True):
348+
with patch("amazon.opentelemetry.distro.sampler.aws_xray_remote_sampler._logger") as mock_logger:
349+
# Call should_sample when cache is expired
350+
result = self.rs._root._root.should_sample(None, 0, "test_span")
351+
352+
# Verify debug log was called
353+
mock_logger.debug.assert_called_once_with("Rule cache is expired so using fallback sampling strategy")
354+
355+
# Verify fallback sampler was used (should return some result)
356+
self.assertIsNotNone(result)
357+
358+
@patch("requests.Session.post", side_effect=mocked_requests_get)
359+
def test_refresh_rules_when_targets_require_it(self, mock_post=None):
360+
"""Tests that sampling rules are refreshed when targets polling indicates it"""
361+
self.rs = AwsXRayRemoteSampler(resource=Resource.get_empty())
362+
363+
# Mock the rule cache update_sampling_targets to return refresh_rules=True
364+
with patch.object(
365+
self.rs._root._root._AwsXRayRemoteSampler__rule_cache,
366+
"update_sampling_targets",
367+
return_value=(True, None), # refresh_rules=True, min_polling_interval=None
368+
):
369+
# Mock get_and_update_sampling_rules to track if it was called
370+
with patch.object(
371+
self.rs._root._root, "_AwsXRayRemoteSampler__get_and_update_sampling_rules"
372+
) as mock_update_rules:
373+
# Call the method that should trigger rule refresh
374+
self.rs._root._root._AwsXRayRemoteSampler__get_and_update_sampling_targets()
375+
376+
# Verify that rules were refreshed
377+
mock_update_rules.assert_called_once()
378+
379+
@patch("requests.Session.post", side_effect=mocked_requests_get)
380+
def test_update_target_polling_interval(self, mock_post=None):
381+
"""Tests that target polling interval is updated when targets polling returns new interval"""
382+
self.rs = AwsXRayRemoteSampler(resource=Resource.get_empty())
383+
384+
# Mock the rule cache update_sampling_targets to return new polling interval
385+
new_interval = 500
386+
with patch.object(
387+
self.rs._root._root._AwsXRayRemoteSampler__rule_cache,
388+
"update_sampling_targets",
389+
return_value=(False, new_interval), # refresh_rules=False, min_polling_interval=500
390+
):
391+
# Store original interval
392+
original_interval = self.rs._root._root._AwsXRayRemoteSampler__target_polling_interval
393+
394+
# Call the method that should update polling interval
395+
self.rs._root._root._AwsXRayRemoteSampler__get_and_update_sampling_targets()
396+
397+
# Verify that polling interval was updated
398+
self.assertEqual(self.rs._root._root._AwsXRayRemoteSampler__target_polling_interval, new_interval)
399+
self.assertNotEqual(original_interval, new_interval)
400+
401+
def test_generate_client_id_format(self):
402+
"""Tests that client ID generation produces correctly formatted hex string"""
403+
self.rs = AwsXRayRemoteSampler(resource=Resource.get_empty())
404+
client_id = self.rs._root._root._AwsXRayRemoteSampler__client_id
405+
406+
# Verify client ID is 24 characters long
407+
self.assertEqual(len(client_id), 24)
408+
409+
# Verify all characters are valid hex characters
410+
valid_hex_chars = set("0123456789abcdef")
411+
for char in client_id:
412+
self.assertIn(char, valid_hex_chars)
413+
414+
def test_internal_sampler_get_description(self):
415+
"""Tests get_description method of internal _AwsXRayRemoteSampler"""
416+
internal_sampler = _AwsXRayRemoteSampler(resource=Resource.get_empty())
417+
418+
try:
419+
description = internal_sampler.get_description()
420+
self.assertEqual(description, "_AwsXRayRemoteSampler{remote sampling with AWS X-Ray}")
421+
finally:
422+
# Clean up timers
423+
internal_sampler._rules_timer.cancel()
424+
internal_sampler._targets_timer.cancel()
425+
426+
@patch("requests.Session.post", side_effect=mocked_requests_get)
427+
def test_rule_and_target_pollers_start_correctly(self, mock_post=None):
428+
"""Tests that both rule and target pollers are started and configured correctly"""
429+
self.rs = AwsXRayRemoteSampler(resource=Resource.get_empty())
430+
431+
# Verify timers are created and started
432+
self.assertIsNotNone(self.rs._root._root._rules_timer)
433+
self.assertIsNotNone(self.rs._root._root._targets_timer)
434+
435+
# Verify timers are daemon threads
436+
self.assertTrue(self.rs._root._root._rules_timer.daemon)
437+
self.assertTrue(self.rs._root._root._targets_timer.daemon)
438+
439+
# Verify jitter values are within expected ranges
440+
rule_jitter = self.rs._root._root._AwsXRayRemoteSampler__rule_polling_jitter
441+
target_jitter = self.rs._root._root._AwsXRayRemoteSampler__target_polling_jitter
442+
443+
self.assertGreaterEqual(rule_jitter, 0.0)
444+
self.assertLessEqual(rule_jitter, 5.0)
445+
self.assertGreaterEqual(target_jitter, 0.0)
446+
self.assertLessEqual(target_jitter, 0.1)

aws-opentelemetry-distro/tests/amazon/opentelemetry/distro/sampler/test_aws_xray_sampling_client.py

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,161 @@ def test_urls_excluded_from_sampling(self):
238238

239239
URLLib3Instrumentor().uninstrument()
240240
RequestsInstrumentor().uninstrument()
241+
242+
def test_constructor_with_none_endpoint(self):
243+
"""Tests constructor behavior when endpoint is None"""
244+
with self.assertLogs(_sampling_client_logger, level="ERROR") as cm:
245+
# Constructor will log error but then crash on concatenation
246+
with self.assertRaises(TypeError):
247+
_AwsXRaySamplingClient(endpoint=None)
248+
249+
# Verify error log was called before the crash
250+
self.assertIn("endpoint must be specified", cm.output[0])
251+
252+
def test_constructor_with_log_level(self):
253+
"""Tests constructor sets log level when specified"""
254+
original_level = _sampling_client_logger.level
255+
try:
256+
_AwsXRaySamplingClient("http://test.com", log_level=logging.DEBUG)
257+
self.assertEqual(_sampling_client_logger.level, logging.DEBUG)
258+
finally:
259+
# Reset log level
260+
_sampling_client_logger.setLevel(original_level)
261+
262+
@patch("requests.Session.post")
263+
def test_get_sampling_rules_none_response(self, mock_post):
264+
"""Tests get_sampling_rules when response is None"""
265+
mock_post.return_value = None
266+
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
267+
268+
with self.assertLogs(_sampling_client_logger, level="ERROR") as cm:
269+
sampling_rules = client.get_sampling_rules()
270+
271+
# Verify error log and empty result
272+
self.assertIn("GetSamplingRules response is None", cm.output[0])
273+
self.assertEqual(len(sampling_rules), 0)
274+
275+
@patch("requests.Session.post")
276+
def test_get_sampling_rules_request_exception(self, mock_post):
277+
"""Tests get_sampling_rules when RequestException occurs"""
278+
mock_post.side_effect = requests.exceptions.RequestException("Connection error")
279+
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
280+
281+
with self.assertLogs(_sampling_client_logger, level="ERROR") as cm:
282+
sampling_rules = client.get_sampling_rules()
283+
284+
# Verify error log and empty result
285+
self.assertIn("Request error occurred", cm.output[0])
286+
self.assertIn("Connection error", cm.output[0])
287+
self.assertEqual(len(sampling_rules), 0)
288+
289+
@patch("requests.Session.post")
290+
def test_get_sampling_rules_json_decode_error(self, mock_post):
291+
"""Tests get_sampling_rules when JSON decode error occurs"""
292+
# Mock response that raises JSONDecodeError when .json() is called
293+
mock_response = mock_post.return_value
294+
mock_response.json.side_effect = json.JSONDecodeError("Invalid JSON", "doc", 0)
295+
296+
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
297+
298+
with self.assertLogs(_sampling_client_logger, level="ERROR") as cm:
299+
sampling_rules = client.get_sampling_rules()
300+
301+
# Verify error log and empty result
302+
self.assertIn("Error in decoding JSON response", cm.output[0])
303+
self.assertEqual(len(sampling_rules), 0)
304+
305+
@patch("requests.Session.post")
306+
def test_get_sampling_rules_general_exception(self, mock_post):
307+
"""Tests get_sampling_rules when general exception occurs"""
308+
mock_post.side_effect = Exception("Unexpected error")
309+
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
310+
311+
with self.assertLogs(_sampling_client_logger, level="ERROR") as cm:
312+
sampling_rules = client.get_sampling_rules()
313+
314+
# Verify error log and empty result
315+
self.assertIn("Error occurred when attempting to fetch rules", cm.output[0])
316+
self.assertIn("Unexpected error", cm.output[0])
317+
self.assertEqual(len(sampling_rules), 0)
318+
319+
@patch("requests.Session.post")
320+
def test_get_sampling_targets_none_response(self, mock_post):
321+
"""Tests get_sampling_targets when response is None"""
322+
mock_post.return_value = None
323+
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
324+
325+
with self.assertLogs(_sampling_client_logger, level="DEBUG") as cm:
326+
response = client.get_sampling_targets([])
327+
328+
# Verify debug log and default response
329+
self.assertIn("GetSamplingTargets response is None", cm.output[0])
330+
self.assertEqual(response.SamplingTargetDocuments, [])
331+
self.assertEqual(response.UnprocessedStatistics, [])
332+
self.assertEqual(response.LastRuleModification, 0.0)
333+
334+
@patch("requests.Session.post")
335+
def test_get_sampling_targets_invalid_response_format(self, mock_post):
336+
"""Tests get_sampling_targets when response format is invalid"""
337+
# Missing required fields
338+
mock_post.return_value.configure_mock(**{"json.return_value": {"InvalidField": "value"}})
339+
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
340+
341+
with self.assertLogs(_sampling_client_logger, level="DEBUG") as cm:
342+
response = client.get_sampling_targets([])
343+
344+
# Verify debug log and default response
345+
self.assertIn("getSamplingTargets response is invalid", cm.output[0])
346+
self.assertEqual(response.SamplingTargetDocuments, [])
347+
self.assertEqual(response.UnprocessedStatistics, [])
348+
self.assertEqual(response.LastRuleModification, 0.0)
349+
350+
@patch("requests.Session.post")
351+
def test_get_sampling_targets_request_exception(self, mock_post):
352+
"""Tests get_sampling_targets when RequestException occurs"""
353+
mock_post.side_effect = requests.exceptions.RequestException("Network error")
354+
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
355+
356+
with self.assertLogs(_sampling_client_logger, level="DEBUG") as cm:
357+
response = client.get_sampling_targets([])
358+
359+
# Verify debug log and default response
360+
self.assertIn("Request error occurred", cm.output[0])
361+
self.assertIn("Network error", cm.output[0])
362+
self.assertEqual(response.SamplingTargetDocuments, [])
363+
self.assertEqual(response.UnprocessedStatistics, [])
364+
self.assertEqual(response.LastRuleModification, 0.0)
365+
366+
@patch("requests.Session.post")
367+
def test_get_sampling_targets_json_decode_error(self, mock_post):
368+
"""Tests get_sampling_targets when JSON decode error occurs"""
369+
# Mock response that raises JSONDecodeError when .json() is called
370+
mock_response = mock_post.return_value
371+
mock_response.json.side_effect = json.JSONDecodeError("Invalid JSON", "doc", 0)
372+
373+
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
374+
375+
with self.assertLogs(_sampling_client_logger, level="DEBUG") as cm:
376+
response = client.get_sampling_targets([])
377+
378+
# Verify debug log and default response
379+
self.assertIn("Error in decoding JSON response", cm.output[0])
380+
self.assertEqual(response.SamplingTargetDocuments, [])
381+
self.assertEqual(response.UnprocessedStatistics, [])
382+
self.assertEqual(response.LastRuleModification, 0.0)
383+
384+
@patch("requests.Session.post")
385+
def test_get_sampling_targets_general_exception(self, mock_post):
386+
"""Tests get_sampling_targets when general exception occurs"""
387+
mock_post.side_effect = Exception("Unexpected error")
388+
client = _AwsXRaySamplingClient("http://127.0.0.1:2000")
389+
390+
with self.assertLogs(_sampling_client_logger, level="DEBUG") as cm:
391+
response = client.get_sampling_targets([])
392+
393+
# Verify debug log and default response
394+
self.assertIn("Error occurred when attempting to fetch targets", cm.output[0])
395+
self.assertIn("Unexpected error", cm.output[0])
396+
self.assertEqual(response.SamplingTargetDocuments, [])
397+
self.assertEqual(response.UnprocessedStatistics, [])
398+
self.assertEqual(response.LastRuleModification, 0.0)

0 commit comments

Comments
 (0)