Skip to content

Commit 8ae453b

Browse files
authored
Merge pull request #12 from homiedopie/feature/INV-1719-circular-issue
INV-1719 - Initial fix for the circular issue for log messages
2 parents c2625f5 + d9141ff commit 8ae453b

File tree

7 files changed

+176
-7
lines changed

7 files changed

+176
-7
lines changed

docker/stackify-python-api-test

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ ARG version
66

77
RUN \
88
apt-get update && \
9-
apt-get install -y flake8 && \
109
pip install --upgrade pip && \
1110
python --version
1211

stackify/handler.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,10 @@ def __init__(self, queue_, max_batch=MAX_BATCH, config=None, **kwargs):
7676
self._started = False
7777

7878
def handle(self, record):
79-
self.messages.append(self.transport.create_message(record))
79+
try:
80+
self.messages.append(self.transport.create_message(record))
81+
except Exception:
82+
internal_logger.exception('Could not handle log message: {}'.format(hasattr(record, 'getMessage') and record.getMessage() or str(record)))
8083

8184
if len(self.messages) >= self.max_batch:
8285
self.send_group()

stackify/transport/agent/message.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import json
21
import sys
32
import traceback
43

54
from stackify.constants import RECORD_VARS
65
from stackify.protos import stackify_agent_pb2
6+
from stackify.utils import data_to_json
77

88

99
class BaseMessage(object):
@@ -103,7 +103,7 @@ def __init__(self, record, api_config, env_details):
103103
if k not in RECORD_VARS}
104104

105105
if data:
106-
log.data = json.dumps(data, default=lambda x: hasattr(x, '__dict__') and x.__dict__ or x.__str__())
106+
log.data = data_to_json(data)
107107

108108
if record.exc_info:
109109
log.error.MergeFrom(Error(record, api_config, env_details).get_object())

stackify/transport/default/log.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import json
2-
31
from stackify.constants import RECORD_VARS
42
from stackify.transport.default.formats import JSONObject
53
from stackify.transport.default.error import StackifyError
4+
from stackify.utils import data_to_json
65

76

87
class LogMsg(JSONObject):
@@ -33,7 +32,7 @@ def from_record(self, record):
3332
if k not in RECORD_VARS}
3433

3534
if data:
36-
self.data = json.dumps(data, default=lambda x: hasattr(x, '__dict__') and x.__dict__ or x.__str__())
35+
self.data = data_to_json(data)
3736

3837
if record.exc_info:
3938
self.Ex = StackifyError()

stackify/utils.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import os
2+
import json
3+
import logging
4+
5+
internal_logger = logging.getLogger(__name__)
26

37

48
def arg_or_env(name, args, default=None, env_key=None):
@@ -13,3 +17,22 @@ def arg_or_env(name, args, default=None, env_key=None):
1317
return default
1418
else:
1519
raise NameError('You must specify the keyword argument {0} or environment variable {1}'.format(name, env_name))
20+
21+
22+
def data_to_json(data):
23+
try:
24+
if object_is_iterable(data) and 'request' in data and hasattr(data['request'], '_messages'):
25+
data['request'] = get_default_object(data['request'])
26+
27+
return json.dumps(data, default=lambda x: get_default_object(x))
28+
except ValueError as e:
29+
internal_logger.exception('Failed to serialize object to json: {} - Exception: {}'.format(data.__str__(), str(e)))
30+
return json.dumps(data.__str__()) # String representation of the object
31+
32+
33+
def get_default_object(obj):
34+
return hasattr(obj, '__dict__') and obj.__dict__ or obj.__str__()
35+
36+
37+
def object_is_iterable(obj):
38+
return hasattr(obj, '__iter__') or isinstance(obj, str)

tests/test_handler.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,25 @@ def test_send_group_crash(self, send_log_group, logmsggroup, logmsg):
110110
self.assertEqual(len(listener.messages), 1)
111111
self.assertEqual(send_log_group.call_count, 1)
112112

113+
@patch('stackify.transport.default.DefaultTransport.create_message')
114+
@patch('stackify.transport.default.DefaultTransport.create_group_message')
115+
@patch('stackify.transport.default.http.HTTPClient.send_log_group')
116+
def test_create_message_crash(self, send_log_group, logmsggroup, logmsg):
117+
'''The listener drops messages after retrying'''
118+
listener = StackifyListener(queue_=Mock(), max_batch=3, config=self.config)
119+
listener.transport._transport.identified = True
120+
121+
logmsg.side_effect = Exception
122+
123+
listener.handle(1)
124+
listener.handle(2)
125+
listener.handle(3)
126+
self.assertEqual(len(listener.messages), 0)
127+
listener.handle(4)
128+
self.assertEqual(len(listener.messages), 0) # messages not created
129+
self.assertEqual(logmsg.call_count, 4) # we called the function 4 times
130+
self.assertEqual(send_log_group.call_count, 0) # since we have exceptions
131+
113132

114133
if __name__ == '__main__':
115134
unittest.main()

tests/test_utils.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
"""
2+
Test the stackify.__init__ setup functions
3+
"""
4+
5+
import unittest
6+
from mock import patch
7+
from .bases import ClearEnvTest
8+
9+
import stackify
10+
import logging
11+
12+
13+
class TestInit(ClearEnvTest):
14+
'''
15+
Test the logger init functionality
16+
'''
17+
18+
def setUp(self):
19+
super(TestInit, self).setUp()
20+
self.config = stackify.transport.application.ApiConfiguration(
21+
application='test_appname',
22+
environment='test_environment',
23+
api_key='test_apikey',
24+
api_url='test_apiurl')
25+
self.loggers = []
26+
27+
def tearDown(self):
28+
super(TestInit, self).tearDown()
29+
global_loggers = logging.Logger.manager.loggerDict
30+
for logger in self.loggers:
31+
del global_loggers[logger.name]
32+
33+
def test_utils_data_to_json_unserializable(self):
34+
dummy = Dummy()
35+
result = stackify.utils.data_to_json(dummy)
36+
substring = '<tests.test_utils.Dummy object at'
37+
self.assertTrue(substring in result)
38+
39+
def test_utils_data_to_json_dict(self):
40+
dummy = {
41+
'test': 'test'
42+
}
43+
result = stackify.utils.data_to_json(dummy)
44+
expected = '{"test": "test"}'
45+
self.assertEqual(expected, result)
46+
47+
def test_utils_data_to_json_list(self):
48+
dummy = [1, 2, 3]
49+
result = stackify.utils.data_to_json(dummy)
50+
expected = '[1, 2, 3]'
51+
self.assertEqual(expected, result)
52+
53+
def test_utils_data_to_json_tuple(self):
54+
dummy = (1, 2)
55+
result = stackify.utils.data_to_json(dummy)
56+
expected = '[1, 2]'
57+
self.assertEqual(expected, result)
58+
59+
def test_utils_data_to_json_dummy_iterable(self):
60+
dummy = DummyInterable()
61+
result = stackify.utils.data_to_json(dummy)
62+
expected = '{"a": 21}'
63+
self.assertEqual(expected, result)
64+
65+
def test_utils_data_to_json_dummy_object_with_property(self):
66+
dummy = DummyProperty()
67+
result = stackify.utils.data_to_json(dummy)
68+
expected = '{"x": 5}'
69+
self.assertEqual(expected, result)
70+
71+
@patch('logging.Logger.exception')
72+
def test_utils_data_to_json_dummy_object_circular(self, func):
73+
dummy = DummyProperty()
74+
data = {
75+
'test': '1',
76+
'dummy': dummy,
77+
'payload': {}
78+
}
79+
data['payload']['dummy'] = data
80+
81+
result = stackify.utils.data_to_json(data)
82+
func.assert_called()
83+
84+
substring = "'dummy': <tests.test_utils.DummyProperty object at"
85+
self.assertTrue(substring in result)
86+
87+
def test_utils_data_to_json_dummy_request(self):
88+
dummy = DummyRequest()
89+
result = stackify.utils.data_to_json(dummy)
90+
substring = '{"_messages": "<tests.test_utils.Dummy object at'
91+
self.assertTrue(substring in result)
92+
93+
94+
class Dummy(object):
95+
pass
96+
97+
98+
class DummyInterable:
99+
def __iter__(self):
100+
self.a = 1
101+
return self
102+
103+
def __next__(self):
104+
if self.a <= 20:
105+
x = self.a
106+
self.a += 1
107+
return x
108+
else:
109+
raise StopIteration
110+
111+
def next(self):
112+
return self.__next__()
113+
114+
115+
class DummyProperty(object):
116+
def __init__(self):
117+
self.x = 5
118+
119+
120+
class DummyRequest(object):
121+
def __init__(self):
122+
self._messages = Dummy()
123+
124+
125+
if __name__ == '__main__':
126+
unittest.main()

0 commit comments

Comments
 (0)