logtail-python 0.2.9__tar.gz → 0.3.0__tar.gz

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 (38) hide show
  1. {logtail-python-0.2.9/logtail_python.egg-info → logtail_python-0.3.0}/PKG-INFO +6 -6
  2. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/__init__.py +1 -1
  3. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/flusher.py +15 -4
  4. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/frame.py +21 -8
  5. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/handler.py +9 -2
  6. {logtail-python-0.2.9 → logtail_python-0.3.0/logtail_python.egg-info}/PKG-INFO +6 -6
  7. {logtail-python-0.2.9 → logtail_python-0.3.0}/setup.py +3 -5
  8. logtail_python-0.3.0/tests/__pycache__/test_flusher.cpython-311.pyc +0 -0
  9. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/test_formatter.cpython-311.pyc +0 -0
  10. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/test_frame.cpython-311.pyc +0 -0
  11. logtail_python-0.3.0/tests/__pycache__/test_handler.cpython-311.pyc +0 -0
  12. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/test_helpers.cpython-311.pyc +0 -0
  13. logtail_python-0.3.0/tests/__pycache__/test_uploader.cpython-311.pyc +0 -0
  14. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_flusher.py +9 -7
  15. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_handler.py +61 -13
  16. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_uploader.py +3 -1
  17. logtail-python-0.2.9/tests/__pycache__/test_flusher.cpython-311.pyc +0 -0
  18. logtail-python-0.2.9/tests/__pycache__/test_handler.cpython-311.pyc +0 -0
  19. logtail-python-0.2.9/tests/__pycache__/test_uploader.cpython-311.pyc +0 -0
  20. {logtail-python-0.2.9 → logtail_python-0.3.0}/LICENSE.md +0 -0
  21. {logtail-python-0.2.9 → logtail_python-0.3.0}/MANIFEST.in +0 -0
  22. {logtail-python-0.2.9 → logtail_python-0.3.0}/README.md +0 -0
  23. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/compat.py +0 -0
  24. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/formatter.py +0 -0
  25. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/helpers.py +0 -0
  26. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/uploader.py +0 -0
  27. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail_python.egg-info/SOURCES.txt +0 -0
  28. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail_python.egg-info/dependency_links.txt +0 -0
  29. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail_python.egg-info/requires.txt +0 -0
  30. {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail_python.egg-info/top_level.txt +0 -0
  31. {logtail-python-0.2.9 → logtail_python-0.3.0}/requirements.txt +0 -0
  32. {logtail-python-0.2.9 → logtail_python-0.3.0}/setup.cfg +0 -0
  33. {logtail-python-0.2.9 → logtail_python-0.3.0}/test-requirements.txt +0 -0
  34. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__init__.py +0 -0
  35. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/__init__.cpython-311.pyc +0 -0
  36. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_formatter.py +0 -0
  37. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_frame.py +0 -0
  38. {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_helpers.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: logtail-python
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: Logtail.com client library
5
5
  Home-page: https://github.com/logtail/logtail-python
6
- Download-URL: https://github.com/logtail/logtail-python/tarball/0.2.9
6
+ Download-URL: https://github.com/logtail/logtail-python/tarball/0.3.0
7
7
  Author: Logtail
8
8
  Author-email: hello@logtail.com
9
9
  License: ISC
@@ -11,18 +11,18 @@ Keywords: api,logtail,logging,client
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: ISC License (ISCL)
13
13
  Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 2
15
- Classifier: Programming Language :: Python :: 2.7
16
14
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.5
18
- Classifier: Programming Language :: Python :: 3.6
19
15
  Classifier: Programming Language :: Python :: 3.7
20
16
  Classifier: Programming Language :: Python :: 3.8
21
17
  Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
22
20
  Classifier: Programming Language :: Python
23
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
22
  Description-Content-Type: text/markdown
25
23
  License-File: LICENSE.md
24
+ Requires-Dist: requests>=2.18.4
25
+ Requires-Dist: msgpack>=0.5.6
26
26
 
27
27
  # [Better Stack](https://betterstack.com/logs) Python client
28
28
 
@@ -5,6 +5,6 @@ from .handler import LogtailHandler
5
5
  from .helpers import LogtailContext, DEFAULT_CONTEXT
6
6
  from .formatter import LogtailFormatter
7
7
 
8
- __version__ = '0.2.9'
8
+ __version__ = '0.3.0'
9
9
 
10
10
  context = DEFAULT_CONTEXT
@@ -10,14 +10,17 @@ RETRY_SCHEDULE = (1, 10, 60) # seconds
10
10
 
11
11
 
12
12
  class FlushWorker(threading.Thread):
13
- def __init__(self, upload, pipe, buffer_capacity, flush_interval):
13
+ def __init__(self, upload, pipe, buffer_capacity, flush_interval, check_interval):
14
14
  threading.Thread.__init__(self)
15
15
  self.parent_thread = threading.current_thread()
16
16
  self.upload = upload
17
17
  self.pipe = pipe
18
18
  self.buffer_capacity = buffer_capacity
19
19
  self.flush_interval = flush_interval
20
+ self.check_interval = check_interval
20
21
  self.should_run = True
22
+ self._flushing = False
23
+ self._clean = True
21
24
 
22
25
  def run(self):
23
26
  while self.should_run:
@@ -27,6 +30,7 @@ class FlushWorker(threading.Thread):
27
30
  last_flush = time.time()
28
31
  time_remaining = _initial_time_remaining(self.flush_interval)
29
32
  frame = []
33
+ self._clean = True
30
34
 
31
35
  # If the parent thread has exited but there are still outstanding
32
36
  # events, attempt to send them before exiting.
@@ -38,16 +42,17 @@ class FlushWorker(threading.Thread):
38
42
  # `flush_interval` seconds have passed without sending any events.
39
43
  while len(frame) < self.buffer_capacity and time_remaining > 0:
40
44
  try:
41
- # Blocks for up to 1.0 seconds for each item to prevent
45
+ # Blocks for up to `check_interval` seconds for each item to prevent
42
46
  # spinning and burning CPU unnecessarily. Could block for the
43
47
  # entire amount of `time_remaining` but then in the case that
44
48
  # the parent thread has exited, that entire amount of time
45
49
  # would be waited before this child worker thread exits.
46
- entry = self.pipe.get(block=(not shutdown), timeout=1.0)
50
+ entry = self.pipe.get(block=(not shutdown), timeout=self.check_interval)
51
+ self._clean = False
47
52
  frame.append(entry)
48
53
  self.pipe.task_done()
49
54
  except queue.Empty:
50
- if shutdown:
55
+ if shutdown or self._flushing:
51
56
  break
52
57
  shutdown = not self.parent_thread.is_alive()
53
58
  time_remaining = _calculate_time_remaining(last_flush, self.flush_interval)
@@ -68,9 +73,15 @@ class FlushWorker(threading.Thread):
68
73
  if response.status_code == 500 and getattr(response, "exception") != None:
69
74
  print('Failed to send logs to Better Stack after {} retries: {}'.format(len(RETRY_SCHEDULE), response.exception))
70
75
 
76
+ self._clean = True
71
77
  if shutdown and self.pipe.empty():
72
78
  self.should_run = False
73
79
 
80
+ def flush(self):
81
+ self._flushing = True
82
+ while not self._clean or not self.pipe.empty():
83
+ time.sleep(self.check_interval)
84
+ self._flushing = False
74
85
 
75
86
  def _initial_time_remaining(flush_interval):
76
87
  return flush_interval
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
  from __future__ import print_function, unicode_literals
3
- from datetime import datetime
3
+ from datetime import datetime, timezone
4
4
 
5
5
  from os import path
6
6
  import __main__
@@ -11,10 +11,8 @@ def create_frame(record, message, context, include_extra_attributes=False):
11
11
  if "request" in r and not isinstance(r["request"], (dict, list, bool, int, float, str)) :
12
12
  del r["request"]
13
13
  frame = {}
14
- # Python 3 only solution if we ever drop Python 2.7
15
- # frame['dt'] = datetime.utcfromtimestamp(r['created']).replace(tzinfo=timezone.utc).isoformat()
16
- frame['dt'] = "{}+00:00".format(datetime.utcfromtimestamp(r['created']).isoformat())
17
- frame['level'] = level = _levelname(r['levelname'])
14
+ frame['dt'] = datetime.fromtimestamp(r['created'], timezone.utc).isoformat()
15
+ frame['level'] = _levelname(r['levelname'])
18
16
  frame['severity'] = int(r['levelno'] / 10)
19
17
  frame['message'] = message
20
18
  frame['context'] = ctx = {}
@@ -41,14 +39,13 @@ def create_frame(record, message, context, include_extra_attributes=False):
41
39
  if events:
42
40
  frame.update(events)
43
41
 
44
- return frame
45
-
42
+ return _remove_circular_dependencies(frame)
46
43
 
47
44
  def _parse_custom_events(record, include_extra_attributes):
48
45
  default_keys = {
49
46
  'args', 'asctime', 'created', 'exc_info', 'exc_text', 'pathname',
50
47
  'funcName', 'levelname', 'levelno', 'lineno', 'module', 'msecs',
51
- 'message', 'msg', 'name', 'pathname', 'process', 'processName',
48
+ 'message', 'msg', 'name', 'process', 'processName',
52
49
  'relativeCreated', 'thread', 'threadName'
53
50
  }
54
51
  events = {}
@@ -60,6 +57,22 @@ def _parse_custom_events(record, include_extra_attributes):
60
57
  events[key] = val
61
58
  return events
62
59
 
60
+ def _remove_circular_dependencies(obj, memo=None):
61
+ if memo is None:
62
+ memo = set()
63
+ if id(obj) in memo:
64
+ return "<omitted circular reference>"
65
+ memo.add(id(obj))
66
+ if isinstance(obj, dict):
67
+ new_dict = {}
68
+ for key, value in obj.items():
69
+ new_dict[key] = _remove_circular_dependencies(value, memo)
70
+ return new_dict
71
+ elif isinstance(obj, list):
72
+ new_list = [_remove_circular_dependencies(item, memo) for item in obj]
73
+ return new_list
74
+ else:
75
+ return obj
63
76
 
64
77
  def _levelname(level):
65
78
  return level.lower()
@@ -12,6 +12,7 @@ from .frame import create_frame
12
12
  DEFAULT_HOST = 'https://in.logs.betterstack.com'
13
13
  DEFAULT_BUFFER_CAPACITY = 1000
14
14
  DEFAULT_FLUSH_INTERVAL = 1
15
+ DEFAULT_CHECK_INTERVAL = 0.1
15
16
  DEFAULT_RAISE_EXCEPTIONS = False
16
17
  DEFAULT_DROP_EXTRA_EVENTS = True
17
18
  DEFAULT_INCLUDE_EXTRA_ATTRIBUTES = True
@@ -23,6 +24,7 @@ class LogtailHandler(logging.Handler):
23
24
  host=DEFAULT_HOST,
24
25
  buffer_capacity=DEFAULT_BUFFER_CAPACITY,
25
26
  flush_interval=DEFAULT_FLUSH_INTERVAL,
27
+ check_interval=DEFAULT_CHECK_INTERVAL,
26
28
  raise_exceptions=DEFAULT_RAISE_EXCEPTIONS,
27
29
  drop_extra_events=DEFAULT_DROP_EXTRA_EVENTS,
28
30
  include_extra_attributes=DEFAULT_INCLUDE_EXTRA_ATTRIBUTES,
@@ -38,6 +40,7 @@ class LogtailHandler(logging.Handler):
38
40
  self.include_extra_attributes = include_extra_attributes
39
41
  self.buffer_capacity = buffer_capacity
40
42
  self.flush_interval = flush_interval
43
+ self.check_interval = check_interval
41
44
  self.raise_exceptions = raise_exceptions
42
45
  self.dropcount = 0
43
46
  # Do not initialize the flush thread yet because it causes issues on Render.
@@ -51,7 +54,8 @@ class LogtailHandler(logging.Handler):
51
54
  self.uploader,
52
55
  self.pipe,
53
56
  self.buffer_capacity,
54
- self.flush_interval
57
+ self.flush_interval,
58
+ self.check_interval,
55
59
  )
56
60
  self.flush_thread.start()
57
61
 
@@ -68,7 +72,10 @@ class LogtailHandler(logging.Handler):
68
72
  # Only raised when not blocking, which means that extra events
69
73
  # should be dropped.
70
74
  self.dropcount += 1
71
- pass
72
75
  except Exception as e:
73
76
  if self.raise_exceptions:
74
77
  raise e
78
+
79
+ def flush(self):
80
+ if self.flush_thread and self.flush_thread.is_alive():
81
+ self.flush_thread.flush()
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: logtail-python
3
- Version: 0.2.9
3
+ Version: 0.3.0
4
4
  Summary: Logtail.com client library
5
5
  Home-page: https://github.com/logtail/logtail-python
6
- Download-URL: https://github.com/logtail/logtail-python/tarball/0.2.9
6
+ Download-URL: https://github.com/logtail/logtail-python/tarball/0.3.0
7
7
  Author: Logtail
8
8
  Author-email: hello@logtail.com
9
9
  License: ISC
@@ -11,18 +11,18 @@ Keywords: api,logtail,logging,client
11
11
  Classifier: Intended Audience :: Developers
12
12
  Classifier: License :: OSI Approved :: ISC License (ISCL)
13
13
  Classifier: Operating System :: OS Independent
14
- Classifier: Programming Language :: Python :: 2
15
- Classifier: Programming Language :: Python :: 2.7
16
14
  Classifier: Programming Language :: Python :: 3
17
- Classifier: Programming Language :: Python :: 3.5
18
- Classifier: Programming Language :: Python :: 3.6
19
15
  Classifier: Programming Language :: Python :: 3.7
20
16
  Classifier: Programming Language :: Python :: 3.8
21
17
  Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
22
20
  Classifier: Programming Language :: Python
23
21
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
22
  Description-Content-Type: text/markdown
25
23
  License-File: LICENSE.md
24
+ Requires-Dist: requests>=2.18.4
25
+ Requires-Dist: msgpack>=0.5.6
26
26
 
27
27
  # [Better Stack](https://betterstack.com/logs) Python client
28
28
 
@@ -3,7 +3,7 @@ import os
3
3
  from setuptools import setup
4
4
 
5
5
 
6
- VERSION = '0.2.9'
6
+ VERSION = '0.3.0'
7
7
  ROOT_DIR = os.path.dirname(__file__)
8
8
 
9
9
  REQUIREMENTS = [
@@ -33,14 +33,12 @@ setup(
33
33
  'Intended Audience :: Developers',
34
34
  'License :: OSI Approved :: ISC License (ISCL)',
35
35
  'Operating System :: OS Independent',
36
- 'Programming Language :: Python :: 2',
37
- 'Programming Language :: Python :: 2.7',
38
36
  'Programming Language :: Python :: 3',
39
- 'Programming Language :: Python :: 3.5',
40
- 'Programming Language :: Python :: 3.6',
41
37
  'Programming Language :: Python :: 3.7',
42
38
  'Programming Language :: Python :: 3.8',
43
39
  'Programming Language :: Python :: 3.9',
40
+ 'Programming Language :: Python :: 3.10',
41
+ 'Programming Language :: Python :: 3.11',
44
42
  'Programming Language :: Python',
45
43
  'Topic :: Software Development :: Libraries :: Python Modules',
46
44
  ],
@@ -6,6 +6,8 @@ import time
6
6
  import threading
7
7
  import unittest
8
8
 
9
+ from unittest.mock import patch
10
+
9
11
  from logtail.compat import queue
10
12
  from logtail.flusher import RETRY_SCHEDULE
11
13
  from logtail.flusher import FlushWorker
@@ -17,20 +19,20 @@ class TestFlushWorker(unittest.TestCase):
17
19
  source_token = 'dummy_source_token'
18
20
  buffer_capacity = 5
19
21
  flush_interval = 2
22
+ check_interval = 0.01
20
23
 
21
24
  def _setup_worker(self, uploader=None):
22
25
  pipe = queue.Queue(maxsize=self.buffer_capacity)
23
26
  uploader = uploader or Uploader(self.source_token, self.host)
24
- fw = FlushWorker(uploader, pipe, self.buffer_capacity, self.flush_interval)
27
+ fw = FlushWorker(uploader, pipe, self.buffer_capacity, self.flush_interval, self.check_interval)
25
28
  return pipe, uploader, fw
26
29
 
27
30
  def test_is_thread(self):
28
- pipe, uploader, fw = self._setup_worker()
31
+ _, _, fw = self._setup_worker()
29
32
  self.assertIsInstance(fw, threading.Thread)
30
33
 
31
34
  def test_flushes_when_queue_is_full(self):
32
35
  first_frame = list(range(self.buffer_capacity))
33
- second_frame = list(range(self.buffer_capacity, self.buffer_capacity * 2))
34
36
  self.calls = 0
35
37
  self.flush_interval = 1000
36
38
 
@@ -51,7 +53,7 @@ class TestFlushWorker(unittest.TestCase):
51
53
 
52
54
  self.assertEqual(self.calls, 1)
53
55
 
54
- @mock.patch('logtail.flusher._calculate_time_remaining')
56
+ @patch('logtail.flusher._calculate_time_remaining')
55
57
  def test_flushes_after_interval(self, calculate_time_remaining):
56
58
  self.buffer_capacity = 10
57
59
  num_items = 2
@@ -83,8 +85,8 @@ class TestFlushWorker(unittest.TestCase):
83
85
  self.assertEqual(self.upload_calls, 1)
84
86
  self.assertEqual(self.timeout_calls, 2)
85
87
 
86
- @mock.patch('logtail.flusher._calculate_time_remaining')
87
- @mock.patch('logtail.flusher._initial_time_remaining')
88
+ @patch('logtail.flusher._calculate_time_remaining')
89
+ @patch('logtail.flusher._initial_time_remaining')
88
90
  def test_does_nothing_without_any_items(self, initial_time_remaining, calculate_time_remaining):
89
91
  calculate_time_remaining.side_effect = lambda a,b: 0.0
90
92
  initial_time_remaining.side_effect = lambda a: 0.0001
@@ -96,7 +98,7 @@ class TestFlushWorker(unittest.TestCase):
96
98
  fw.step()
97
99
  self.assertFalse(uploader.called)
98
100
 
99
- @mock.patch('logtail.flusher.time.sleep')
101
+ @patch('logtail.flusher.time.sleep')
100
102
  def test_retries_according_to_schedule(self, mock_sleep):
101
103
  first_frame = list(range(self.buffer_capacity))
102
104
 
@@ -6,20 +6,22 @@ import threading
6
6
  import unittest
7
7
  import logging
8
8
 
9
- from logtail import LogtailHandler, context
9
+ from unittest.mock import patch
10
10
 
11
+ from logtail import LogtailHandler, context
12
+ from logtail.handler import FlushWorker
11
13
 
12
14
  class TestLogtailHandler(unittest.TestCase):
13
15
  source_token = 'dummy_source_token'
14
16
  host = 'dummy_host'
15
17
 
16
- @mock.patch('logtail.handler.FlushWorker')
18
+ @patch('logtail.handler.FlushWorker')
17
19
  def test_handler_creates_uploader_from_args(self, MockWorker):
18
20
  handler = LogtailHandler(source_token=self.source_token, host=self.host)
19
21
  self.assertEqual(handler.uploader.source_token, self.source_token)
20
22
  self.assertEqual(handler.uploader.host, self.host)
21
23
 
22
- @mock.patch('logtail.handler.FlushWorker')
24
+ @patch('logtail.handler.FlushWorker')
23
25
  def test_handler_creates_pipe_from_args(self, MockWorker):
24
26
  buffer_capacity = 9
25
27
  flush_interval = 1
@@ -30,11 +32,12 @@ class TestLogtailHandler(unittest.TestCase):
30
32
  )
31
33
  self.assertTrue(handler.pipe.empty())
32
34
 
33
- @mock.patch('logtail.handler.FlushWorker')
35
+ @patch('logtail.handler.FlushWorker')
34
36
  def test_handler_creates_and_starts_worker_from_args_after_first_log(self, MockWorker):
35
37
  buffer_capacity = 9
36
38
  flush_interval = 9
37
- handler = LogtailHandler(source_token=self.source_token, buffer_capacity=buffer_capacity, flush_interval=flush_interval)
39
+ check_interval = 4
40
+ handler = LogtailHandler(source_token=self.source_token, buffer_capacity=buffer_capacity, flush_interval=flush_interval, check_interval=check_interval)
38
41
 
39
42
  self.assertFalse(MockWorker.called)
40
43
 
@@ -47,11 +50,12 @@ class TestLogtailHandler(unittest.TestCase):
47
50
  handler.uploader,
48
51
  handler.pipe,
49
52
  buffer_capacity,
50
- flush_interval
53
+ flush_interval,
54
+ check_interval,
51
55
  )
52
56
  self.assertEqual(handler.flush_thread.start.call_count, 1)
53
57
 
54
- @mock.patch('logtail.handler.FlushWorker')
58
+ @patch('logtail.handler.FlushWorker')
55
59
  def test_emit_starts_thread_if_not_alive(self, MockWorker):
56
60
  handler = LogtailHandler(source_token=self.source_token)
57
61
 
@@ -67,7 +71,7 @@ class TestLogtailHandler(unittest.TestCase):
67
71
 
68
72
  self.assertEqual(handler.flush_thread.start.call_count, 2)
69
73
 
70
- @mock.patch('logtail.handler.FlushWorker')
74
+ @patch('logtail.handler.FlushWorker')
71
75
  def test_emit_drops_records_if_configured(self, MockWorker):
72
76
  buffer_capacity = 1
73
77
  handler = LogtailHandler(
@@ -87,7 +91,7 @@ class TestLogtailHandler(unittest.TestCase):
87
91
  self.assertTrue(handler.pipe.empty())
88
92
  self.assertEqual(handler.dropcount, 1)
89
93
 
90
- @mock.patch('logtail.handler.FlushWorker')
94
+ @patch('logtail.handler.FlushWorker')
91
95
  def test_emit_does_not_drop_records_if_configured(self, MockWorker):
92
96
  buffer_capacity = 1
93
97
  handler = LogtailHandler(
@@ -118,7 +122,7 @@ class TestLogtailHandler(unittest.TestCase):
118
122
 
119
123
  self.assertEqual(handler.dropcount, 0)
120
124
 
121
- @mock.patch('logtail.handler.FlushWorker')
125
+ @patch('logtail.handler.FlushWorker')
122
126
  def test_error_suppression(self, MockWorker):
123
127
  buffer_capacity = 1
124
128
  handler = LogtailHandler(
@@ -139,7 +143,7 @@ class TestLogtailHandler(unittest.TestCase):
139
143
  handler.raise_exceptions = False
140
144
  logger.critical('hello')
141
145
 
142
- @mock.patch('logtail.handler.FlushWorker')
146
+ @patch('logtail.handler.FlushWorker')
143
147
  def test_can_send_unserializable_extra_data(self, MockWorker):
144
148
  buffer_capacity = 1
145
149
  handler = LogtailHandler(
@@ -158,8 +162,8 @@ class TestLogtailHandler(unittest.TestCase):
158
162
  self.assertRegex(log_entry['data']['unserializable'], r'^<tests\.test_handler\.UnserializableObject object at 0x[0-f]+>$')
159
163
  self.assertTrue(handler.pipe.empty())
160
164
 
161
- @mock.patch('logtail.handler.FlushWorker')
162
- def test_can_send_unserializable_extra_context(self, MockWorker):
165
+ @patch('logtail.handler.FlushWorker')
166
+ def test_can_send_unserializable_context(self, MockWorker):
163
167
  buffer_capacity = 1
164
168
  handler = LogtailHandler(
165
169
  source_token=self.source_token,
@@ -178,6 +182,50 @@ class TestLogtailHandler(unittest.TestCase):
178
182
  self.assertRegex(log_entry['context']['data']['unserializable'], r'^<tests\.test_handler\.UnserializableObject object at 0x[0-f]+>$')
179
183
  self.assertTrue(handler.pipe.empty())
180
184
 
185
+ @patch('logtail.handler.FlushWorker')
186
+ def test_can_send_circular_dependency_in_extra_data(self, MockWorker):
187
+ buffer_capacity = 1
188
+ handler = LogtailHandler(
189
+ source_token=self.source_token,
190
+ buffer_capacity=buffer_capacity
191
+ )
192
+
193
+ logger = logging.getLogger(__name__)
194
+ logger.handlers = []
195
+ logger.addHandler(handler)
196
+ circular_dependency = {'egg': {}}
197
+ circular_dependency['egg']['chicken'] = circular_dependency
198
+ logger.info('hello', extra={'data': circular_dependency})
199
+
200
+ log_entry = handler.pipe.get()
201
+
202
+ self.assertEqual(log_entry['message'], 'hello')
203
+ self.assertEqual(log_entry['data']['egg']['chicken'], "<omitted circular reference>")
204
+ self.assertTrue(handler.pipe.empty())
205
+
206
+
207
+ @patch('logtail.handler.FlushWorker')
208
+ def test_can_send_circular_dependency_in_context(self, MockWorker):
209
+ buffer_capacity = 1
210
+ handler = LogtailHandler(
211
+ source_token=self.source_token,
212
+ buffer_capacity=buffer_capacity
213
+ )
214
+
215
+ logger = logging.getLogger(__name__)
216
+ logger.handlers = []
217
+ logger.addHandler(handler)
218
+ circular_dependency = {'egg': {}}
219
+ circular_dependency['egg']['chicken'] = circular_dependency
220
+ with context(data=circular_dependency):
221
+ logger.info('hello')
222
+
223
+ log_entry = handler.pipe.get()
224
+
225
+ self.assertEqual(log_entry['message'], 'hello')
226
+ self.assertEqual(log_entry['context']['data']['egg']['chicken']['egg'], "<omitted circular reference>")
227
+ self.assertTrue(handler.pipe.empty())
228
+
181
229
 
182
230
  class UnserializableObject(object):
183
231
  """ Because this is a custom class, it cannot be serialized into JSON. """
@@ -4,6 +4,8 @@ import msgpack
4
4
  import mock
5
5
  import unittest
6
6
 
7
+ from unittest.mock import patch
8
+
7
9
  from logtail.uploader import Uploader
8
10
 
9
11
 
@@ -12,7 +14,7 @@ class TestUploader(unittest.TestCase):
12
14
  source_token = 'dummy_source_token'
13
15
  frame = [1, 2, 3]
14
16
 
15
- @mock.patch('logtail.uploader.requests.Session.post')
17
+ @patch('logtail.uploader.requests.Session.post')
16
18
  def test_call(self, post):
17
19
  def mock_post(endpoint, data=None, headers=None):
18
20
  # Check that the data is sent to ther correct endpoint
File without changes
File without changes