Skip to content

Commit

Permalink
L-1200 Detect and handle circular dependencies (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
PetrHeinz authored Jan 12, 2024
1 parent ee324b0 commit b291afa
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 3 deletions.
19 changes: 17 additions & 2 deletions logtail/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ def create_frame(record, message, context, include_extra_attributes=False):
if events:
frame.update(events)

return frame

return _remove_circular_dependencies(frame)

def _parse_custom_events(record, include_extra_attributes):
default_keys = {
Expand All @@ -60,6 +59,22 @@ def _parse_custom_events(record, include_extra_attributes):
events[key] = val
return events

def _remove_circular_dependencies(obj, memo=None):
if memo is None:
memo = set()
if id(obj) in memo:
return "<omitted circular reference>"
memo.add(id(obj))
if isinstance(obj, dict):
new_dict = {}
for key, value in obj.items():
new_dict[key] = _remove_circular_dependencies(value, memo)
return new_dict
elif isinstance(obj, list):
new_list = [_remove_circular_dependencies(item, memo) for item in obj]
return new_list
else:
return obj

def _levelname(level):
return level.lower()
Expand Down
46 changes: 45 additions & 1 deletion tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def test_can_send_unserializable_extra_data(self, MockWorker):
self.assertTrue(handler.pipe.empty())

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

@mock.patch('logtail.handler.FlushWorker')
def test_can_send_circular_dependency_in_extra_data(self, MockWorker):
buffer_capacity = 1
handler = LogtailHandler(
source_token=self.source_token,
buffer_capacity=buffer_capacity
)

logger = logging.getLogger(__name__)
logger.handlers = []
logger.addHandler(handler)
circular_dependency = {'egg': {}}
circular_dependency['egg']['chicken'] = circular_dependency
logger.info('hello', extra={'data': circular_dependency})

log_entry = handler.pipe.get()

self.assertEqual(log_entry['message'], 'hello')
self.assertEqual(log_entry['data']['egg']['chicken'], "<omitted circular reference>")
self.assertTrue(handler.pipe.empty())


@mock.patch('logtail.handler.FlushWorker')
def test_can_send_circular_dependency_in_context(self, MockWorker):
buffer_capacity = 1
handler = LogtailHandler(
source_token=self.source_token,
buffer_capacity=buffer_capacity
)

logger = logging.getLogger(__name__)
logger.handlers = []
logger.addHandler(handler)
circular_dependency = {'egg': {}}
circular_dependency['egg']['chicken'] = circular_dependency
with context(data=circular_dependency):
logger.info('hello')

log_entry = handler.pipe.get()

self.assertEqual(log_entry['message'], 'hello')
self.assertEqual(log_entry['context']['data']['egg']['chicken']['egg'], "<omitted circular reference>")
self.assertTrue(handler.pipe.empty())


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

0 comments on commit b291afa

Please sign in to comment.