logtail-python 0.4.0__tar.gz → 0.4.2__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.0/logtail_python.egg-info → logtail_python-0.4.2}/PKG-INFO +3 -2
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail/__init__.py +1 -1
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail/flusher.py +42 -6
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail/handler.py +34 -3
- logtail_python-0.4.2/logtail/rq.py +29 -0
- logtail_python-0.4.2/logtail/uploader.py +47 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2/logtail_python.egg-info}/PKG-INFO +3 -2
- logtail_python-0.4.2/logtail_python.egg-info/SOURCES.txt +29 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/setup.py +2 -1
- {logtail_python-0.4.0 → logtail_python-0.4.2}/test-requirements.txt +1 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/tests/test_flusher.py +73 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/tests/test_handler.py +145 -1
- logtail_python-0.4.2/tests/test_rq.py +81 -0
- logtail_python-0.4.2/tests/test_uploader.py +66 -0
- logtail_python-0.4.0/logtail/uploader.py +0 -27
- logtail_python-0.4.0/logtail_python.egg-info/SOURCES.txt +0 -42
- logtail_python-0.4.0/tests/__pycache__/__init__.cpython-311.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/__init__.cpython-314.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_flusher.cpython-311.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_flusher.cpython-314-pytest-9.1.1.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_flusher.cpython-314.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_formatter.cpython-311.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_formatter.cpython-314-pytest-9.1.1.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_frame.cpython-311.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_frame.cpython-314-pytest-9.1.1.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_handler.cpython-311.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_handler.cpython-314-pytest-9.1.1.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_helpers.cpython-311.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_helpers.cpython-314-pytest-9.1.1.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_uploader.cpython-311.pyc +0 -0
- logtail_python-0.4.0/tests/__pycache__/test_uploader.cpython-314-pytest-9.1.1.pyc +0 -0
- logtail_python-0.4.0/tests/test_uploader.py +0 -36
- {logtail_python-0.4.0 → logtail_python-0.4.2}/LICENSE.md +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/MANIFEST.in +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/README.md +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail/compat.py +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail/formatter.py +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail/frame.py +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail/helpers.py +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail_python.egg-info/dependency_links.txt +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail_python.egg-info/requires.txt +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/logtail_python.egg-info/top_level.txt +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/requirements.txt +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/setup.cfg +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/tests/__init__.py +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/tests/test_formatter.py +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/tests/test_frame.py +0 -0
- {logtail_python-0.4.0 → logtail_python-0.4.2}/tests/test_helpers.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.2
|
|
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.2
|
|
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
|
|
@@ -9,6 +9,14 @@ from .compat import queue
|
|
|
9
9
|
RETRY_SCHEDULE = (1, 10, 60) # seconds
|
|
10
10
|
|
|
11
11
|
|
|
12
|
+
class TransportFrame(dict):
|
|
13
|
+
"""A record logged by a flush worker itself, i.e. urllib3 describing our own upload.
|
|
14
|
+
|
|
15
|
+
It rides along with the next batch of regular records but never triggers an upload
|
|
16
|
+
on its own, since that upload would log again and the queue would never drain.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
|
|
12
20
|
class FlushWorker(threading.Thread):
|
|
13
21
|
def __init__(self, upload, pipe, buffer_capacity, flush_interval, check_interval):
|
|
14
22
|
threading.Thread.__init__(self)
|
|
@@ -71,7 +79,7 @@ class FlushWorker(threading.Thread):
|
|
|
71
79
|
# count) and sends them to the Better Stack endpoint all at once. If the
|
|
72
80
|
# request fails in a way that can be retried, it is retried with an
|
|
73
81
|
# exponential backoff in between attempts.
|
|
74
|
-
if frame:
|
|
82
|
+
if frame and not all(isinstance(entry, TransportFrame) for entry in frame):
|
|
75
83
|
response = None
|
|
76
84
|
for delay in RETRY_SCHEDULE + (None, ):
|
|
77
85
|
response = self.upload(frame)
|
|
@@ -80,18 +88,46 @@ class FlushWorker(threading.Thread):
|
|
|
80
88
|
if delay is not None:
|
|
81
89
|
time.sleep(delay)
|
|
82
90
|
|
|
83
|
-
|
|
91
|
+
# A network error surfaces as a Fake500 that carries an `exception`
|
|
92
|
+
# attribute; a genuine server HTTP 500 is a real `requests.Response`
|
|
93
|
+
# that has no such attribute. Use a default so the latter doesn't
|
|
94
|
+
# raise AttributeError here and kill the flush thread (dropping all
|
|
95
|
+
# subsequent logs for the life of the process).
|
|
96
|
+
if response.status_code == 500 and getattr(response, "exception", None) is not None:
|
|
84
97
|
print('Failed to send logs to Better Stack after {} retries: {}'.format(len(RETRY_SCHEDULE), response.exception))
|
|
85
98
|
|
|
86
99
|
self._clean = True
|
|
87
100
|
if shutdown and self.pipe.empty():
|
|
88
101
|
self.should_run = False
|
|
89
102
|
|
|
90
|
-
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
|
+
"""
|
|
91
117
|
self._flushing = True
|
|
92
|
-
|
|
93
|
-
time.
|
|
94
|
-
|
|
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
|
|
127
|
+
|
|
128
|
+
def in_flush_worker():
|
|
129
|
+
return isinstance(threading.current_thread(), FlushWorker)
|
|
130
|
+
|
|
95
131
|
|
|
96
132
|
def _initial_time_remaining(flush_interval):
|
|
97
133
|
return flush_interval
|
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
from __future__ import print_function, unicode_literals
|
|
3
3
|
import logging
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
6
|
+
import weakref
|
|
5
7
|
|
|
6
8
|
from .compat import queue
|
|
7
9
|
from .helpers import DEFAULT_CONTEXT
|
|
8
|
-
from .flusher import FlushWorker
|
|
10
|
+
from .flusher import FlushWorker, TransportFrame, in_flush_worker
|
|
9
11
|
from .uploader import Uploader
|
|
10
12
|
from .frame import create_frame
|
|
11
13
|
|
|
@@ -17,6 +19,10 @@ DEFAULT_RAISE_EXCEPTIONS = False
|
|
|
17
19
|
DEFAULT_DROP_EXTRA_EVENTS = True
|
|
18
20
|
DEFAULT_INCLUDE_EXTRA_ATTRIBUTES = True
|
|
19
21
|
DEFAULT_TIMEOUT = 30
|
|
22
|
+
DEFAULT_FLUSH_TIMEOUT = 30
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_handlers = weakref.WeakSet()
|
|
20
26
|
|
|
21
27
|
|
|
22
28
|
class LogtailHandler(logging.Handler):
|
|
@@ -31,6 +37,7 @@ class LogtailHandler(logging.Handler):
|
|
|
31
37
|
include_extra_attributes=DEFAULT_INCLUDE_EXTRA_ATTRIBUTES,
|
|
32
38
|
context=DEFAULT_CONTEXT,
|
|
33
39
|
timeout=DEFAULT_TIMEOUT,
|
|
40
|
+
flush_timeout=DEFAULT_FLUSH_TIMEOUT,
|
|
34
41
|
level=logging.NOTSET):
|
|
35
42
|
super(LogtailHandler, self).__init__(level=level)
|
|
36
43
|
self.source_token = source_token
|
|
@@ -47,9 +54,11 @@ class LogtailHandler(logging.Handler):
|
|
|
47
54
|
self.flush_interval = flush_interval
|
|
48
55
|
self.check_interval = check_interval
|
|
49
56
|
self.raise_exceptions = raise_exceptions
|
|
57
|
+
self.flush_timeout = flush_timeout
|
|
50
58
|
self.dropcount = 0
|
|
51
59
|
# Do not initialize the flush thread yet because it causes issues on Render.
|
|
52
60
|
self.flush_thread = None
|
|
61
|
+
_handlers.add(self)
|
|
53
62
|
|
|
54
63
|
def ensure_flush_thread_alive(self):
|
|
55
64
|
if self.flush_thread and self.flush_thread.is_alive():
|
|
@@ -71,8 +80,12 @@ class LogtailHandler(logging.Handler):
|
|
|
71
80
|
message = self.format(record)
|
|
72
81
|
frame = create_frame(record, message, self.context, include_extra_attributes=self.include_extra_attributes)
|
|
73
82
|
serializable_frame = json.loads(json.dumps(frame, default=str))
|
|
83
|
+
transport = in_flush_worker()
|
|
84
|
+
if transport:
|
|
85
|
+
serializable_frame = TransportFrame(serializable_frame)
|
|
74
86
|
try:
|
|
75
|
-
|
|
87
|
+
# The flush worker must never block on its own queue, so its records are dropped when full.
|
|
88
|
+
self.pipe.put(serializable_frame, block=(not self.drop_extra_events) and not transport)
|
|
76
89
|
except queue.Full:
|
|
77
90
|
# Only raised when not blocking, which means that extra events
|
|
78
91
|
# should be dropped.
|
|
@@ -83,4 +96,22 @@ class LogtailHandler(logging.Handler):
|
|
|
83
96
|
|
|
84
97
|
def flush(self):
|
|
85
98
|
if self.flush_thread and self.flush_thread.is_alive():
|
|
86
|
-
|
|
99
|
+
if not self.flush_thread.flush(timeout=self.flush_timeout):
|
|
100
|
+
print('Gave up waiting for Better Stack uploads after {}s, logs are still buffered'.format(self.flush_timeout))
|
|
101
|
+
|
|
102
|
+
def _reset_after_fork(self):
|
|
103
|
+
# A forked child inherits the parent's queue with whatever was still buffered in it,
|
|
104
|
+
# a flush thread object whose thread does not exist in the child, and an HTTP session
|
|
105
|
+
# whose socket it shares with the parent, so it starts over with fresh ones.
|
|
106
|
+
self.pipe = queue.Queue(maxsize=self.buffer_capacity)
|
|
107
|
+
self.flush_thread = None
|
|
108
|
+
self.uploader.reset()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _reset_handlers_after_fork():
|
|
112
|
+
for handler in _handlers:
|
|
113
|
+
handler._reset_after_fork()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
if hasattr(os, 'register_at_fork'):
|
|
117
|
+
os.register_at_fork(after_in_child=_reset_handlers_after_fork)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
from __future__ import print_function, unicode_literals
|
|
3
|
+
import logging
|
|
4
|
+
|
|
5
|
+
import rq
|
|
6
|
+
|
|
7
|
+
from .handler import LogtailHandler
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Worker(rq.Worker):
|
|
11
|
+
"""An RQ worker that sends buffered log records after every job.
|
|
12
|
+
|
|
13
|
+
RQ runs each job in a forked work horse and ends it with os._exit(), which skips the
|
|
14
|
+
interpreter shutdown that would otherwise let LogtailHandler flush. Use it with
|
|
15
|
+
`rq worker --worker-class logtail.rq.Worker` or `RQ = {"WORKER_CLASS": "logtail.rq.Worker"}`
|
|
16
|
+
in django-rq.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def perform_job(self, job, queue):
|
|
20
|
+
try:
|
|
21
|
+
return super(Worker, self).perform_job(job, queue)
|
|
22
|
+
finally:
|
|
23
|
+
for handler in _logtail_handlers():
|
|
24
|
+
handler.flush()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _logtail_handlers():
|
|
28
|
+
loggers = [logging.getLogger()] + [logger for logger in logging.Logger.manager.loggerDict.values() if isinstance(logger, logging.Logger)]
|
|
29
|
+
return {handler for logger in loggers for handler in logger.handlers if isinstance(handler, LogtailHandler)}
|
|
@@ -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.2
|
|
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.2
|
|
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
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
LICENSE.md
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
requirements.txt
|
|
5
|
+
setup.cfg
|
|
6
|
+
setup.py
|
|
7
|
+
test-requirements.txt
|
|
8
|
+
logtail/__init__.py
|
|
9
|
+
logtail/compat.py
|
|
10
|
+
logtail/flusher.py
|
|
11
|
+
logtail/formatter.py
|
|
12
|
+
logtail/frame.py
|
|
13
|
+
logtail/handler.py
|
|
14
|
+
logtail/helpers.py
|
|
15
|
+
logtail/rq.py
|
|
16
|
+
logtail/uploader.py
|
|
17
|
+
logtail_python.egg-info/PKG-INFO
|
|
18
|
+
logtail_python.egg-info/SOURCES.txt
|
|
19
|
+
logtail_python.egg-info/dependency_links.txt
|
|
20
|
+
logtail_python.egg-info/requires.txt
|
|
21
|
+
logtail_python.egg-info/top_level.txt
|
|
22
|
+
tests/__init__.py
|
|
23
|
+
tests/test_flusher.py
|
|
24
|
+
tests/test_formatter.py
|
|
25
|
+
tests/test_frame.py
|
|
26
|
+
tests/test_handler.py
|
|
27
|
+
tests/test_helpers.py
|
|
28
|
+
tests/test_rq.py
|
|
29
|
+
tests/test_uploader.py
|
|
@@ -3,7 +3,7 @@ import os
|
|
|
3
3
|
from setuptools import setup
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
VERSION = '0.4.
|
|
6
|
+
VERSION = '0.4.2'
|
|
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
|
],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
from __future__ import print_function, unicode_literals
|
|
3
3
|
import mock
|
|
4
|
+
import requests
|
|
4
5
|
import time
|
|
5
6
|
import threading
|
|
6
7
|
import unittest
|
|
@@ -123,6 +124,48 @@ class TestFlushWorker(unittest.TestCase):
|
|
|
123
124
|
self.assertEqual(self.uploader_calls, len(RETRY_SCHEDULE) + 1)
|
|
124
125
|
self.assertEqual(self.sleep_calls, len(RETRY_SCHEDULE))
|
|
125
126
|
|
|
127
|
+
@patch('logtail.flusher.time.sleep')
|
|
128
|
+
def test_step_does_not_crash_on_real_server_500(self, _mock_sleep):
|
|
129
|
+
# Regression: a genuine server HTTP 500 returns a real requests.Response,
|
|
130
|
+
# which (unlike the network-error Fake500) has no `.exception` attribute.
|
|
131
|
+
# `getattr(response, "exception")` without a default raised AttributeError
|
|
132
|
+
# here, which propagates out of step()/run() and kills the flush thread —
|
|
133
|
+
# every subsequent log is then queued but never sent. (Note: MagicMock
|
|
134
|
+
# would auto-vivify `.exception` and hide the bug, so use a real Response.)
|
|
135
|
+
first_frame = list(range(self.buffer_capacity))
|
|
136
|
+
|
|
137
|
+
def uploader(frame):
|
|
138
|
+
resp = requests.models.Response()
|
|
139
|
+
resp.status_code = 500
|
|
140
|
+
return resp
|
|
141
|
+
|
|
142
|
+
pipe, _, fw = self._setup_worker(uploader)
|
|
143
|
+
for log in first_frame:
|
|
144
|
+
pipe.put(log, block=False)
|
|
145
|
+
|
|
146
|
+
fw.step() # must not raise AttributeError
|
|
147
|
+
|
|
148
|
+
self.assertTrue(fw._clean)
|
|
149
|
+
|
|
150
|
+
@patch('logtail.flusher.time.sleep')
|
|
151
|
+
def test_step_still_logs_network_error_fake500(self, _mock_sleep):
|
|
152
|
+
# The network-error path (Fake500 carrying an exception) must still log.
|
|
153
|
+
from logtail.uploader import Fake500
|
|
154
|
+
|
|
155
|
+
first_frame = list(range(self.buffer_capacity))
|
|
156
|
+
|
|
157
|
+
def uploader(frame):
|
|
158
|
+
return Fake500(Exception('network is down'))
|
|
159
|
+
|
|
160
|
+
pipe, _, fw = self._setup_worker(uploader)
|
|
161
|
+
for log in first_frame:
|
|
162
|
+
pipe.put(log, block=False)
|
|
163
|
+
|
|
164
|
+
with patch('builtins.print') as mock_print:
|
|
165
|
+
fw.step()
|
|
166
|
+
|
|
167
|
+
mock_print.assert_called_once()
|
|
168
|
+
|
|
126
169
|
def test_shutdown_condition_empties_queue_and_shuts_down(self):
|
|
127
170
|
self.buffer_capacity = 10
|
|
128
171
|
num_items = 5
|
|
@@ -190,3 +233,33 @@ class TestFlushWorker(unittest.TestCase):
|
|
|
190
233
|
self.assertFalse(threading.excepthook.called)
|
|
191
234
|
|
|
192
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)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# coding: utf-8
|
|
2
2
|
from __future__ import print_function, unicode_literals
|
|
3
3
|
import mock
|
|
4
|
+
import os
|
|
4
5
|
import time
|
|
5
6
|
import threading
|
|
6
7
|
import unittest
|
|
@@ -26,11 +27,49 @@ class TestLogtailHandler(unittest.TestCase):
|
|
|
26
27
|
# Test default timeout
|
|
27
28
|
handler = LogtailHandler(source_token=self.source_token, host=self.host)
|
|
28
29
|
self.assertEqual(handler.uploader.timeout, 30)
|
|
29
|
-
|
|
30
|
+
|
|
30
31
|
# Test custom timeout
|
|
31
32
|
handler = LogtailHandler(source_token=self.source_token, host=self.host, timeout=10)
|
|
32
33
|
self.assertEqual(handler.uploader.timeout, 10)
|
|
33
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
|
+
|
|
34
73
|
@patch('logtail.handler.FlushWorker')
|
|
35
74
|
def test_handler_creates_pipe_from_args(self, MockWorker):
|
|
36
75
|
buffer_capacity = 9
|
|
@@ -277,6 +316,111 @@ class TestLogtailHandler(unittest.TestCase):
|
|
|
277
316
|
self.assertEqual(log_entry['context']['data']['egg']['chicken']['egg'], "<omitted circular reference>")
|
|
278
317
|
self.assertTrue(handler.pipe.empty())
|
|
279
318
|
|
|
319
|
+
@patch('logtail.handler.FlushWorker')
|
|
320
|
+
def test_forked_child_starts_with_a_fresh_queue_thread_and_session(self, MockWorker):
|
|
321
|
+
handler = LogtailHandler(source_token=self.source_token)
|
|
322
|
+
logger = logging.getLogger(__name__)
|
|
323
|
+
logger.handlers = []
|
|
324
|
+
logger.addHandler(handler)
|
|
325
|
+
logger.critical('queued in the parent')
|
|
326
|
+
uploader = handler.uploader
|
|
327
|
+
parent_session = uploader.session
|
|
328
|
+
|
|
329
|
+
pid = os.fork()
|
|
330
|
+
if pid == 0:
|
|
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)
|
|
335
|
+
os._exit(0 if fresh else 1)
|
|
336
|
+
_, status = os.waitpid(pid, 0)
|
|
337
|
+
|
|
338
|
+
self.assertEqual(os.waitstatus_to_exitcode(status), 0)
|
|
339
|
+
self.assertEqual(handler.pipe.get(block=False)['message'], 'queued in the parent')
|
|
340
|
+
|
|
341
|
+
def test_flush_returns_when_the_upload_itself_logs_to_the_handler(self):
|
|
342
|
+
logger = logging.getLogger(__name__)
|
|
343
|
+
logger.handlers = []
|
|
344
|
+
logger.setLevel(logging.DEBUG)
|
|
345
|
+
handler = LogtailHandler(source_token=self.source_token, flush_interval=0.01, check_interval=0.01)
|
|
346
|
+
uploads = []
|
|
347
|
+
|
|
348
|
+
def upload(frame):
|
|
349
|
+
uploads.append(frame)
|
|
350
|
+
# urllib3 logs every request it makes at DEBUG, and a handler on the
|
|
351
|
+
# root logger receives that record.
|
|
352
|
+
logger.debug('http://in.logs.betterstack.com:443 "POST / HTTP/1.1" 202 0')
|
|
353
|
+
return mock.MagicMock(status_code=202)
|
|
354
|
+
|
|
355
|
+
handler.uploader = upload
|
|
356
|
+
logger.addHandler(handler)
|
|
357
|
+
logger.info('hello')
|
|
358
|
+
self.addCleanup(self._stop_flush_worker, handler)
|
|
359
|
+
|
|
360
|
+
flushed = threading.Event()
|
|
361
|
+
threading.Thread(target=lambda: (handler.flush(), flushed.set()), daemon=True).start()
|
|
362
|
+
|
|
363
|
+
self.assertTrue(flushed.wait(2), 'flush() did not return')
|
|
364
|
+
self.assertEqual([f['message'] for frame in uploads for f in frame], ['hello'])
|
|
365
|
+
|
|
366
|
+
def test_records_logged_by_the_upload_ride_along_with_the_next_batch(self):
|
|
367
|
+
logger = logging.getLogger(__name__)
|
|
368
|
+
logger.handlers = []
|
|
369
|
+
logger.setLevel(logging.DEBUG)
|
|
370
|
+
# Only a full buffer sends a batch within the test.
|
|
371
|
+
handler = LogtailHandler(source_token=self.source_token, buffer_capacity=2, flush_interval=1000, check_interval=0.01)
|
|
372
|
+
uploads = []
|
|
373
|
+
|
|
374
|
+
def upload(frame):
|
|
375
|
+
logger.debug('POST / 202') # what urllib3 does, still inside the request
|
|
376
|
+
uploads.append([f['message'] for f in frame])
|
|
377
|
+
return mock.MagicMock(status_code=202)
|
|
378
|
+
|
|
379
|
+
handler.uploader = upload
|
|
380
|
+
logger.addHandler(handler)
|
|
381
|
+
self.addCleanup(self._stop_flush_worker, handler)
|
|
382
|
+
|
|
383
|
+
logger.info('hello')
|
|
384
|
+
logger.info('hello again')
|
|
385
|
+
self._wait_until(lambda: len(uploads) == 1)
|
|
386
|
+
logger.info('world')
|
|
387
|
+
self._wait_until(lambda: len(uploads) == 2)
|
|
388
|
+
|
|
389
|
+
self.assertEqual(uploads, [['hello', 'hello again'], ['POST / 202', 'world']])
|
|
390
|
+
|
|
391
|
+
def test_a_batch_of_only_the_uploads_own_records_is_dropped_not_sent(self):
|
|
392
|
+
logger = logging.getLogger(__name__)
|
|
393
|
+
logger.handlers = []
|
|
394
|
+
logger.setLevel(logging.DEBUG)
|
|
395
|
+
handler = LogtailHandler(source_token=self.source_token, flush_interval=0.01, check_interval=0.01)
|
|
396
|
+
uploads = []
|
|
397
|
+
|
|
398
|
+
def upload(frame):
|
|
399
|
+
logger.debug('POST / 202')
|
|
400
|
+
uploads.append([f['message'] for f in frame])
|
|
401
|
+
return mock.MagicMock(status_code=202)
|
|
402
|
+
|
|
403
|
+
handler.uploader = upload
|
|
404
|
+
logger.addHandler(handler)
|
|
405
|
+
self.addCleanup(self._stop_flush_worker, handler)
|
|
406
|
+
|
|
407
|
+
logger.info('hello')
|
|
408
|
+
self._wait_until(lambda: len(uploads) == 1)
|
|
409
|
+
time.sleep(0.1) # several flush intervals, enough for the loop to show if it existed
|
|
410
|
+
|
|
411
|
+
self.assertEqual(uploads, [['hello']])
|
|
412
|
+
self.assertTrue(handler.pipe.empty())
|
|
413
|
+
|
|
414
|
+
def _wait_until(self, condition):
|
|
415
|
+
deadline = time.time() + 2
|
|
416
|
+
while not condition() and time.time() < deadline:
|
|
417
|
+
time.sleep(0.01)
|
|
418
|
+
self.assertTrue(condition())
|
|
419
|
+
|
|
420
|
+
def _stop_flush_worker(self, handler):
|
|
421
|
+
handler.flush_thread.should_run = False
|
|
422
|
+
handler.flush_thread.join(1)
|
|
423
|
+
|
|
280
424
|
|
|
281
425
|
class UnserializableObject(object):
|
|
282
426
|
""" Because this is a custom class, it cannot be serialized into JSON. """
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# coding: utf-8
|
|
2
|
+
from __future__ import print_function, unicode_literals
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import mock
|
|
6
|
+
import os
|
|
7
|
+
import unittest
|
|
8
|
+
|
|
9
|
+
from unittest.mock import patch
|
|
10
|
+
|
|
11
|
+
import rq
|
|
12
|
+
|
|
13
|
+
from logtail import LogtailHandler
|
|
14
|
+
from logtail.rq import Worker
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TestWorker(unittest.TestCase):
|
|
18
|
+
source_token = 'dummy_source_token'
|
|
19
|
+
|
|
20
|
+
def setUp(self):
|
|
21
|
+
self.logger = logging.getLogger(__name__)
|
|
22
|
+
self.logger.handlers = []
|
|
23
|
+
self.logger.setLevel(logging.DEBUG)
|
|
24
|
+
self.uploads = []
|
|
25
|
+
# Only an explicit flush can send anything within the test.
|
|
26
|
+
self.handler = LogtailHandler(source_token=self.source_token, flush_interval=1000, check_interval=0.01)
|
|
27
|
+
self.handler.uploader = self._upload
|
|
28
|
+
self.logger.addHandler(self.handler)
|
|
29
|
+
connection = mock.MagicMock()
|
|
30
|
+
connection.connection_pool.connection_kwargs = {}
|
|
31
|
+
self.worker = Worker(['default'], connection=connection, prepare_for_work=False)
|
|
32
|
+
|
|
33
|
+
def tearDown(self):
|
|
34
|
+
if self.handler.flush_thread:
|
|
35
|
+
self.handler.flush_thread.should_run = False
|
|
36
|
+
self.handler.flush_thread.join(1)
|
|
37
|
+
|
|
38
|
+
def _upload(self, frame):
|
|
39
|
+
self.uploads.append([f['message'] for f in frame])
|
|
40
|
+
return mock.MagicMock(status_code=202)
|
|
41
|
+
|
|
42
|
+
def test_is_an_rq_worker(self):
|
|
43
|
+
self.assertIsInstance(self.worker, rq.Worker)
|
|
44
|
+
|
|
45
|
+
@patch.object(rq.Worker, 'perform_job')
|
|
46
|
+
def test_flushes_logtail_handlers_after_the_job(self, perform_job):
|
|
47
|
+
perform_job.side_effect = lambda job, queue: self.logger.info('inside the job') or True
|
|
48
|
+
|
|
49
|
+
self.assertTrue(self.worker.perform_job(mock.sentinel.job, mock.sentinel.queue))
|
|
50
|
+
|
|
51
|
+
perform_job.assert_called_once_with(mock.sentinel.job, mock.sentinel.queue)
|
|
52
|
+
self.assertEqual(self.uploads, [['inside the job']])
|
|
53
|
+
|
|
54
|
+
@patch.object(rq.Worker, 'perform_job')
|
|
55
|
+
def test_flushes_logtail_handlers_when_the_job_raises(self, perform_job):
|
|
56
|
+
def fail(job, queue):
|
|
57
|
+
self.logger.error('the job blew up')
|
|
58
|
+
raise ValueError('boom')
|
|
59
|
+
perform_job.side_effect = fail
|
|
60
|
+
|
|
61
|
+
with self.assertRaises(ValueError):
|
|
62
|
+
self.worker.perform_job(mock.sentinel.job, mock.sentinel.queue)
|
|
63
|
+
|
|
64
|
+
self.assertEqual(self.uploads, [['the job blew up']])
|
|
65
|
+
|
|
66
|
+
@patch.object(rq.Worker, 'perform_job')
|
|
67
|
+
def test_records_logged_in_the_work_horse_are_sent_before_it_exits(self, perform_job):
|
|
68
|
+
# RQ forks a work horse per job and ends it with os._exit(), which skips
|
|
69
|
+
# the interpreter shutdown that would otherwise flush the handler.
|
|
70
|
+
perform_job.side_effect = lambda job, queue: self.logger.info('inside the work horse')
|
|
71
|
+
read_end, write_end = os.pipe()
|
|
72
|
+
self.handler.uploader = lambda frame: os.write(write_end, json.dumps([f['message'] for f in frame]).encode()) and mock.MagicMock(status_code=202)
|
|
73
|
+
|
|
74
|
+
pid = os.fork()
|
|
75
|
+
if pid == 0:
|
|
76
|
+
self.worker.main_work_horse(mock.MagicMock(), mock.MagicMock())
|
|
77
|
+
os.waitpid(pid, 0)
|
|
78
|
+
os.close(write_end)
|
|
79
|
+
os.set_blocking(read_end, False)
|
|
80
|
+
|
|
81
|
+
self.assertEqual(json.loads(os.read(read_end, 4096)), ['inside the work horse'])
|
|
@@ -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,42 +0,0 @@
|
|
|
1
|
-
LICENSE.md
|
|
2
|
-
MANIFEST.in
|
|
3
|
-
README.md
|
|
4
|
-
requirements.txt
|
|
5
|
-
setup.cfg
|
|
6
|
-
setup.py
|
|
7
|
-
test-requirements.txt
|
|
8
|
-
logtail/__init__.py
|
|
9
|
-
logtail/compat.py
|
|
10
|
-
logtail/flusher.py
|
|
11
|
-
logtail/formatter.py
|
|
12
|
-
logtail/frame.py
|
|
13
|
-
logtail/handler.py
|
|
14
|
-
logtail/helpers.py
|
|
15
|
-
logtail/uploader.py
|
|
16
|
-
logtail_python.egg-info/PKG-INFO
|
|
17
|
-
logtail_python.egg-info/SOURCES.txt
|
|
18
|
-
logtail_python.egg-info/dependency_links.txt
|
|
19
|
-
logtail_python.egg-info/requires.txt
|
|
20
|
-
logtail_python.egg-info/top_level.txt
|
|
21
|
-
tests/__init__.py
|
|
22
|
-
tests/test_flusher.py
|
|
23
|
-
tests/test_formatter.py
|
|
24
|
-
tests/test_frame.py
|
|
25
|
-
tests/test_handler.py
|
|
26
|
-
tests/test_helpers.py
|
|
27
|
-
tests/test_uploader.py
|
|
28
|
-
tests/__pycache__/__init__.cpython-311.pyc
|
|
29
|
-
tests/__pycache__/__init__.cpython-314.pyc
|
|
30
|
-
tests/__pycache__/test_flusher.cpython-311.pyc
|
|
31
|
-
tests/__pycache__/test_flusher.cpython-314-pytest-9.1.1.pyc
|
|
32
|
-
tests/__pycache__/test_flusher.cpython-314.pyc
|
|
33
|
-
tests/__pycache__/test_formatter.cpython-311.pyc
|
|
34
|
-
tests/__pycache__/test_formatter.cpython-314-pytest-9.1.1.pyc
|
|
35
|
-
tests/__pycache__/test_frame.cpython-311.pyc
|
|
36
|
-
tests/__pycache__/test_frame.cpython-314-pytest-9.1.1.pyc
|
|
37
|
-
tests/__pycache__/test_handler.cpython-311.pyc
|
|
38
|
-
tests/__pycache__/test_handler.cpython-314-pytest-9.1.1.pyc
|
|
39
|
-
tests/__pycache__/test_helpers.cpython-311.pyc
|
|
40
|
-
tests/__pycache__/test_helpers.cpython-314-pytest-9.1.1.pyc
|
|
41
|
-
tests/__pycache__/test_uploader.cpython-311.pyc
|
|
42
|
-
tests/__pycache__/test_uploader.cpython-314-pytest-9.1.1.pyc
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|