1
1
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2
2
# SPDX-License-Identifier: Apache-2.0
3
3
4
+ import json
5
+ import logging
4
6
import unittest
5
- from unittest .mock import patch
6
7
from 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
7
13
8
14
from amazon .opentelemetry .distro .exporter .aws .metrics .console_emf_exporter import ConsoleEmfExporter
9
15
10
16
11
17
class TestConsoleEmfExporter (unittest .TestCase ):
12
18
"""Test ConsoleEmfExporter class."""
13
19
20
+ def setUp (self ):
21
+ """Set up test fixtures."""
22
+ self .exporter = ConsoleEmfExporter ()
23
+
14
24
def test_namespace_initialization (self ):
15
25
"""Test exporter initialization with different namespace scenarios."""
16
26
# Test default namespace
@@ -25,10 +35,37 @@ def test_namespace_initialization(self):
25
35
exporter = ConsoleEmfExporter (namespace = None )
26
36
self .assertEqual (exporter .namespace , "default" )
27
37
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 )
31
52
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."""
32
69
# Create a simple log event with EMF-formatted message
33
70
test_message = (
34
71
'{"_aws":{"Timestamp":1640995200000,"CloudWatchMetrics":[{"Namespace":"TestNamespace",'
@@ -39,12 +76,233 @@ def test_send_log_event(self):
39
76
40
77
# Capture stdout to verify the output
41
78
with patch ("sys.stdout" , new_callable = StringIO ) as mock_stdout :
42
- exporter ._export (log_event )
79
+ self . exporter ._export (log_event )
43
80
44
- # Verify the message was printed to stdout
81
+ # Verify the message was printed to stdout with flush
45
82
captured_output = mock_stdout .getvalue ().strip ()
46
83
self .assertEqual (captured_output , test_message )
47
84
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
+
48
306
49
307
if __name__ == "__main__" :
50
308
unittest .main ()
0 commit comments