logtail-python 0.4.1__tar.gz → 0.4.3__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.4.1 → logtail_python-0.4.3}/PKG-INFO +3 -2
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/__init__.py +1 -1
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/flusher.py +23 -4
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/frame.py +6 -1
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/handler.py +15 -3
- logtail_python-0.4.3/logtail/uploader.py +47 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/PKG-INFO +3 -2
- {logtail_python-0.4.1 → logtail_python-0.4.3}/setup.py +2 -1
- {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_flusher.py +30 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_frame.py +15 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_handler.py +74 -3
- logtail_python-0.4.3/tests/test_uploader.py +66 -0
- logtail_python-0.4.1/logtail/uploader.py +0 -27
- logtail_python-0.4.1/tests/test_uploader.py +0 -36
- {logtail_python-0.4.1 → logtail_python-0.4.3}/LICENSE.md +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/MANIFEST.in +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/README.md +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/compat.py +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/formatter.py +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/helpers.py +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/rq.py +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/SOURCES.txt +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/dependency_links.txt +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/requires.txt +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/top_level.txt +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/requirements.txt +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/setup.cfg +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/test-requirements.txt +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/__init__.py +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_formatter.py +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_helpers.py +0 -0
- {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_rq.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logtail-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Better Stack client library
|
|
5
5
|
Home-page: https://github.com/logtail/logtail-python
|
|
6
|
-
Download-URL: https://github.com/logtail/logtail-python/tarball/0.4.
|
|
6
|
+
Download-URL: https://github.com/logtail/logtail-python/tarball/0.4.3
|
|
7
7
|
Author: Logtail
|
|
8
8
|
Author-email: hello@logtail.com
|
|
9
9
|
License: ISC
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
21
|
Classifier: Programming Language :: Python
|
|
21
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
23
|
Requires-Python: >=3.10
|
|
@@ -100,11 +100,30 @@ class FlushWorker(threading.Thread):
|
|
|
100
100
|
if shutdown and self.pipe.empty():
|
|
101
101
|
self.should_run = False
|
|
102
102
|
|
|
103
|
-
def flush(self):
|
|
103
|
+
def flush(self, timeout=None):
|
|
104
|
+
"""Block until the worker has drained the queue.
|
|
105
|
+
|
|
106
|
+
If ``timeout`` is given (in seconds), return after at most that much
|
|
107
|
+
wall time has elapsed even if the queue is still non-empty. Returns
|
|
108
|
+
``True`` if the queue was drained, ``False`` if the timeout fired.
|
|
109
|
+
|
|
110
|
+
The bound matters when the caller holds the global logging lock, as
|
|
111
|
+
``logging.config.dictConfig()`` does while it flushes and closes the
|
|
112
|
+
handlers it replaces: if this worker is inside an upload whose urllib3
|
|
113
|
+
``debug()`` call needs that same lock, an unbounded wait deadlocks the
|
|
114
|
+
process. It also keeps a slow or unreachable endpoint from stalling a
|
|
115
|
+
shutdown forever.
|
|
116
|
+
"""
|
|
104
117
|
self._flushing = True
|
|
105
|
-
|
|
106
|
-
time.
|
|
107
|
-
|
|
118
|
+
try:
|
|
119
|
+
deadline = None if timeout is None else time.time() + timeout
|
|
120
|
+
while not self._clean or not self.pipe.empty():
|
|
121
|
+
if deadline is not None and time.time() >= deadline:
|
|
122
|
+
return False
|
|
123
|
+
time.sleep(self.check_interval)
|
|
124
|
+
return True
|
|
125
|
+
finally:
|
|
126
|
+
self._flushing = False
|
|
108
127
|
|
|
109
128
|
def in_flush_worker():
|
|
110
129
|
return isinstance(threading.current_thread(), FlushWorker)
|
|
@@ -93,4 +93,9 @@ def _relative_to_main_module_if_possible(pathname):
|
|
|
93
93
|
return _relative_to_main_module(pathname) if has_main_module else pathname
|
|
94
94
|
|
|
95
95
|
def _relative_to_main_module(pathname):
|
|
96
|
-
|
|
96
|
+
try:
|
|
97
|
+
return path.relpath(pathname, path.dirname(__main__.__file__))
|
|
98
|
+
except ValueError:
|
|
99
|
+
# Windows has no relative path between drives, and a PyInstaller build unpacks the
|
|
100
|
+
# script to the system drive while the application may run from another one.
|
|
101
|
+
return pathname
|
|
@@ -19,6 +19,8 @@ DEFAULT_RAISE_EXCEPTIONS = False
|
|
|
19
19
|
DEFAULT_DROP_EXTRA_EVENTS = True
|
|
20
20
|
DEFAULT_INCLUDE_EXTRA_ATTRIBUTES = True
|
|
21
21
|
DEFAULT_TIMEOUT = 30
|
|
22
|
+
DEFAULT_FLUSH_TIMEOUT = 30
|
|
23
|
+
|
|
22
24
|
|
|
23
25
|
_handlers = weakref.WeakSet()
|
|
24
26
|
|
|
@@ -35,6 +37,7 @@ class LogtailHandler(logging.Handler):
|
|
|
35
37
|
include_extra_attributes=DEFAULT_INCLUDE_EXTRA_ATTRIBUTES,
|
|
36
38
|
context=DEFAULT_CONTEXT,
|
|
37
39
|
timeout=DEFAULT_TIMEOUT,
|
|
40
|
+
flush_timeout=DEFAULT_FLUSH_TIMEOUT,
|
|
38
41
|
level=logging.NOTSET):
|
|
39
42
|
super(LogtailHandler, self).__init__(level=level)
|
|
40
43
|
self.source_token = source_token
|
|
@@ -51,6 +54,7 @@ class LogtailHandler(logging.Handler):
|
|
|
51
54
|
self.flush_interval = flush_interval
|
|
52
55
|
self.check_interval = check_interval
|
|
53
56
|
self.raise_exceptions = raise_exceptions
|
|
57
|
+
self.flush_timeout = flush_timeout
|
|
54
58
|
self.dropcount = 0
|
|
55
59
|
# Do not initialize the flush thread yet because it causes issues on Render.
|
|
56
60
|
self.flush_thread = None
|
|
@@ -91,8 +95,16 @@ class LogtailHandler(logging.Handler):
|
|
|
91
95
|
raise e
|
|
92
96
|
|
|
93
97
|
def flush(self):
|
|
94
|
-
if self.flush_thread and self.flush_thread.is_alive():
|
|
95
|
-
|
|
98
|
+
if not (self.flush_thread and self.flush_thread.is_alive()):
|
|
99
|
+
return
|
|
100
|
+
if logging._lock._is_owned():
|
|
101
|
+
# logging.config.dictConfig() flushes the handlers it replaces while holding the global
|
|
102
|
+
# logging lock, and the worker's upload needs that lock whenever urllib3's logger has a
|
|
103
|
+
# level-cache miss, so waiting here could only deadlock. Nothing is lost: the worker
|
|
104
|
+
# keeps running and sends what is queued as soon as the lock is released.
|
|
105
|
+
return
|
|
106
|
+
if not self.flush_thread.flush(timeout=self.flush_timeout):
|
|
107
|
+
print('Gave up waiting for Better Stack uploads after {}s, logs are still buffered'.format(self.flush_timeout))
|
|
96
108
|
|
|
97
109
|
def _reset_after_fork(self):
|
|
98
110
|
# A forked child inherits the parent's queue with whatever was still buffered in it,
|
|
@@ -100,7 +112,7 @@ class LogtailHandler(logging.Handler):
|
|
|
100
112
|
# whose socket it shares with the parent, so it starts over with fresh ones.
|
|
101
113
|
self.pipe = queue.Queue(maxsize=self.buffer_capacity)
|
|
102
114
|
self.flush_thread = None
|
|
103
|
-
self.uploader
|
|
115
|
+
self.uploader.reset()
|
|
104
116
|
|
|
105
117
|
|
|
106
118
|
def _reset_handlers_after_fork():
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
from __future__ import print_function, unicode_literals
|
|
3
|
+
import os
|
|
4
|
+
import msgpack
|
|
5
|
+
import requests
|
|
6
|
+
import requests.utils
|
|
7
|
+
|
|
8
|
+
class Fake500(object):
|
|
9
|
+
def __init__(self, exception):
|
|
10
|
+
self.status_code = 500
|
|
11
|
+
self.exception = exception
|
|
12
|
+
|
|
13
|
+
class Uploader(object):
|
|
14
|
+
def __init__(self, source_token, host, timeout):
|
|
15
|
+
self.source_token = source_token
|
|
16
|
+
self.host = host
|
|
17
|
+
self.timeout = timeout
|
|
18
|
+
# requests would otherwise ask the environment, and on macOS SystemConfiguration, for
|
|
19
|
+
# proxy settings on every request. Resolving them once here keeps the flush thread out
|
|
20
|
+
# of CoreFoundation, which refuses to run in a child forked from a multithreaded
|
|
21
|
+
# process, such as an RQ work horse. The CA bundle variables follow the same rule.
|
|
22
|
+
self.proxies = requests.utils.get_environ_proxies(host)
|
|
23
|
+
self.verify = os.environ.get('REQUESTS_CA_BUNDLE') or os.environ.get('CURL_CA_BUNDLE') or True
|
|
24
|
+
self.headers = {
|
|
25
|
+
'Authorization': 'Bearer %s' % source_token,
|
|
26
|
+
'Content-Type': 'application/msgpack',
|
|
27
|
+
}
|
|
28
|
+
self.session = self._new_session()
|
|
29
|
+
|
|
30
|
+
def __call__(self, frame):
|
|
31
|
+
data = msgpack.packb(frame, use_bin_type=True)
|
|
32
|
+
try:
|
|
33
|
+
return self.session.post(self.host, data=data, headers=self.headers, timeout=self.timeout)
|
|
34
|
+
except requests.RequestException as e:
|
|
35
|
+
return Fake500(e)
|
|
36
|
+
|
|
37
|
+
def reset(self):
|
|
38
|
+
# A forked child shares the parent's pooled socket, so it needs a session of its own,
|
|
39
|
+
# built from the settings resolved above rather than looked up again.
|
|
40
|
+
self.session = self._new_session()
|
|
41
|
+
|
|
42
|
+
def _new_session(self):
|
|
43
|
+
session = requests.Session()
|
|
44
|
+
session.trust_env = False
|
|
45
|
+
session.proxies = self.proxies
|
|
46
|
+
session.verify = self.verify
|
|
47
|
+
return session
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logtail-python
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: Better Stack client library
|
|
5
5
|
Home-page: https://github.com/logtail/logtail-python
|
|
6
|
-
Download-URL: https://github.com/logtail/logtail-python/tarball/0.4.
|
|
6
|
+
Download-URL: https://github.com/logtail/logtail-python/tarball/0.4.3
|
|
7
7
|
Author: Logtail
|
|
8
8
|
Author-email: hello@logtail.com
|
|
9
9
|
License: ISC
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.11
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.12
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.13
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
20
21
|
Classifier: Programming Language :: Python
|
|
21
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
23
|
Requires-Python: >=3.10
|
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
from setuptools import setup
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
VERSION = '0.4.
|
|
6
|
+
VERSION = '0.4.3'
|
|
7
7
|
ROOT_DIR = os.path.dirname(__file__)
|
|
8
8
|
|
|
9
9
|
REQUIREMENTS = [
|
|
@@ -40,6 +40,7 @@ setup(
|
|
|
40
40
|
'Programming Language :: Python :: 3.12',
|
|
41
41
|
'Programming Language :: Python :: 3.13',
|
|
42
42
|
'Programming Language :: Python :: 3.14',
|
|
43
|
+
'Programming Language :: Python :: 3.15',
|
|
43
44
|
'Programming Language :: Python',
|
|
44
45
|
'Topic :: Software Development :: Libraries :: Python Modules',
|
|
45
46
|
],
|
|
@@ -233,3 +233,33 @@ class TestFlushWorker(unittest.TestCase):
|
|
|
233
233
|
self.assertFalse(threading.excepthook.called)
|
|
234
234
|
|
|
235
235
|
threading.excepthook = original_excepthook
|
|
236
|
+
|
|
237
|
+
def test_flush_returns_immediately_on_empty_clean_queue(self):
|
|
238
|
+
_, _, fw = self._setup_worker()
|
|
239
|
+
# _clean defaults to True and pipe is empty — flush should return at once.
|
|
240
|
+
t1 = time.time()
|
|
241
|
+
result = fw.flush()
|
|
242
|
+
elapsed = time.time() - t1
|
|
243
|
+
self.assertTrue(result)
|
|
244
|
+
self.assertLess(elapsed, 0.1)
|
|
245
|
+
|
|
246
|
+
def test_flush_with_timeout_returns_false_when_queue_not_drained(self):
|
|
247
|
+
pipe, _, fw = self._setup_worker()
|
|
248
|
+
# Put an item in but never start the worker thread → queue stays full,
|
|
249
|
+
# so flush() will spin until the timeout fires.
|
|
250
|
+
pipe.put(object(), block=False)
|
|
251
|
+
|
|
252
|
+
t1 = time.time()
|
|
253
|
+
result = fw.flush(timeout=0.05)
|
|
254
|
+
elapsed = time.time() - t1
|
|
255
|
+
|
|
256
|
+
self.assertFalse(result)
|
|
257
|
+
# Bounded: should return shortly after the timeout, not block forever.
|
|
258
|
+
self.assertLess(elapsed, 1.0)
|
|
259
|
+
self.assertGreaterEqual(elapsed, 0.04)
|
|
260
|
+
|
|
261
|
+
def test_flush_clears_flushing_flag_even_on_timeout(self):
|
|
262
|
+
pipe, _, fw = self._setup_worker()
|
|
263
|
+
pipe.put(object(), block=False)
|
|
264
|
+
fw.flush(timeout=0.05)
|
|
265
|
+
self.assertFalse(fw._flushing)
|
|
@@ -4,9 +4,12 @@ from logtail.frame import create_frame
|
|
|
4
4
|
from logtail.handler import LogtailHandler
|
|
5
5
|
from logtail.helpers import LogtailContext
|
|
6
6
|
import datetime
|
|
7
|
+
import ntpath
|
|
7
8
|
import unittest
|
|
8
9
|
import logging
|
|
9
10
|
|
|
11
|
+
from unittest.mock import patch
|
|
12
|
+
|
|
10
13
|
class TestLogtailLogEntry(unittest.TestCase):
|
|
11
14
|
def test_create_frame_happy_path(self):
|
|
12
15
|
handler = LogtailHandler(source_token="some-source-token")
|
|
@@ -36,3 +39,15 @@ class TestLogtailLogEntry(unittest.TestCase):
|
|
|
36
39
|
|
|
37
40
|
frame = create_frame(log_record, log_record.getMessage(), LogtailContext(), include_extra_attributes=True)
|
|
38
41
|
self.assertIn('non_dict_key', frame)
|
|
42
|
+
|
|
43
|
+
@patch('logtail.frame.path', ntpath)
|
|
44
|
+
@patch('logtail.frame.__main__')
|
|
45
|
+
def test_create_frame_keeps_the_absolute_path_when_the_file_is_on_another_drive(self, main):
|
|
46
|
+
# PyInstaller unpacks the script to the system drive while the application's files stay
|
|
47
|
+
# on the drive it was started from, and Windows has no relative path between drives.
|
|
48
|
+
main.__file__ = 'C:\\Users\\zi\\AppData\\Local\\Temp\\_MEI61722\\main.py'
|
|
49
|
+
log_record = logging.LogRecord("logtail-test", 20, 'D:\\workspace\\app\\main.py', 10, "Some log message", [], None)
|
|
50
|
+
|
|
51
|
+
frame = create_frame(log_record, log_record.getMessage(), LogtailContext())
|
|
52
|
+
|
|
53
|
+
self.assertEqual(frame['context']['runtime']['file'], 'D:\\workspace\\app\\main.py')
|
|
@@ -27,11 +27,49 @@ class TestLogtailHandler(unittest.TestCase):
|
|
|
27
27
|
# Test default timeout
|
|
28
28
|
handler = LogtailHandler(source_token=self.source_token, host=self.host)
|
|
29
29
|
self.assertEqual(handler.uploader.timeout, 30)
|
|
30
|
-
|
|
30
|
+
|
|
31
31
|
# Test custom timeout
|
|
32
32
|
handler = LogtailHandler(source_token=self.source_token, host=self.host, timeout=10)
|
|
33
33
|
self.assertEqual(handler.uploader.timeout, 10)
|
|
34
34
|
|
|
35
|
+
@patch('logtail.handler.FlushWorker')
|
|
36
|
+
def test_handler_stores_flush_timeout(self, MockWorker):
|
|
37
|
+
# Default
|
|
38
|
+
handler = LogtailHandler(source_token=self.source_token, host=self.host)
|
|
39
|
+
self.assertEqual(handler.flush_timeout, 30)
|
|
40
|
+
# Custom
|
|
41
|
+
handler = LogtailHandler(source_token=self.source_token, host=self.host, flush_timeout=2)
|
|
42
|
+
self.assertEqual(handler.flush_timeout, 2)
|
|
43
|
+
# Explicit None disables the timeout (legacy unbounded wait)
|
|
44
|
+
handler = LogtailHandler(source_token=self.source_token, host=self.host, flush_timeout=None)
|
|
45
|
+
self.assertIsNone(handler.flush_timeout)
|
|
46
|
+
|
|
47
|
+
@patch('logtail.handler.FlushWorker')
|
|
48
|
+
def test_handler_passes_flush_timeout_to_worker(self, MockWorker):
|
|
49
|
+
handler = LogtailHandler(source_token=self.source_token, host=self.host, flush_timeout=7)
|
|
50
|
+
# Trigger flush_thread creation by emitting once.
|
|
51
|
+
logger = logging.getLogger(__name__)
|
|
52
|
+
logger.handlers = []
|
|
53
|
+
logger.addHandler(handler)
|
|
54
|
+
logger.critical('hello')
|
|
55
|
+
# The mocked FlushWorker is alive by default; flush() forwards timeout.
|
|
56
|
+
handler.flush()
|
|
57
|
+
handler.flush_thread.flush.assert_called_with(timeout=7)
|
|
58
|
+
|
|
59
|
+
@patch('logtail.handler.print', create=True)
|
|
60
|
+
@patch('logtail.handler.FlushWorker')
|
|
61
|
+
def test_flush_reports_giving_up(self, MockWorker, mock_print):
|
|
62
|
+
handler = LogtailHandler(source_token=self.source_token, host=self.host, flush_timeout=3)
|
|
63
|
+
logger = logging.getLogger(__name__)
|
|
64
|
+
logger.handlers = []
|
|
65
|
+
logger.addHandler(handler)
|
|
66
|
+
logger.critical('hello')
|
|
67
|
+
handler.flush_thread.flush.return_value = False
|
|
68
|
+
|
|
69
|
+
handler.flush()
|
|
70
|
+
|
|
71
|
+
mock_print.assert_called_once_with('Gave up waiting for Better Stack uploads after 3s, logs are still buffered')
|
|
72
|
+
|
|
35
73
|
@patch('logtail.handler.FlushWorker')
|
|
36
74
|
def test_handler_creates_pipe_from_args(self, MockWorker):
|
|
37
75
|
buffer_capacity = 9
|
|
@@ -285,11 +323,15 @@ class TestLogtailHandler(unittest.TestCase):
|
|
|
285
323
|
logger.handlers = []
|
|
286
324
|
logger.addHandler(handler)
|
|
287
325
|
logger.critical('queued in the parent')
|
|
288
|
-
|
|
326
|
+
uploader = handler.uploader
|
|
327
|
+
parent_session = uploader.session
|
|
289
328
|
|
|
290
329
|
pid = os.fork()
|
|
291
330
|
if pid == 0:
|
|
292
|
-
|
|
331
|
+
# The child gets its own session, but never resolves proxy settings again: on macOS
|
|
332
|
+
# that means SystemConfiguration, which refuses to run in a forked child.
|
|
333
|
+
fresh = (handler.pipe.empty() and handler.flush_thread is None and handler.uploader is uploader
|
|
334
|
+
and uploader.session is not parent_session and uploader.session.trust_env is False)
|
|
293
335
|
os._exit(0 if fresh else 1)
|
|
294
336
|
_, status = os.waitpid(pid, 0)
|
|
295
337
|
|
|
@@ -369,6 +411,35 @@ class TestLogtailHandler(unittest.TestCase):
|
|
|
369
411
|
self.assertEqual(uploads, [['hello']])
|
|
370
412
|
self.assertTrue(handler.pipe.empty())
|
|
371
413
|
|
|
414
|
+
def test_flush_does_not_wait_while_the_caller_holds_the_logging_lock(self):
|
|
415
|
+
# logging.config.dictConfig() flushes the handlers it replaces while holding the global
|
|
416
|
+
# logging lock, and the worker's upload needs that lock whenever urllib3's logger has a
|
|
417
|
+
# level-cache miss, so waiting for the worker there can only deadlock.
|
|
418
|
+
logger = logging.getLogger(__name__)
|
|
419
|
+
logger.handlers = []
|
|
420
|
+
logger.setLevel(logging.INFO)
|
|
421
|
+
handler = LogtailHandler(source_token=self.source_token, flush_interval=0.01, check_interval=0.01, flush_timeout=2)
|
|
422
|
+
uploads = []
|
|
423
|
+
in_upload = threading.Event()
|
|
424
|
+
|
|
425
|
+
def upload(frame):
|
|
426
|
+
in_upload.set()
|
|
427
|
+
with logging._lock: # what urllib3's isEnabledFor() does on a level-cache miss
|
|
428
|
+
uploads.append([f['message'] for f in frame])
|
|
429
|
+
return mock.MagicMock(status_code=202)
|
|
430
|
+
|
|
431
|
+
handler.uploader = upload
|
|
432
|
+
logger.addHandler(handler)
|
|
433
|
+
self.addCleanup(self._stop_flush_worker, handler)
|
|
434
|
+
|
|
435
|
+
with logging._lock:
|
|
436
|
+
logger.info('hello')
|
|
437
|
+
self.assertTrue(in_upload.wait(2))
|
|
438
|
+
started = time.time()
|
|
439
|
+
handler.flush()
|
|
440
|
+
self.assertLess(time.time() - started, 0.5, 'flush() waited for a worker that needs the lock we hold')
|
|
441
|
+
self._wait_until(lambda: uploads == [['hello']])
|
|
442
|
+
|
|
372
443
|
def _wait_until(self, condition):
|
|
373
444
|
deadline = time.time() + 2
|
|
374
445
|
while not condition() and time.time() < deadline:
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
from __future__ import print_function, unicode_literals
|
|
3
|
+
import msgpack
|
|
4
|
+
import mock
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
from unittest.mock import patch
|
|
8
|
+
|
|
9
|
+
from logtail.uploader import Uploader
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestUploader(unittest.TestCase):
|
|
13
|
+
host = 'https://in.logtail.com'
|
|
14
|
+
source_token = 'dummy_source_token'
|
|
15
|
+
frame = [1, 2, 3]
|
|
16
|
+
timeout = 30
|
|
17
|
+
|
|
18
|
+
@patch('logtail.uploader.requests.Session.post')
|
|
19
|
+
def test_call(self, post):
|
|
20
|
+
def mock_post(endpoint, data=None, headers=None, timeout=None):
|
|
21
|
+
# Check that the data is sent to ther correct endpoint
|
|
22
|
+
self.assertEqual(endpoint, self.host)
|
|
23
|
+
# Check the content-type
|
|
24
|
+
self.assertIsInstance(headers, dict)
|
|
25
|
+
self.assertIn('Authorization', headers)
|
|
26
|
+
self.assertEqual('application/msgpack', headers.get('Content-Type'))
|
|
27
|
+
# Check the content was msgpacked correctly
|
|
28
|
+
self.assertEqual(msgpack.unpackb(data, raw=False), self.frame)
|
|
29
|
+
# Check that timeout is passed to the request
|
|
30
|
+
self.assertEqual(timeout, 30)
|
|
31
|
+
|
|
32
|
+
post.side_effect = mock_post
|
|
33
|
+
u = Uploader(self.source_token, self.host, self.timeout)
|
|
34
|
+
u(self.frame)
|
|
35
|
+
|
|
36
|
+
self.assertTrue(post.called)
|
|
37
|
+
|
|
38
|
+
@patch('logtail.uploader.requests.utils.get_environ_proxies', return_value={'https': 'http://proxy.internal:3128'})
|
|
39
|
+
def test_resolves_proxy_settings_once_instead_of_on_every_request(self, get_environ_proxies):
|
|
40
|
+
u = Uploader(self.source_token, self.host, self.timeout)
|
|
41
|
+
|
|
42
|
+
with patch('logtail.uploader.requests.Session.post'):
|
|
43
|
+
u(self.frame)
|
|
44
|
+
u(self.frame)
|
|
45
|
+
|
|
46
|
+
get_environ_proxies.assert_called_once_with(self.host)
|
|
47
|
+
self.assertEqual(u.session.proxies, {'https': 'http://proxy.internal:3128'})
|
|
48
|
+
self.assertFalse(u.session.trust_env)
|
|
49
|
+
|
|
50
|
+
@patch.dict('os.environ', {'REQUESTS_CA_BUNDLE': '/etc/ssl/corporate-ca.pem'})
|
|
51
|
+
def test_uses_the_ca_bundle_from_the_environment(self):
|
|
52
|
+
u = Uploader(self.source_token, self.host, self.timeout)
|
|
53
|
+
|
|
54
|
+
self.assertEqual(u.session.verify, '/etc/ssl/corporate-ca.pem')
|
|
55
|
+
|
|
56
|
+
@patch('logtail.uploader.requests.utils.get_environ_proxies', return_value={'https': 'http://proxy.internal:3128'})
|
|
57
|
+
def test_reset_replaces_the_session_but_keeps_the_resolved_settings(self, get_environ_proxies):
|
|
58
|
+
u = Uploader(self.source_token, self.host, self.timeout)
|
|
59
|
+
session = u.session
|
|
60
|
+
|
|
61
|
+
u.reset()
|
|
62
|
+
|
|
63
|
+
self.assertIsNot(u.session, session)
|
|
64
|
+
self.assertEqual(u.session.proxies, {'https': 'http://proxy.internal:3128'})
|
|
65
|
+
self.assertFalse(u.session.trust_env)
|
|
66
|
+
get_environ_proxies.assert_called_once_with(self.host)
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
from __future__ import print_function, unicode_literals
|
|
3
|
-
import msgpack
|
|
4
|
-
import requests
|
|
5
|
-
|
|
6
|
-
class Fake500(object):
|
|
7
|
-
def __init__(self, exception):
|
|
8
|
-
self.status_code = 500
|
|
9
|
-
self.exception = exception
|
|
10
|
-
|
|
11
|
-
class Uploader(object):
|
|
12
|
-
def __init__(self, source_token, host, timeout):
|
|
13
|
-
self.source_token = source_token
|
|
14
|
-
self.host = host
|
|
15
|
-
self.timeout = timeout
|
|
16
|
-
self.session = requests.Session()
|
|
17
|
-
self.headers = {
|
|
18
|
-
'Authorization': 'Bearer %s' % source_token,
|
|
19
|
-
'Content-Type': 'application/msgpack',
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
def __call__(self, frame):
|
|
23
|
-
data = msgpack.packb(frame, use_bin_type=True)
|
|
24
|
-
try:
|
|
25
|
-
return self.session.post(self.host, data=data, headers=self.headers, timeout=self.timeout)
|
|
26
|
-
except requests.RequestException as e:
|
|
27
|
-
return Fake500(e)
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# coding: utf-8
|
|
2
|
-
from __future__ import print_function, unicode_literals
|
|
3
|
-
import msgpack
|
|
4
|
-
import mock
|
|
5
|
-
import unittest
|
|
6
|
-
|
|
7
|
-
from unittest.mock import patch
|
|
8
|
-
|
|
9
|
-
from logtail.uploader import Uploader
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class TestUploader(unittest.TestCase):
|
|
13
|
-
host = 'https://in.logtail.com'
|
|
14
|
-
source_token = 'dummy_source_token'
|
|
15
|
-
frame = [1, 2, 3]
|
|
16
|
-
timeout = 30
|
|
17
|
-
|
|
18
|
-
@patch('logtail.uploader.requests.Session.post')
|
|
19
|
-
def test_call(self, post):
|
|
20
|
-
def mock_post(endpoint, data=None, headers=None, timeout=None):
|
|
21
|
-
# Check that the data is sent to ther correct endpoint
|
|
22
|
-
self.assertEqual(endpoint, self.host)
|
|
23
|
-
# Check the content-type
|
|
24
|
-
self.assertIsInstance(headers, dict)
|
|
25
|
-
self.assertIn('Authorization', headers)
|
|
26
|
-
self.assertEqual('application/msgpack', headers.get('Content-Type'))
|
|
27
|
-
# Check the content was msgpacked correctly
|
|
28
|
-
self.assertEqual(msgpack.unpackb(data, raw=False), self.frame)
|
|
29
|
-
# Check that timeout is passed to the request
|
|
30
|
-
self.assertEqual(timeout, 30)
|
|
31
|
-
|
|
32
|
-
post.side_effect = mock_post
|
|
33
|
-
u = Uploader(self.source_token, self.host, self.timeout)
|
|
34
|
-
u(self.frame)
|
|
35
|
-
|
|
36
|
-
self.assertTrue(post.called)
|
|
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
|