@cdklabs/cdk-appmod-catalog-blueprints 1.5.0 → 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.
- package/.jsii +2537 -204
- package/lib/document-processing/adapter/adapter.d.ts +4 -2
- package/lib/document-processing/adapter/adapter.js +1 -1
- package/lib/document-processing/adapter/queued-s3-adapter.d.ts +9 -2
- package/lib/document-processing/adapter/queued-s3-adapter.js +29 -15
- package/lib/document-processing/agentic-document-processing.d.ts +4 -0
- package/lib/document-processing/agentic-document-processing.js +20 -10
- package/lib/document-processing/base-document-processing.d.ts +54 -2
- package/lib/document-processing/base-document-processing.js +136 -82
- package/lib/document-processing/bedrock-document-processing.d.ts +202 -2
- package/lib/document-processing/bedrock-document-processing.js +717 -77
- package/lib/document-processing/chunking-config.d.ts +614 -0
- package/lib/document-processing/chunking-config.js +5 -0
- package/lib/document-processing/default-document-processing-config.js +1 -1
- package/lib/document-processing/index.d.ts +1 -0
- package/lib/document-processing/index.js +2 -1
- package/lib/document-processing/resources/aggregation/handler.py +567 -0
- package/lib/document-processing/resources/aggregation/requirements.txt +7 -0
- package/lib/document-processing/resources/aggregation/test_handler.py +362 -0
- package/lib/document-processing/resources/cleanup/handler.py +276 -0
- package/lib/document-processing/resources/cleanup/requirements.txt +5 -0
- package/lib/document-processing/resources/cleanup/test_handler.py +436 -0
- package/lib/document-processing/resources/default-bedrock-invoke/index.py +85 -3
- package/lib/document-processing/resources/default-bedrock-invoke/test_index.py +622 -0
- package/lib/document-processing/resources/pdf-chunking/README.md +313 -0
- package/lib/document-processing/resources/pdf-chunking/chunking_strategies.py +460 -0
- package/lib/document-processing/resources/pdf-chunking/error_handling.py +491 -0
- package/lib/document-processing/resources/pdf-chunking/handler.py +958 -0
- package/lib/document-processing/resources/pdf-chunking/metrics.py +435 -0
- package/lib/document-processing/resources/pdf-chunking/requirements.txt +3 -0
- package/lib/document-processing/resources/pdf-chunking/strategy_selection.py +420 -0
- package/lib/document-processing/resources/pdf-chunking/structured_logging.py +457 -0
- package/lib/document-processing/resources/pdf-chunking/test_chunking_strategies.py +353 -0
- package/lib/document-processing/resources/pdf-chunking/test_error_handling.py +487 -0
- package/lib/document-processing/resources/pdf-chunking/test_handler.py +609 -0
- package/lib/document-processing/resources/pdf-chunking/test_integration.py +694 -0
- package/lib/document-processing/resources/pdf-chunking/test_metrics.py +532 -0
- package/lib/document-processing/resources/pdf-chunking/test_strategy_selection.py +471 -0
- package/lib/document-processing/resources/pdf-chunking/test_structured_logging.py +449 -0
- package/lib/document-processing/resources/pdf-chunking/test_token_estimation.py +374 -0
- package/lib/document-processing/resources/pdf-chunking/token_estimation.py +189 -0
- package/lib/document-processing/tests/agentic-document-processing-nag.test.js +4 -3
- package/lib/document-processing/tests/agentic-document-processing.test.js +488 -4
- package/lib/document-processing/tests/base-document-processing-nag.test.js +9 -2
- package/lib/document-processing/tests/base-document-processing-schema.test.d.ts +1 -0
- package/lib/document-processing/tests/base-document-processing-schema.test.js +337 -0
- package/lib/document-processing/tests/base-document-processing.test.js +114 -8
- package/lib/document-processing/tests/bedrock-document-processing-chunking-nag.test.d.ts +1 -0
- package/lib/document-processing/tests/bedrock-document-processing-chunking-nag.test.js +382 -0
- package/lib/document-processing/tests/bedrock-document-processing-nag.test.js +4 -3
- package/lib/document-processing/tests/bedrock-document-processing-security.test.d.ts +1 -0
- package/lib/document-processing/tests/bedrock-document-processing-security.test.js +389 -0
- package/lib/document-processing/tests/bedrock-document-processing.test.js +808 -8
- package/lib/document-processing/tests/chunking-config.test.d.ts +1 -0
- package/lib/document-processing/tests/chunking-config.test.js +238 -0
- package/lib/document-processing/tests/queued-s3-adapter-nag.test.js +9 -2
- package/lib/document-processing/tests/queued-s3-adapter.test.js +17 -6
- package/lib/framework/agents/base-agent.js +1 -1
- package/lib/framework/agents/batch-agent.js +1 -1
- package/lib/framework/agents/default-agent-config.js +1 -1
- package/lib/framework/bedrock/bedrock.js +1 -1
- package/lib/framework/custom-resource/default-runtimes.js +1 -1
- package/lib/framework/foundation/access-log.js +1 -1
- package/lib/framework/foundation/eventbridge-broker.js +1 -1
- package/lib/framework/foundation/network.js +1 -1
- package/lib/framework/tests/access-log.test.js +5 -2
- package/lib/framework/tests/batch-agent.test.js +5 -2
- package/lib/framework/tests/bedrock.test.js +5 -2
- package/lib/framework/tests/eventbridge-broker.test.js +5 -2
- package/lib/framework/tests/framework-nag.test.js +16 -8
- package/lib/framework/tests/network.test.js +9 -4
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/lib/utilities/data-loader.js +1 -1
- package/lib/utilities/lambda-iam-utils.js +1 -1
- package/lib/utilities/observability/cloudfront-distribution-observability-property-injector.js +1 -1
- package/lib/utilities/observability/default-observability-config.js +1 -1
- package/lib/utilities/observability/lambda-observability-property-injector.js +1 -1
- package/lib/utilities/observability/log-group-data-protection-utils.js +1 -1
- package/lib/utilities/observability/powertools-config.d.ts +10 -1
- package/lib/utilities/observability/powertools-config.js +19 -3
- package/lib/utilities/observability/state-machine-observability-property-injector.js +1 -1
- package/lib/utilities/test-utils.d.ts +43 -0
- package/lib/utilities/test-utils.js +56 -0
- package/lib/utilities/tests/data-loader-nag.test.js +3 -2
- package/lib/utilities/tests/data-loader.test.js +3 -2
- package/lib/webapp/frontend-construct.js +1 -1
- package/lib/webapp/tests/frontend-construct-nag.test.js +3 -2
- package/lib/webapp/tests/frontend-construct.test.js +3 -2
- package/package.json +6 -5
- package/lib/document-processing/resources/default-error-handler/index.js +0 -46
- package/lib/document-processing/resources/default-pdf-processor/index.js +0 -46
- 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()
|