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.
Files changed (32) hide show
  1. {logtail_python-0.4.1 → logtail_python-0.4.3}/PKG-INFO +3 -2
  2. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/__init__.py +1 -1
  3. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/flusher.py +23 -4
  4. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/frame.py +6 -1
  5. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/handler.py +15 -3
  6. logtail_python-0.4.3/logtail/uploader.py +47 -0
  7. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/PKG-INFO +3 -2
  8. {logtail_python-0.4.1 → logtail_python-0.4.3}/setup.py +2 -1
  9. {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_flusher.py +30 -0
  10. {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_frame.py +15 -0
  11. {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_handler.py +74 -3
  12. logtail_python-0.4.3/tests/test_uploader.py +66 -0
  13. logtail_python-0.4.1/logtail/uploader.py +0 -27
  14. logtail_python-0.4.1/tests/test_uploader.py +0 -36
  15. {logtail_python-0.4.1 → logtail_python-0.4.3}/LICENSE.md +0 -0
  16. {logtail_python-0.4.1 → logtail_python-0.4.3}/MANIFEST.in +0 -0
  17. {logtail_python-0.4.1 → logtail_python-0.4.3}/README.md +0 -0
  18. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/compat.py +0 -0
  19. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/formatter.py +0 -0
  20. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/helpers.py +0 -0
  21. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail/rq.py +0 -0
  22. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/SOURCES.txt +0 -0
  23. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/dependency_links.txt +0 -0
  24. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/requires.txt +0 -0
  25. {logtail_python-0.4.1 → logtail_python-0.4.3}/logtail_python.egg-info/top_level.txt +0 -0
  26. {logtail_python-0.4.1 → logtail_python-0.4.3}/requirements.txt +0 -0
  27. {logtail_python-0.4.1 → logtail_python-0.4.3}/setup.cfg +0 -0
  28. {logtail_python-0.4.1 → logtail_python-0.4.3}/test-requirements.txt +0 -0
  29. {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/__init__.py +0 -0
  30. {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_formatter.py +0 -0
  31. {logtail_python-0.4.1 → logtail_python-0.4.3}/tests/test_helpers.py +0 -0
  32. {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.1
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.1
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
@@ -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.4.1'
8
+ __version__ = '0.4.3'
9
9
 
10
10
  context = DEFAULT_CONTEXT
@@ -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
- while not self._clean or not self.pipe.empty():
106
- time.sleep(self.check_interval)
107
- self._flushing = False
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
- return path.relpath(pathname, path.dirname(__main__.__file__))
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
- self.flush_thread.flush()
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 = Uploader(self.source_token, self.host, self.uploader.timeout)
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.1
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.1
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.1'
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
- parent_session = handler.uploader.session
326
+ uploader = handler.uploader
327
+ parent_session = uploader.session
289
328
 
290
329
  pid = os.fork()
291
330
  if pid == 0:
292
- fresh = handler.pipe.empty() and handler.flush_thread is None and handler.uploader.session is not parent_session
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