11# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22# SPDX-License-Identifier: Apache-2.0
33
4+ import json
5+ import logging
46import unittest
5- from unittest .mock import patch
67from io import StringIO
8+ from unittest .mock import MagicMock , patch
9+
10+ from opentelemetry .sdk .metrics import Counter , MeterProvider
11+ from opentelemetry .sdk .metrics .export import AggregationTemporality , MetricsData
12+ from opentelemetry .sdk .resources import Resource
713
814from amazon .opentelemetry .distro .exporter .aws .metrics .console_emf_exporter import ConsoleEmfExporter
915
1016
1117class TestConsoleEmfExporter (unittest .TestCase ):
1218 """Test ConsoleEmfExporter class."""
1319
20+ def setUp (self ):
21+ """Set up test fixtures."""
22+ self .exporter = ConsoleEmfExporter ()
23+
1424 def test_namespace_initialization (self ):
1525 """Test exporter initialization with different namespace scenarios."""
1626 # Test default namespace
@@ -25,10 +35,37 @@ def test_namespace_initialization(self):
2535 exporter = ConsoleEmfExporter (namespace = None )
2636 self .assertEqual (exporter .namespace , "default" )
2737
28- def test_send_log_event (self ):
29- """Test that log events are properly sent to console output."""
30- exporter = ConsoleEmfExporter ()
38+ # Test empty string namespace (should remain empty)
39+ exporter = ConsoleEmfExporter (namespace = "" )
40+ self .assertEqual (exporter .namespace , "" )
41+
42+ def test_initialization_with_parameters (self ):
43+ """Test exporter initialization with optional parameters."""
44+ # Test with preferred_temporality
45+ preferred_temporality = {Counter : AggregationTemporality .CUMULATIVE }
46+ exporter = ConsoleEmfExporter (
47+ namespace = "TestNamespace" ,
48+ preferred_temporality = preferred_temporality
49+ )
50+ self .assertEqual (exporter .namespace , "TestNamespace" )
51+ self .assertEqual (exporter ._preferred_temporality [Counter ], AggregationTemporality .CUMULATIVE )
3152
53+ # Test with preferred_aggregation
54+ preferred_aggregation = {Counter : "TestAggregation" }
55+ exporter = ConsoleEmfExporter (
56+ preferred_aggregation = preferred_aggregation
57+ )
58+ self .assertEqual (exporter ._preferred_aggregation [Counter ], "TestAggregation" )
59+
60+ # Test with additional kwargs
61+ exporter = ConsoleEmfExporter (
62+ namespace = "TestNamespace" ,
63+ extra_param = "ignored" # Should be ignored
64+ )
65+ self .assertEqual (exporter .namespace , "TestNamespace" )
66+
67+ def test_export_log_event_success (self ):
68+ """Test that log events are properly sent to console output."""
3269 # Create a simple log event with EMF-formatted message
3370 test_message = (
3471 '{"_aws":{"Timestamp":1640995200000,"CloudWatchMetrics":[{"Namespace":"TestNamespace",'
@@ -39,12 +76,233 @@ def test_send_log_event(self):
3976
4077 # Capture stdout to verify the output
4178 with patch ("sys.stdout" , new_callable = StringIO ) as mock_stdout :
42- exporter ._export (log_event )
79+ self . exporter ._export (log_event )
4380
44- # Verify the message was printed to stdout
81+ # Verify the message was printed to stdout with flush
4582 captured_output = mock_stdout .getvalue ().strip ()
4683 self .assertEqual (captured_output , test_message )
4784
85+ def test_export_log_event_empty_message (self ):
86+ """Test handling of log events with empty messages."""
87+ log_event = {"message" : "" , "timestamp" : 1640995200000 }
88+
89+ with patch ("sys.stdout" , new_callable = StringIO ) as mock_stdout :
90+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
91+ self .exporter ._export (log_event )
92+
93+ # Should not print anything for empty message
94+ captured_output = mock_stdout .getvalue ().strip ()
95+ self .assertEqual (captured_output , "" )
96+
97+ # Should log a warning
98+ mock_logger .warning .assert_called_once ()
99+
100+ def test_export_log_event_missing_message (self ):
101+ """Test handling of log events without message key."""
102+ log_event = {"timestamp" : 1640995200000 }
103+
104+ with patch ("sys.stdout" , new_callable = StringIO ) as mock_stdout :
105+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
106+ self .exporter ._export (log_event )
107+
108+ # Should not print anything when message is missing
109+ captured_output = mock_stdout .getvalue ().strip ()
110+ self .assertEqual (captured_output , "" )
111+
112+ # Should log a warning
113+ mock_logger .warning .assert_called_once ()
114+
115+ def test_export_log_event_with_none_message (self ):
116+ """Test handling of log events with None message."""
117+ log_event = {"message" : None , "timestamp" : 1640995200000 }
118+
119+ with patch ("sys.stdout" , new_callable = StringIO ) as mock_stdout :
120+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
121+ self .exporter ._export (log_event )
122+
123+ # Should not print anything when message is None
124+ captured_output = mock_stdout .getvalue ().strip ()
125+ self .assertEqual (captured_output , "" )
126+
127+ # Should log a warning
128+ mock_logger .warning .assert_called_once ()
129+
130+ def test_export_log_event_print_exception (self ):
131+ """Test error handling when print() raises an exception."""
132+ log_event = {"message" : "test message" , "timestamp" : 1640995200000 }
133+
134+ with patch ("builtins.print" , side_effect = Exception ("Print failed" )):
135+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
136+ self .exporter ._export (log_event )
137+
138+ # Should log the error
139+ mock_logger .error .assert_called_once ()
140+ args = mock_logger .error .call_args [0 ]
141+ self .assertIn ("Failed to write EMF log to console" , args [0 ])
142+ self .assertEqual (args [1 ], log_event )
143+ self .assertIn ("Print failed" , str (args [2 ]))
144+
145+ def test_export_log_event_various_message_types (self ):
146+ """Test _export with various message types."""
147+ # Test with JSON string
148+ json_message = '{"key": "value"}'
149+ log_event = {"message" : json_message , "timestamp" : 1640995200000 }
150+
151+ with patch ("sys.stdout" , new_callable = StringIO ) as mock_stdout :
152+ self .exporter ._export (log_event )
153+
154+ captured_output = mock_stdout .getvalue ().strip ()
155+ self .assertEqual (captured_output , json_message )
156+
157+ # Test with plain string
158+ plain_message = "Simple log message"
159+ log_event = {"message" : plain_message , "timestamp" : 1640995200000 }
160+
161+ with patch ("sys.stdout" , new_callable = StringIO ) as mock_stdout :
162+ self .exporter ._export (log_event )
163+
164+ captured_output = mock_stdout .getvalue ().strip ()
165+ self .assertEqual (captured_output , plain_message )
166+
167+ def test_force_flush (self ):
168+ """Test force_flush method."""
169+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
170+ # Test with default timeout
171+ result = self .exporter .force_flush ()
172+ self .assertTrue (result )
173+ mock_logger .debug .assert_called_once ()
174+
175+ # Reset mock for next call
176+ mock_logger .reset_mock ()
177+
178+ # Test with custom timeout
179+ result = self .exporter .force_flush (timeout_millis = 5000 )
180+ self .assertTrue (result )
181+ mock_logger .debug .assert_called_once ()
182+
183+ def test_shutdown (self ):
184+ """Test shutdown method."""
185+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
186+ # Test with no timeout
187+ result = self .exporter .shutdown ()
188+ self .assertTrue (result )
189+ mock_logger .debug .assert_called_once_with (
190+ "ConsoleEmfExporter shutdown called with timeout_millis=%s" , None
191+ )
192+
193+ # Reset mock for next call
194+ mock_logger .reset_mock ()
195+
196+ # Test with timeout
197+ result = self .exporter .shutdown (timeout_millis = 3000 )
198+ self .assertTrue (result )
199+ mock_logger .debug .assert_called_once_with (
200+ "ConsoleEmfExporter shutdown called with timeout_millis=%s" , 3000
201+ )
202+
203+ # Reset mock for next call
204+ mock_logger .reset_mock ()
205+
206+ # Test with additional kwargs
207+ result = self .exporter .shutdown (timeout_millis = 3000 , extra_arg = "ignored" )
208+ self .assertTrue (result )
209+ mock_logger .debug .assert_called_once ()
210+
211+ def test_integration_with_metrics_data (self ):
212+ """Test the full export flow with actual MetricsData."""
213+ # Create a mock MetricsData
214+ mock_metrics_data = MagicMock (spec = MetricsData )
215+ mock_resource_metrics = MagicMock ()
216+ mock_scope_metrics = MagicMock ()
217+ mock_metric = MagicMock ()
218+
219+ # Set up the mock hierarchy
220+ mock_metrics_data .resource_metrics = [mock_resource_metrics ]
221+ mock_resource_metrics .scope_metrics = [mock_scope_metrics ]
222+ mock_scope_metrics .metrics = [mock_metric ]
223+
224+ # Mock the metric to have no data_points to avoid complex setup
225+ mock_metric .data = None
226+
227+ with patch ("sys.stdout" , new_callable = StringIO ):
228+ result = self .exporter .export (mock_metrics_data )
229+
230+ # Should succeed even with no actual metrics
231+ from opentelemetry .sdk .metrics .export import MetricExportResult
232+ self .assertEqual (result , MetricExportResult .SUCCESS )
233+
234+ def test_integration_export_success (self ):
235+ """Test export method returns success."""
236+ # Create empty MetricsData
237+ mock_metrics_data = MagicMock (spec = MetricsData )
238+ mock_metrics_data .resource_metrics = []
239+
240+ from opentelemetry .sdk .metrics .export import MetricExportResult
241+ result = self .exporter .export (mock_metrics_data )
242+ self .assertEqual (result , MetricExportResult .SUCCESS )
243+
244+ def test_integration_export_with_timeout (self ):
245+ """Test export method with timeout parameter."""
246+ mock_metrics_data = MagicMock (spec = MetricsData )
247+ mock_metrics_data .resource_metrics = []
248+
249+ from opentelemetry .sdk .metrics .export import MetricExportResult
250+ result = self .exporter .export (mock_metrics_data , timeout_millis = 5000 )
251+ self .assertEqual (result , MetricExportResult .SUCCESS )
252+
253+ def test_export_failure_handling (self ):
254+ """Test export method handles exceptions and returns failure."""
255+ # Create a mock that raises an exception
256+ mock_metrics_data = MagicMock (spec = MetricsData )
257+ mock_metrics_data .resource_metrics = [MagicMock ()]
258+
259+ # Make the resource_metrics access raise an exception
260+ type(mock_metrics_data ).resource_metrics = property (
261+ lambda self : (_ for _ in ()).throw (Exception ("Test exception" ))
262+ )
263+
264+ # Patch the logger in the base_emf_exporter since that's where the error logging happens
265+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.base_emf_exporter.logger" ) as mock_logger :
266+ from opentelemetry .sdk .metrics .export import MetricExportResult
267+ result = self .exporter .export (mock_metrics_data )
268+
269+ self .assertEqual (result , MetricExportResult .FAILURE )
270+ mock_logger .error .assert_called_once ()
271+ self .assertIn ("Failed to export metrics" , mock_logger .error .call_args [0 ][0 ])
272+
273+ def test_flush_output_verification (self ):
274+ """Test that print is called with flush=True."""
275+ log_event = {"message" : "test message" , "timestamp" : 1640995200000 }
276+
277+ with patch ("builtins.print" ) as mock_print :
278+ self .exporter ._export (log_event )
279+
280+ # Verify print was called with flush=True
281+ mock_print .assert_called_once_with ("test message" , flush = True )
282+
283+ def test_logger_levels (self ):
284+ """Test that appropriate log levels are used."""
285+ # Test debug logging in force_flush
286+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
287+ self .exporter .force_flush ()
288+ mock_logger .debug .assert_called_once ()
289+
290+ # Test debug logging in shutdown
291+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
292+ self .exporter .shutdown ()
293+ mock_logger .debug .assert_called_once ()
294+
295+ # Test warning logging for empty message
296+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
297+ self .exporter ._export ({"message" : "" , "timestamp" : 123 })
298+ mock_logger .warning .assert_called_once ()
299+
300+ # Test error logging for exception
301+ with patch ("builtins.print" , side_effect = Exception ("Test" )):
302+ with patch ("amazon.opentelemetry.distro.exporter.aws.metrics.console_emf_exporter.logger" ) as mock_logger :
303+ self .exporter ._export ({"message" : "test" , "timestamp" : 123 })
304+ mock_logger .error .assert_called_once ()
305+
48306
49307if __name__ == "__main__" :
50308 unittest .main ()
0 commit comments