Skip to content

Commit b291afa

Browse files
authored
L-1200 Detect and handle circular dependencies (#23)
1 parent ee324b0 commit b291afa

File tree

2 files changed

+62
-3
lines changed

2 files changed

+62
-3
lines changed

logtail/frame.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,7 @@ def create_frame(record, message, context, include_extra_attributes=False):
4141
if events:
4242
frame.update(events)
4343

44-
return frame
45-
44+
return _remove_circular_dependencies(frame)
4645

4746
def _parse_custom_events(record, include_extra_attributes):
4847
default_keys = {
@@ -60,6 +59,22 @@ def _parse_custom_events(record, include_extra_attributes):
6059
events[key] = val
6160
return events
6261

62+
def _remove_circular_dependencies(obj, memo=None):
63+
if memo is None:
64+
memo = set()
65+
if id(obj) in memo:
66+
return "<omitted circular reference>"
67+
memo.add(id(obj))
68+
if isinstance(obj, dict):
69+
new_dict = {}
70+
for key, value in obj.items():
71+
new_dict[key] = _remove_circular_dependencies(value, memo)
72+
return new_dict
73+
elif isinstance(obj, list):
74+
new_list = [_remove_circular_dependencies(item, memo) for item in obj]
75+
return new_list
76+
else:
77+
return obj
6378

6479
def _levelname(level):
6580
return level.lower()

tests/test_handler.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def test_can_send_unserializable_extra_data(self, MockWorker):
159159
self.assertTrue(handler.pipe.empty())
160160

161161
@mock.patch('logtail.handler.FlushWorker')
162-
def test_can_send_unserializable_extra_context(self, MockWorker):
162+
def test_can_send_unserializable_context(self, MockWorker):
163163
buffer_capacity = 1
164164
handler = LogtailHandler(
165165
source_token=self.source_token,
@@ -178,6 +178,50 @@ def test_can_send_unserializable_extra_context(self, MockWorker):
178178
self.assertRegex(log_entry['context']['data']['unserializable'], r'^<tests\.test_handler\.UnserializableObject object at 0x[0-f]+>$')
179179
self.assertTrue(handler.pipe.empty())
180180

181+
@mock.patch('logtail.handler.FlushWorker')
182+
def test_can_send_circular_dependency_in_extra_data(self, MockWorker):
183+
buffer_capacity = 1
184+
handler = LogtailHandler(
185+
source_token=self.source_token,
186+
buffer_capacity=buffer_capacity
187+
)
188+
189+
logger = logging.getLogger(__name__)
190+
logger.handlers = []
191+
logger.addHandler(handler)
192+
circular_dependency = {'egg': {}}
193+
circular_dependency['egg']['chicken'] = circular_dependency
194+
logger.info('hello', extra={'data': circular_dependency})
195+
196+
log_entry = handler.pipe.get()
197+
198+
self.assertEqual(log_entry['message'], 'hello')
199+
self.assertEqual(log_entry['data']['egg']['chicken'], "<omitted circular reference>")
200+
self.assertTrue(handler.pipe.empty())
201+
202+
203+
@mock.patch('logtail.handler.FlushWorker')
204+
def test_can_send_circular_dependency_in_context(self, MockWorker):
205+
buffer_capacity = 1
206+
handler = LogtailHandler(
207+
source_token=self.source_token,
208+
buffer_capacity=buffer_capacity
209+
)
210+
211+
logger = logging.getLogger(__name__)
212+
logger.handlers = []
213+
logger.addHandler(handler)
214+
circular_dependency = {'egg': {}}
215+
circular_dependency['egg']['chicken'] = circular_dependency
216+
with context(data=circular_dependency):
217+
logger.info('hello')
218+
219+
log_entry = handler.pipe.get()
220+
221+
self.assertEqual(log_entry['message'], 'hello')
222+
self.assertEqual(log_entry['context']['data']['egg']['chicken']['egg'], "<omitted circular reference>")
223+
self.assertTrue(handler.pipe.empty())
224+
181225

182226
class UnserializableObject(object):
183227
""" Because this is a custom class, it cannot be serialized into JSON. """

0 commit comments

Comments
 (0)