@cdklabs/cdk-appmod-catalog-blueprints 1.4.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.jsii +2579 -194
  2. package/lib/document-processing/adapter/adapter.d.ts +4 -2
  3. package/lib/document-processing/adapter/adapter.js +1 -1
  4. package/lib/document-processing/adapter/queued-s3-adapter.d.ts +9 -2
  5. package/lib/document-processing/adapter/queued-s3-adapter.js +29 -15
  6. package/lib/document-processing/agentic-document-processing.d.ts +4 -0
  7. package/lib/document-processing/agentic-document-processing.js +20 -10
  8. package/lib/document-processing/base-document-processing.d.ts +54 -2
  9. package/lib/document-processing/base-document-processing.js +136 -82
  10. package/lib/document-processing/bedrock-document-processing.d.ts +202 -2
  11. package/lib/document-processing/bedrock-document-processing.js +717 -77
  12. package/lib/document-processing/chunking-config.d.ts +614 -0
  13. package/lib/document-processing/chunking-config.js +5 -0
  14. package/lib/document-processing/default-document-processing-config.js +1 -1
  15. package/lib/document-processing/index.d.ts +1 -0
  16. package/lib/document-processing/index.js +2 -1
  17. package/lib/document-processing/resources/aggregation/handler.py +567 -0
  18. package/lib/document-processing/resources/aggregation/requirements.txt +7 -0
  19. package/lib/document-processing/resources/aggregation/test_handler.py +362 -0
  20. package/lib/document-processing/resources/cleanup/handler.py +276 -0
  21. package/lib/document-processing/resources/cleanup/requirements.txt +5 -0
  22. package/lib/document-processing/resources/cleanup/test_handler.py +436 -0
  23. package/lib/document-processing/resources/default-bedrock-invoke/index.py +85 -3
  24. package/lib/document-processing/resources/default-bedrock-invoke/test_index.py +622 -0
  25. package/lib/document-processing/resources/pdf-chunking/README.md +313 -0
  26. package/lib/document-processing/resources/pdf-chunking/chunking_strategies.py +460 -0
  27. package/lib/document-processing/resources/pdf-chunking/error_handling.py +491 -0
  28. package/lib/document-processing/resources/pdf-chunking/handler.py +958 -0
  29. package/lib/document-processing/resources/pdf-chunking/metrics.py +435 -0
  30. package/lib/document-processing/resources/pdf-chunking/requirements.txt +3 -0
  31. package/lib/document-processing/resources/pdf-chunking/strategy_selection.py +420 -0
  32. package/lib/document-processing/resources/pdf-chunking/structured_logging.py +457 -0
  33. package/lib/document-processing/resources/pdf-chunking/test_chunking_strategies.py +353 -0
  34. package/lib/document-processing/resources/pdf-chunking/test_error_handling.py +487 -0
  35. package/lib/document-processing/resources/pdf-chunking/test_handler.py +609 -0
  36. package/lib/document-processing/resources/pdf-chunking/test_integration.py +694 -0
  37. package/lib/document-processing/resources/pdf-chunking/test_metrics.py +532 -0
  38. package/lib/document-processing/resources/pdf-chunking/test_strategy_selection.py +471 -0
  39. package/lib/document-processing/resources/pdf-chunking/test_structured_logging.py +449 -0
  40. package/lib/document-processing/resources/pdf-chunking/test_token_estimation.py +374 -0
  41. package/lib/document-processing/resources/pdf-chunking/token_estimation.py +189 -0
  42. package/lib/document-processing/tests/agentic-document-processing-nag.test.js +4 -3
  43. package/lib/document-processing/tests/agentic-document-processing.test.js +488 -4
  44. package/lib/document-processing/tests/base-document-processing-nag.test.js +9 -2
  45. package/lib/document-processing/tests/base-document-processing-schema.test.d.ts +1 -0
  46. package/lib/document-processing/tests/base-document-processing-schema.test.js +337 -0
  47. package/lib/document-processing/tests/base-document-processing.test.js +114 -8
  48. package/lib/document-processing/tests/bedrock-document-processing-chunking-nag.test.d.ts +1 -0
  49. package/lib/document-processing/tests/bedrock-document-processing-chunking-nag.test.js +382 -0
  50. package/lib/document-processing/tests/bedrock-document-processing-nag.test.js +4 -3
  51. package/lib/document-processing/tests/bedrock-document-processing-security.test.d.ts +1 -0
  52. package/lib/document-processing/tests/bedrock-document-processing-security.test.js +389 -0
  53. package/lib/document-processing/tests/bedrock-document-processing.test.js +808 -8
  54. package/lib/document-processing/tests/chunking-config.test.d.ts +1 -0
  55. package/lib/document-processing/tests/chunking-config.test.js +238 -0
  56. package/lib/document-processing/tests/queued-s3-adapter-nag.test.js +9 -2
  57. package/lib/document-processing/tests/queued-s3-adapter.test.js +17 -6
  58. package/lib/framework/agents/base-agent.js +1 -1
  59. package/lib/framework/agents/batch-agent.js +1 -1
  60. package/lib/framework/agents/default-agent-config.js +1 -1
  61. package/lib/framework/bedrock/bedrock.js +1 -1
  62. package/lib/framework/custom-resource/default-runtimes.js +1 -1
  63. package/lib/framework/foundation/access-log.js +1 -1
  64. package/lib/framework/foundation/eventbridge-broker.js +1 -1
  65. package/lib/framework/foundation/network.d.ts +4 -2
  66. package/lib/framework/foundation/network.js +52 -41
  67. package/lib/framework/tests/access-log.test.js +5 -2
  68. package/lib/framework/tests/batch-agent.test.js +5 -2
  69. package/lib/framework/tests/bedrock.test.js +5 -2
  70. package/lib/framework/tests/eventbridge-broker.test.js +5 -2
  71. package/lib/framework/tests/framework-nag.test.js +26 -7
  72. package/lib/framework/tests/network.test.js +30 -2
  73. package/lib/tsconfig.tsbuildinfo +1 -1
  74. package/lib/utilities/data-loader.js +1 -1
  75. package/lib/utilities/lambda-iam-utils.js +1 -1
  76. package/lib/utilities/observability/cloudfront-distribution-observability-property-injector.js +1 -1
  77. package/lib/utilities/observability/default-observability-config.js +1 -1
  78. package/lib/utilities/observability/lambda-observability-property-injector.js +1 -1
  79. package/lib/utilities/observability/log-group-data-protection-utils.js +1 -1
  80. package/lib/utilities/observability/powertools-config.d.ts +10 -1
  81. package/lib/utilities/observability/powertools-config.js +19 -3
  82. package/lib/utilities/observability/state-machine-observability-property-injector.js +1 -1
  83. package/lib/utilities/test-utils.d.ts +43 -0
  84. package/lib/utilities/test-utils.js +56 -0
  85. package/lib/utilities/tests/data-loader-nag.test.js +3 -2
  86. package/lib/utilities/tests/data-loader.test.js +3 -2
  87. package/lib/webapp/frontend-construct.js +1 -1
  88. package/lib/webapp/tests/frontend-construct-nag.test.js +3 -2
  89. package/lib/webapp/tests/frontend-construct.test.js +3 -2
  90. package/package.json +6 -5
  91. package/lib/document-processing/resources/default-error-handler/index.js +0 -46
  92. package/lib/document-processing/resources/default-pdf-processor/index.js +0 -46
  93. package/lib/document-processing/resources/default-pdf-validator/index.js +0 -36
@@ -0,0 +1,449 @@
1
+ """
2
+ Tests for structured logging module.
3
+
4
+ This module tests the structured logging functionality including:
5
+ - JSON log format validation
6
+ - Required fields presence
7
+ - Correlation ID propagation
8
+ - Document context handling
9
+ - Log level configuration
10
+
11
+ Requirements: 7.5
12
+ """
13
+
14
+ import json
15
+ import logging
16
+ import os
17
+ import sys
18
+ import unittest
19
+ from io import StringIO
20
+ from unittest.mock import patch, MagicMock
21
+
22
+ # Add the module path
23
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
24
+
25
+ from structured_logging import (
26
+ StructuredLogFormatter,
27
+ StandardLogFormatter,
28
+ StructuredLogger,
29
+ get_logger,
30
+ log_strategy_selection,
31
+ log_chunking_operation,
32
+ with_correlation_id,
33
+ is_observability_enabled
34
+ )
35
+
36
+
37
+ class TestIsObservabilityEnabled(unittest.TestCase):
38
+ """Tests for is_observability_enabled function."""
39
+
40
+ def test_observability_enabled_when_metrics_not_disabled(self):
41
+ """Test observability is enabled when POWERTOOLS_METRICS_DISABLED is not 'true'."""
42
+ with patch.dict(os.environ, {'POWERTOOLS_METRICS_DISABLED': 'false'}):
43
+ self.assertTrue(is_observability_enabled())
44
+
45
+ def test_observability_disabled_when_metrics_disabled(self):
46
+ """Test observability is disabled when POWERTOOLS_METRICS_DISABLED is 'true'."""
47
+ with patch.dict(os.environ, {'POWERTOOLS_METRICS_DISABLED': 'true'}):
48
+ self.assertFalse(is_observability_enabled())
49
+
50
+ def test_observability_enabled_by_default(self):
51
+ """Test observability is enabled by default when env var not set."""
52
+ with patch.dict(os.environ, {}, clear=True):
53
+ # Remove the env var if it exists
54
+ os.environ.pop('POWERTOOLS_METRICS_DISABLED', None)
55
+ self.assertTrue(is_observability_enabled())
56
+
57
+
58
+ class TestStructuredLogFormatter(unittest.TestCase):
59
+ """Tests for StructuredLogFormatter class."""
60
+
61
+ def setUp(self):
62
+ """Set up test fixtures."""
63
+ self.formatter = StructuredLogFormatter(service_name='test-service')
64
+ self.logger = logging.getLogger('test_formatter')
65
+ self.logger.setLevel(logging.DEBUG)
66
+ self.stream = StringIO()
67
+ self.handler = logging.StreamHandler(self.stream)
68
+ self.handler.setFormatter(self.formatter)
69
+ self.logger.handlers = [self.handler]
70
+ self.logger.propagate = False
71
+
72
+ def test_log_format_is_valid_json(self):
73
+ """Test that log output is valid JSON."""
74
+ self.logger.info("Test message")
75
+ log_output = self.stream.getvalue().strip()
76
+
77
+ # Should be valid JSON
78
+ parsed = json.loads(log_output)
79
+ self.assertIsInstance(parsed, dict)
80
+
81
+ def test_log_contains_required_fields(self):
82
+ """Test that log contains all required fields."""
83
+ self.logger.info("Test message")
84
+ log_output = self.stream.getvalue().strip()
85
+ parsed = json.loads(log_output)
86
+
87
+ # Check required fields
88
+ self.assertIn('timestamp', parsed)
89
+ self.assertIn('level', parsed)
90
+ self.assertIn('message', parsed)
91
+ self.assertIn('logger', parsed)
92
+ self.assertIn('service', parsed)
93
+
94
+ def test_log_level_is_correct(self):
95
+ """Test that log level is correctly recorded."""
96
+ self.logger.info("Info message")
97
+ log_output = self.stream.getvalue().strip()
98
+ parsed = json.loads(log_output)
99
+
100
+ self.assertEqual(parsed['level'], 'INFO')
101
+
102
+ def test_log_message_is_correct(self):
103
+ """Test that log message is correctly recorded."""
104
+ self.logger.info("Test message content")
105
+ log_output = self.stream.getvalue().strip()
106
+ parsed = json.loads(log_output)
107
+
108
+ self.assertEqual(parsed['message'], 'Test message content')
109
+
110
+ def test_log_service_name_is_correct(self):
111
+ """Test that service name is correctly recorded."""
112
+ self.logger.info("Test message")
113
+ log_output = self.stream.getvalue().strip()
114
+ parsed = json.loads(log_output)
115
+
116
+ self.assertEqual(parsed['service'], 'test-service')
117
+
118
+ def test_error_log_includes_location(self):
119
+ """Test that error logs include location information."""
120
+ self.logger.error("Error message")
121
+ log_output = self.stream.getvalue().strip()
122
+ parsed = json.loads(log_output)
123
+
124
+ self.assertIn('location', parsed)
125
+ self.assertIn('file', parsed['location'])
126
+ self.assertIn('line', parsed['location'])
127
+ self.assertIn('function', parsed['location'])
128
+
129
+ def test_extra_context_is_included(self):
130
+ """Test that extra context fields are included."""
131
+ self.logger.info("Test message", extra={'documentId': 'doc-123', 'chunkIndex': 0})
132
+ log_output = self.stream.getvalue().strip()
133
+ parsed = json.loads(log_output)
134
+
135
+ self.assertIn('context', parsed)
136
+ self.assertEqual(parsed['context']['documentId'], 'doc-123')
137
+ self.assertEqual(parsed['context']['chunkIndex'], 0)
138
+
139
+ def test_timestamp_is_iso_format(self):
140
+ """Test that timestamp is in ISO 8601 format."""
141
+ self.logger.info("Test message")
142
+ log_output = self.stream.getvalue().strip()
143
+ parsed = json.loads(log_output)
144
+
145
+ # Should contain 'T' separator and timezone info
146
+ timestamp = parsed['timestamp']
147
+ self.assertIn('T', timestamp)
148
+ # Should end with timezone indicator (Z or +00:00)
149
+ self.assertTrue(timestamp.endswith('Z') or '+' in timestamp or timestamp.endswith('+00:00'))
150
+
151
+
152
+ class TestStructuredLogger(unittest.TestCase):
153
+ """Tests for StructuredLogger class."""
154
+
155
+ def setUp(self):
156
+ """Set up test fixtures."""
157
+ # Clear any existing context
158
+ StructuredLogger._correlation_id = None
159
+ StructuredLogger._document_id = None
160
+ StructuredLogger._chunk_index = None
161
+
162
+ def tearDown(self):
163
+ """Clean up after tests."""
164
+ StructuredLogger._correlation_id = None
165
+ StructuredLogger._document_id = None
166
+ StructuredLogger._chunk_index = None
167
+
168
+ def test_set_correlation_id_generates_uuid(self):
169
+ """Test that set_correlation_id generates a UUID when none provided."""
170
+ logger = get_logger('test')
171
+ correlation_id = logger.set_correlation_id()
172
+
173
+ self.assertIsNotNone(correlation_id)
174
+ # UUID format: 8-4-4-4-12 characters
175
+ self.assertEqual(len(correlation_id), 36)
176
+ self.assertEqual(correlation_id.count('-'), 4)
177
+
178
+ def test_set_correlation_id_uses_provided_value(self):
179
+ """Test that set_correlation_id uses provided value."""
180
+ logger = get_logger('test')
181
+ correlation_id = logger.set_correlation_id('custom-correlation-id')
182
+
183
+ self.assertEqual(correlation_id, 'custom-correlation-id')
184
+
185
+ def test_get_correlation_id_returns_set_value(self):
186
+ """Test that get_correlation_id returns the set value."""
187
+ logger = get_logger('test')
188
+ logger.set_correlation_id('test-id')
189
+
190
+ self.assertEqual(logger.get_correlation_id(), 'test-id')
191
+
192
+ def test_set_document_context(self):
193
+ """Test that document context is set correctly."""
194
+ logger = get_logger('test')
195
+ logger.set_document_context(document_id='doc-123', chunk_index=5)
196
+
197
+ self.assertEqual(StructuredLogger._document_id, 'doc-123')
198
+ self.assertEqual(StructuredLogger._chunk_index, 5)
199
+
200
+ def test_clear_context(self):
201
+ """Test that clear_context clears all context."""
202
+ logger = get_logger('test')
203
+ logger.set_correlation_id('test-id')
204
+ logger.set_document_context(document_id='doc-123', chunk_index=5)
205
+
206
+ logger.clear_context()
207
+
208
+ self.assertIsNone(StructuredLogger._correlation_id)
209
+ self.assertIsNone(StructuredLogger._document_id)
210
+ self.assertIsNone(StructuredLogger._chunk_index)
211
+
212
+ def test_build_extra_includes_correlation_id(self):
213
+ """Test that _build_extra includes correlation ID."""
214
+ logger = get_logger('test')
215
+ logger.set_correlation_id('corr-123')
216
+
217
+ extra = logger._build_extra()
218
+
219
+ self.assertEqual(extra['correlationId'], 'corr-123')
220
+
221
+ def test_build_extra_includes_document_id(self):
222
+ """Test that _build_extra includes document ID."""
223
+ logger = get_logger('test')
224
+ logger.set_document_context(document_id='doc-456')
225
+
226
+ extra = logger._build_extra()
227
+
228
+ self.assertEqual(extra['documentId'], 'doc-456')
229
+
230
+ def test_build_extra_includes_chunk_index(self):
231
+ """Test that _build_extra includes chunk index."""
232
+ logger = get_logger('test')
233
+ logger.set_document_context(chunk_index=3)
234
+
235
+ extra = logger._build_extra()
236
+
237
+ self.assertEqual(extra['chunkIndex'], 3)
238
+
239
+ def test_build_extra_merges_provided_extra(self):
240
+ """Test that _build_extra merges provided extra fields."""
241
+ logger = get_logger('test')
242
+ logger.set_correlation_id('corr-123')
243
+
244
+ extra = logger._build_extra({'customField': 'value'})
245
+
246
+ self.assertEqual(extra['correlationId'], 'corr-123')
247
+ self.assertEqual(extra['customField'], 'value')
248
+
249
+
250
+ class TestLogStrategySelection(unittest.TestCase):
251
+ """Tests for log_strategy_selection function."""
252
+
253
+ def setUp(self):
254
+ """Set up test fixtures."""
255
+ self.logger = get_logger('test_strategy')
256
+ # Clear context
257
+ StructuredLogger._correlation_id = None
258
+ StructuredLogger._document_id = None
259
+ StructuredLogger._chunk_index = None
260
+
261
+ def test_log_strategy_selection_logs_info(self):
262
+ """Test that log_strategy_selection logs at INFO level."""
263
+ with patch.object(self.logger, 'info') as mock_info:
264
+ log_strategy_selection(
265
+ logger=self.logger,
266
+ strategy='hybrid',
267
+ requires_chunking=True,
268
+ reason='Document exceeds thresholds',
269
+ document_pages=150,
270
+ document_tokens=200000,
271
+ page_threshold=100,
272
+ token_threshold=150000,
273
+ page_threshold_exceeded=True,
274
+ token_threshold_exceeded=True
275
+ )
276
+
277
+ mock_info.assert_called_once()
278
+ call_args = mock_info.call_args
279
+ self.assertIn('CHUNKING_REQUIRED', call_args[0][0])
280
+
281
+ def test_log_strategy_selection_includes_all_fields(self):
282
+ """Test that log_strategy_selection includes all required fields."""
283
+ with patch.object(self.logger, 'info') as mock_info:
284
+ log_strategy_selection(
285
+ logger=self.logger,
286
+ strategy='token-based',
287
+ requires_chunking=False,
288
+ reason='Below threshold',
289
+ document_pages=50,
290
+ document_tokens=100000,
291
+ page_threshold=100,
292
+ token_threshold=150000,
293
+ page_threshold_exceeded=False,
294
+ token_threshold_exceeded=False
295
+ )
296
+
297
+ call_args = mock_info.call_args
298
+ extra = call_args[1]['extra']
299
+
300
+ self.assertEqual(extra['strategy'], 'token-based')
301
+ self.assertEqual(extra['requiresChunking'], False)
302
+ self.assertIn('documentCharacteristics', extra)
303
+ self.assertIn('thresholds', extra)
304
+
305
+
306
+ class TestLogChunkingOperation(unittest.TestCase):
307
+ """Tests for log_chunking_operation function."""
308
+
309
+ def setUp(self):
310
+ """Set up test fixtures."""
311
+ self.logger = get_logger('test_operation')
312
+
313
+ def test_log_chunking_operation_success(self):
314
+ """Test logging successful chunking operation."""
315
+ with patch.object(self.logger, 'info') as mock_info:
316
+ log_chunking_operation(
317
+ logger=self.logger,
318
+ operation='split',
319
+ document_id='doc-123',
320
+ chunk_count=5,
321
+ success=True,
322
+ duration_ms=1500.0
323
+ )
324
+
325
+ mock_info.assert_called_once()
326
+ call_args = mock_info.call_args
327
+ self.assertIn('split', call_args[0][0])
328
+
329
+ def test_log_chunking_operation_failure(self):
330
+ """Test logging failed chunking operation."""
331
+ with patch.object(self.logger, 'error') as mock_error:
332
+ log_chunking_operation(
333
+ logger=self.logger,
334
+ operation='upload',
335
+ document_id='doc-456',
336
+ success=False,
337
+ error_message='S3 access denied'
338
+ )
339
+
340
+ mock_error.assert_called_once()
341
+ call_args = mock_error.call_args
342
+ extra = call_args[1]['extra']
343
+
344
+ self.assertEqual(extra['success'], False)
345
+ self.assertEqual(extra['errorMessage'], 'S3 access denied')
346
+
347
+
348
+ class TestWithCorrelationIdDecorator(unittest.TestCase):
349
+ """Tests for with_correlation_id decorator."""
350
+
351
+ def setUp(self):
352
+ """Set up test fixtures."""
353
+ # Clear context
354
+ StructuredLogger._correlation_id = None
355
+ StructuredLogger._document_id = None
356
+ StructuredLogger._chunk_index = None
357
+
358
+ def tearDown(self):
359
+ """Clean up after tests."""
360
+ StructuredLogger._correlation_id = None
361
+ StructuredLogger._document_id = None
362
+ StructuredLogger._chunk_index = None
363
+
364
+ def test_decorator_extracts_correlation_id_from_event(self):
365
+ """Test that decorator extracts correlation ID from event."""
366
+ @with_correlation_id
367
+ def handler(event, context):
368
+ logger = get_logger()
369
+ return logger.get_correlation_id()
370
+
371
+ result = handler({'correlationId': 'event-corr-id'}, None)
372
+
373
+ self.assertEqual(result, 'event-corr-id')
374
+
375
+ def test_decorator_extracts_correlation_id_from_headers(self):
376
+ """Test that decorator extracts correlation ID from headers."""
377
+ @with_correlation_id
378
+ def handler(event, context):
379
+ logger = get_logger()
380
+ return logger.get_correlation_id()
381
+
382
+ result = handler({'headers': {'x-correlation-id': 'header-corr-id'}}, None)
383
+
384
+ self.assertEqual(result, 'header-corr-id')
385
+
386
+ def test_decorator_generates_correlation_id_if_not_present(self):
387
+ """Test that decorator generates correlation ID if not in event."""
388
+ @with_correlation_id
389
+ def handler(event, context):
390
+ logger = get_logger()
391
+ return logger.get_correlation_id()
392
+
393
+ result = handler({}, None)
394
+
395
+ self.assertIsNotNone(result)
396
+ # UUID format
397
+ self.assertEqual(len(result), 36)
398
+
399
+ def test_decorator_sets_document_context(self):
400
+ """Test that decorator sets document context from event."""
401
+ @with_correlation_id
402
+ def handler(event, context):
403
+ return StructuredLogger._document_id
404
+
405
+ result = handler({'documentId': 'doc-789'}, None)
406
+
407
+ self.assertEqual(result, 'doc-789')
408
+
409
+ def test_decorator_clears_context_after_execution(self):
410
+ """Test that decorator clears context after handler execution."""
411
+ @with_correlation_id
412
+ def handler(event, context):
413
+ return 'done'
414
+
415
+ handler({'documentId': 'doc-123', 'correlationId': 'corr-123'}, None)
416
+
417
+ # Context should be cleared
418
+ self.assertIsNone(StructuredLogger._correlation_id)
419
+ self.assertIsNone(StructuredLogger._document_id)
420
+
421
+
422
+ class TestLogLevelConfiguration(unittest.TestCase):
423
+ """Tests for log level configuration."""
424
+
425
+ def test_default_log_level_is_info(self):
426
+ """Test that default log level is INFO."""
427
+ with patch.dict(os.environ, {}, clear=True):
428
+ os.environ.pop('LOG_LEVEL', None)
429
+ logger = StructuredLogger('test_level')
430
+
431
+ self.assertEqual(logger.logger.level, logging.INFO)
432
+
433
+ def test_log_level_from_environment(self):
434
+ """Test that log level is read from environment."""
435
+ with patch.dict(os.environ, {'LOG_LEVEL': 'ERROR'}):
436
+ logger = StructuredLogger('test_level_env')
437
+
438
+ self.assertEqual(logger.logger.level, logging.ERROR)
439
+
440
+ def test_invalid_log_level_defaults_to_info(self):
441
+ """Test that invalid log level defaults to INFO."""
442
+ with patch.dict(os.environ, {'LOG_LEVEL': 'INVALID'}):
443
+ logger = StructuredLogger('test_level_invalid')
444
+
445
+ self.assertEqual(logger.logger.level, logging.INFO)
446
+
447
+
448
+ if __name__ == '__main__':
449
+ unittest.main()