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.
- {logtail-python-0.2.9/logtail_python.egg-info → logtail_python-0.3.0}/PKG-INFO +6 -6
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/__init__.py +1 -1
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/flusher.py +15 -4
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/frame.py +21 -8
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/handler.py +9 -2
- {logtail-python-0.2.9 → logtail_python-0.3.0/logtail_python.egg-info}/PKG-INFO +6 -6
- {logtail-python-0.2.9 → logtail_python-0.3.0}/setup.py +3 -5
- logtail_python-0.3.0/tests/__pycache__/test_flusher.cpython-311.pyc +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/test_formatter.cpython-311.pyc +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/test_frame.cpython-311.pyc +0 -0
- logtail_python-0.3.0/tests/__pycache__/test_handler.cpython-311.pyc +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/test_helpers.cpython-311.pyc +0 -0
- logtail_python-0.3.0/tests/__pycache__/test_uploader.cpython-311.pyc +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_flusher.py +9 -7
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_handler.py +61 -13
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_uploader.py +3 -1
- logtail-python-0.2.9/tests/__pycache__/test_flusher.cpython-311.pyc +0 -0
- logtail-python-0.2.9/tests/__pycache__/test_handler.cpython-311.pyc +0 -0
- logtail-python-0.2.9/tests/__pycache__/test_uploader.cpython-311.pyc +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/LICENSE.md +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/MANIFEST.in +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/README.md +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/compat.py +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/formatter.py +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/helpers.py +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail/uploader.py +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail_python.egg-info/SOURCES.txt +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail_python.egg-info/dependency_links.txt +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail_python.egg-info/requires.txt +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/logtail_python.egg-info/top_level.txt +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/requirements.txt +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/setup.cfg +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/test-requirements.txt +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__init__.py +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_formatter.py +0 -0
- {logtail-python-0.2.9 → logtail_python-0.3.0}/tests/test_frame.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
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=
|
|
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
|
-
|
|
15
|
-
|
|
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', '
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
],
|
|
Binary file
|
{logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/test_formatter.cpython-311.pyc
RENAMED
|
Binary file
|
|
Binary file
|
|
Binary file
|
{logtail-python-0.2.9 → logtail_python-0.3.0}/tests/__pycache__/test_helpers.cpython-311.pyc
RENAMED
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
87
|
-
@
|
|
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
|
-
@
|
|
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
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
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
|
-
@
|
|
162
|
-
def
|
|
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
|
-
@
|
|
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
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|