Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions example-project/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ pip install logtail-python
To run the example application, simply run the following command:

```bash
python example-project.py <source-token>
python example-project.py <source_token> <ingesting_host>
```

Don't forget to replace `<source-token>` with your actual source toke which you can find in the source settings.
_Don't forget to replace `<source_token>` and `<ingesting_host>` with your actual source token and ingesting host which you can find by going to **[Sources](https://telemetry.betterstack.com/team/0/sources) -> Configure** in Better Stack._

If you have trouble running the command above, check your Python installation and try running it with the `python3` command instead.

Expand Down Expand Up @@ -51,7 +51,7 @@ Then we need to create a `handler`, which will be responsible for handling our l

```python
# Create handler
handler = LogtailHandler(source_token=sys.argv[1])
handler = LogtailHandler(source_token=sys.argv[1], host=sys.argv[2])

# Create logger
logger = logging.getLogger(__name__)
Expand Down
6 changes: 3 additions & 3 deletions example-project/example-project.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
import sys

# Check for program arguments
if len(sys.argv) != 2:
print("Program requires source token as an argument, run the program as followed\npython example-project.py <source-token>");
if len(sys.argv) != 3:
print("Program requires source token and ingesting host as an argument, run the program as followed\npython example-project.py <source_token> <ingesting_host>");
sys.exit();

# Create handler
handler = LogtailHandler(source_token=sys.argv[1])
handler = LogtailHandler(source_token=sys.argv[1], host=sys.argv[2])

# Create logger
logger = logging.getLogger(__name__)
Expand Down
2 changes: 1 addition & 1 deletion logtail/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@
from .helpers import LogtailContext, DEFAULT_CONTEXT
from .formatter import LogtailFormatter

__version__ = '0.3.0'
__version__ = '0.3.4'

context = DEFAULT_CONTEXT
21 changes: 16 additions & 5 deletions logtail/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,28 @@ def _parse_custom_events(record, include_extra_attributes):
def _remove_circular_dependencies(obj, memo=None):
if memo is None:
memo = set()
if id(obj) in memo:

# Skip immutable types, which can't contain circular dependencies
if isinstance(obj, (str, int, float, bool, type(None))):
return obj

# For mutable objects, check for circular references
obj_id = id(obj)
if obj_id in memo:
return "<omitted circular reference>"
memo.add(id(obj))
memo.add(obj_id)

if isinstance(obj, dict):
new_dict = {}
for key, value in obj.items():
new_dict[key] = _remove_circular_dependencies(value, memo)
new_dict[key] = _remove_circular_dependencies(value, memo.copy())
return new_dict
elif isinstance(obj, list):
new_list = [_remove_circular_dependencies(item, memo) for item in obj]
return new_list
return [_remove_circular_dependencies(item, memo.copy()) for item in obj]
elif isinstance(obj, tuple):
return tuple(_remove_circular_dependencies(item, memo.copy()) for item in obj)
elif isinstance(obj, set):
return {_remove_circular_dependencies(item, memo.copy()) for item in obj}
else:
return obj

Expand Down
11 changes: 8 additions & 3 deletions logtail/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
from .uploader import Uploader
from .frame import create_frame

DEFAULT_HOST = 'https://in.logs.betterstack.com'
DEFAULT_HOST = 'in.logs.betterstack.com'
DEFAULT_BUFFER_CAPACITY = 1000
DEFAULT_FLUSH_INTERVAL = 1
DEFAULT_CHECK_INTERVAL = 0.1
DEFAULT_RAISE_EXCEPTIONS = False
DEFAULT_DROP_EXTRA_EVENTS = True
DEFAULT_INCLUDE_EXTRA_ATTRIBUTES = True
DEFAULT_TIMEOUT = 30


class LogtailHandler(logging.Handler):
Expand All @@ -29,13 +30,17 @@ def __init__(self,
drop_extra_events=DEFAULT_DROP_EXTRA_EVENTS,
include_extra_attributes=DEFAULT_INCLUDE_EXTRA_ATTRIBUTES,
context=DEFAULT_CONTEXT,
timeout=DEFAULT_TIMEOUT,
level=logging.NOTSET):
super(LogtailHandler, self).__init__(level=level)
self.source_token = source_token
self.host = host
if host.startswith('https://') or host.startswith('http://'):
self.host = host
else:
self.host = "https://" + host
self.context = context
self.pipe = queue.Queue(maxsize=buffer_capacity)
self.uploader = Uploader(self.source_token, self.host)
self.uploader = Uploader(self.source_token, self.host, timeout)
self.drop_extra_events = drop_extra_events
self.include_extra_attributes = include_extra_attributes
self.buffer_capacity = buffer_capacity
Expand Down
7 changes: 4 additions & 3 deletions logtail/uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ def __init__(self, exception):
self.exception = exception

class Uploader(object):
def __init__(self, source_token, host):
def __init__(self, source_token, host, timeout):
self.source_token = source_token
self.host = host
self.timeout = timeout
self.session = requests.Session()
self.headers = {
'Authorization': 'Bearer %s' % source_token,
Expand All @@ -21,6 +22,6 @@ def __init__(self, source_token, host):
def __call__(self, frame):
data = msgpack.packb(frame, use_bin_type=True)
try:
return self.session.post(self.host, data=data, headers=self.headers)
return self.session.post(self.host, data=data, headers=self.headers, timeout=self.timeout)
except requests.RequestException as e:
return Fake500(e)
return Fake500(e)
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from setuptools import setup


VERSION = '0.3.0'
VERSION = '0.3.4'
ROOT_DIR = os.path.dirname(__file__)

REQUIREMENTS = [
Expand Down
3 changes: 2 additions & 1 deletion tests/test_flusher.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ class TestFlushWorker(unittest.TestCase):
buffer_capacity = 5
flush_interval = 2
check_interval = 0.01
timeout = 0.1

def _setup_worker(self, uploader=None):
pipe = queue.Queue(maxsize=self.buffer_capacity)
uploader = uploader or Uploader(self.source_token, self.host)
uploader = uploader or Uploader(self.source_token, self.host, self.timeout)
fw = FlushWorker(uploader, pipe, self.buffer_capacity, self.flush_interval, self.check_interval)
return pipe, uploader, fw

Expand Down
53 changes: 52 additions & 1 deletion tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,17 @@ class TestLogtailHandler(unittest.TestCase):
def test_handler_creates_uploader_from_args(self, MockWorker):
handler = LogtailHandler(source_token=self.source_token, host=self.host)
self.assertEqual(handler.uploader.source_token, self.source_token)
self.assertEqual(handler.uploader.host, self.host)
self.assertEqual(handler.uploader.host, "https://" + self.host)

@patch('logtail.handler.FlushWorker')
def test_handler_passes_timeout_to_uploader(self, MockWorker):
# Test default timeout
handler = LogtailHandler(source_token=self.source_token, host=self.host)
self.assertEqual(handler.uploader.timeout, 30)

# Test custom timeout
handler = LogtailHandler(source_token=self.source_token, host=self.host, timeout=10)
self.assertEqual(handler.uploader.timeout, 10)

@patch('logtail.handler.FlushWorker')
def test_handler_creates_pipe_from_args(self, MockWorker):
Expand Down Expand Up @@ -203,6 +213,47 @@ def test_can_send_circular_dependency_in_extra_data(self, MockWorker):
self.assertEqual(log_entry['data']['egg']['chicken'], "<omitted circular reference>")
self.assertTrue(handler.pipe.empty())

@patch('logtail.handler.FlushWorker')
def test_can_have_multiple_instance_of_same_string_in_extra_data(self, MockWorker):
buffer_capacity = 1
handler = LogtailHandler(
source_token=self.source_token,
buffer_capacity=buffer_capacity
)

logger = logging.getLogger(__name__)
logger.handlers = []
logger.addHandler(handler)
test_string = 'this is a test string'
logger.info('hello', extra={'test1': test_string, 'test2': test_string})

log_entry = handler.pipe.get()

self.assertEqual(log_entry['message'], 'hello')
self.assertEqual(log_entry['test1'], 'this is a test string')
self.assertEqual(log_entry['test2'], 'this is a test string')
self.assertTrue(handler.pipe.empty())

@patch('logtail.handler.FlushWorker')
def test_can_have_multiple_instance_of_same_array_in_extra_data(self, MockWorker):
buffer_capacity = 1
handler = LogtailHandler(
source_token=self.source_token,
buffer_capacity=buffer_capacity
)

logger = logging.getLogger(__name__)
logger.handlers = []
logger.addHandler(handler)
test_array = ['this is a test string']
logger.info('hello', extra={'test1': test_array, 'test2': test_array})

log_entry = handler.pipe.get()

self.assertEqual(log_entry['message'], 'hello')
self.assertEqual(log_entry['test1'], ['this is a test string'])
self.assertEqual(log_entry['test2'], ['this is a test string'])
self.assertTrue(handler.pipe.empty())

@patch('logtail.handler.FlushWorker')
def test_can_send_circular_dependency_in_context(self, MockWorker):
Expand Down
7 changes: 5 additions & 2 deletions tests/test_uploader.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ class TestUploader(unittest.TestCase):
host = 'https://in.logtail.com'
source_token = 'dummy_source_token'
frame = [1, 2, 3]
timeout = 30

@patch('logtail.uploader.requests.Session.post')
def test_call(self, post):
def mock_post(endpoint, data=None, headers=None):
def mock_post(endpoint, data=None, headers=None, timeout=None):
# Check that the data is sent to ther correct endpoint
self.assertEqual(endpoint, self.host)
# Check the content-type
Expand All @@ -25,9 +26,11 @@ def mock_post(endpoint, data=None, headers=None):
self.assertEqual('application/msgpack', headers.get('Content-Type'))
# Check the content was msgpacked correctly
self.assertEqual(msgpack.unpackb(data, raw=False), self.frame)
# Check that timeout is passed to the request
self.assertEqual(timeout, 30)

post.side_effect = mock_post
u = Uploader(self.source_token, self.host)
u = Uploader(self.source_token, self.host, self.timeout)
u(self.frame)

self.assertTrue(post.called)