Skip to content

Commit 00cd4d5

Browse files
authored
Merge pull request #449 from rollbar/fixed/shortener-multi-level-shortening
Fixed shortener multi level shortening
2 parents ad596ac + 7ece3cb commit 00cd4d5

File tree

5 files changed

+192
-79
lines changed

5 files changed

+192
-79
lines changed

rollbar/__init__.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -402,12 +402,6 @@ def init(access_token, environment='production', scrub_fields=None, url_fields=N
402402
# trace frame values using the ShortReprTransform.
403403
_serialize_transform = SerializableTransform(safe_repr=SETTINGS['locals']['safe_repr'],
404404
safelist_types=SETTINGS['locals']['safelisted_types'])
405-
_transforms = [
406-
ScrubRedactTransform(),
407-
_serialize_transform,
408-
ScrubTransform(suffixes=[(field,) for field in SETTINGS['scrub_fields']], redact_char='*'),
409-
ScrubUrlTransform(suffixes=[(field,) for field in SETTINGS['url_fields']], params_to_scrub=SETTINGS['scrub_fields'])
410-
]
411405

412406
# A list of key prefixes to apply our shortener transform to. The request
413407
# being included in the body key is old behavior and is being retained for
@@ -431,7 +425,13 @@ def init(access_token, environment='production', scrub_fields=None, url_fields=N
431425
shortener = ShortenerTransform(safe_repr=SETTINGS['locals']['safe_repr'],
432426
keys=shortener_keys,
433427
**SETTINGS['locals']['sizes'])
434-
_transforms.append(shortener)
428+
_transforms = [
429+
shortener,
430+
ScrubRedactTransform(),
431+
_serialize_transform,
432+
ScrubTransform(suffixes=[(field,) for field in SETTINGS['scrub_fields']], redact_char='*'),
433+
ScrubUrlTransform(suffixes=[(field,) for field in SETTINGS['url_fields']], params_to_scrub=SETTINGS['scrub_fields'])
434+
]
435435
_threads = queue.Queue()
436436
events.reset()
437437
filters.add_builtin_filters(SETTINGS)

rollbar/lib/__init__.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,25 +33,36 @@ def prefix_match(key, prefixes):
3333
return False
3434

3535

36-
def key_in(key, keys):
36+
def key_in(key, canonicals):
3737
if not key:
3838
return False
3939

40-
for k in keys:
41-
if key_match(k, key):
40+
for c in canonicals:
41+
if key_match(key, c):
4242
return True
4343

4444
return False
4545

4646

47-
def key_match(key1, key2):
48-
if len(key1) != len(key2):
47+
def key_depth(key, canonicals) -> int:
48+
if not key:
49+
return 0
50+
51+
for c in canonicals:
52+
if key_match(key, c):
53+
return len(c)
54+
55+
return 0
56+
57+
58+
def key_match(key, canonical):
59+
if len(key) < len(canonical):
4960
return False
5061

51-
for p1, p2 in zip(key1, key2):
52-
if '*' == p1 or '*' == p2:
62+
for k, c in zip(key, canonical):
63+
if '*' == c:
5364
continue
54-
if p1 == p2:
65+
if c == k:
5566
continue
5667
return False
5768

rollbar/lib/transforms/shortener.py

Lines changed: 19 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from collections.abc import Mapping
77

88
from rollbar.lib import (
9-
integer_types, key_in, number_types, sequence_types,
9+
integer_types, key_in, key_depth, number_types, sequence_types,
1010
string_types)
1111
from rollbar.lib.transform import Transform
1212

@@ -47,8 +47,6 @@ def _get_max_size(self, obj):
4747

4848
return self._repr.maxother
4949

50-
def _get_max_level(self):
51-
return getattr(self._repr, 'maxlevel')
5250
def _shorten_sequence(self, obj, max_keys):
5351
_len = len(obj)
5452
if _len <= max_keys:
@@ -79,44 +77,14 @@ def _shorten_other(self, obj):
7977

8078
return self._repr.repr(obj)
8179

82-
def traverse_obj(self, obj, level=1):
83-
def seq_iter(o):
84-
return o if isinstance(o, dict) else range(len(o))
85-
86-
for k in seq_iter(obj):
87-
max_size = self._get_max_size(obj[k])
88-
if isinstance(obj[k], dict):
89-
obj[k] = self._shorten_mapping(obj[k], max_size)
90-
if level == self._get_max_level():
91-
del obj[k]
92-
return
93-
self.traverse_obj(obj[k], level + 1)
94-
elif isinstance(obj[k], sequence_types):
95-
obj[k] = self._shorten_sequence(obj[k], max_size)
96-
if level == self._get_max_level():
97-
del obj[k]
98-
return
99-
self.traverse_obj(obj[k], level + 1)
100-
else:
101-
obj[k] = self._shorten(obj[k])
102-
return obj
103-
10480
def _shorten(self, val):
10581
max_size = self._get_max_size(val)
10682

10783
if isinstance(val, dict):
108-
val = self._shorten_mapping(val, max_size)
109-
return self.traverse_obj(val)
110-
111-
if isinstance(val, string_types):
84+
return self._shorten_mapping(val, max_size)
85+
if isinstance(val, (string_types, sequence_types)):
11286
return self._shorten_sequence(val, max_size)
11387

114-
if isinstance(val, sequence_types):
115-
val = self._shorten_sequence(val, max_size)
116-
if isinstance(val, string_types):
117-
return val
118-
return self.traverse_obj(val)
119-
12088
if isinstance(val, number_types):
12189
return self._shorten_basic(val, self._repr.maxlong)
12290

@@ -128,7 +96,23 @@ def _should_shorten(self, val, key):
12896

12997
return key_in(key, self.keys)
13098

99+
def _should_drop(self, val, key) -> bool:
100+
if not key:
101+
return False
102+
103+
maxdepth = key_depth(key, self.keys)
104+
if maxdepth == 0:
105+
return False
106+
107+
return (maxdepth + self._repr.maxlevel) <= len(key)
108+
131109
def default(self, o, key=None):
110+
if self._should_drop(o, key):
111+
if isinstance(o, dict):
112+
return '{...}'
113+
if isinstance(o, sequence_types):
114+
return '[...]'
115+
132116
if self._should_shorten(o, key):
133117
return self._shorten(o)
134118

rollbar/test/test_lib.py

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,66 @@
1-
from rollbar.lib import dict_merge, prefix_match
1+
from rollbar.lib import dict_merge, prefix_match, key_match, key_depth
22

33
from rollbar.test import BaseTest
44

55

66
class RollbarLibTest(BaseTest):
7-
def test_dict_merge_not_dict(self):
8-
a = {'a': {'b': 42}}
9-
b = 99
10-
result = dict_merge(a, b)
11-
12-
self.assertEqual(99, result)
13-
147
def test_prefix_match(self):
158
key = ['password', 'argspec', '0']
169
self.assertTrue(prefix_match(key, [['password']]))
1710

18-
def test_prefix_match(self):
11+
def test_prefix_match_dont_match(self):
1912
key = ['environ', 'argspec', '0']
2013
self.assertFalse(prefix_match(key, [['password']]))
2114

15+
def test_key_match(self):
16+
canonical = ['body', 'trace', 'frames', '*', 'locals', '*']
17+
key = ['body', 'trace', 'frames', 5, 'locals', 'foo']
18+
19+
self.assertTrue(key_match(key, canonical))
20+
21+
def test_key_match_dont_match(self):
22+
canonical = ['body', 'trace', 'frames', '*', 'locals', '*']
23+
key = ['body', 'trace', 'frames', 5, 'bar', 'foo']
24+
25+
self.assertFalse(key_match(key, canonical))
26+
27+
def test_key_match_wildcard_end(self):
28+
canonical = ['body', 'trace', 'frames', '*', 'locals', '*']
29+
key = ['body', 'trace', 'frames', 5, 'locals', 'foo', 'bar']
30+
31+
self.assertTrue(key_match(key, canonical))
32+
33+
def test_key_match_too_short(self):
34+
canonical = ['body', 'trace', 'frames', '*', 'locals', '*']
35+
key = ['body', 'trace', 'frames', 5, 'locals']
36+
37+
self.assertFalse(key_match(key, canonical))
38+
39+
def test_key_depth(self):
40+
canonicals = [['body', 'trace', 'frames', '*', 'locals', '*']]
41+
key = ['body', 'trace', 'frames', 5, 'locals', 'foo']
42+
43+
self.assertEqual(6, key_depth(key, canonicals))
44+
45+
def test_key_depth_dont_match(self):
46+
canonicals = [['body', 'trace', 'frames', '*', 'locals', '*']]
47+
key = ['body', 'trace', 'frames', 5, 'bar', 'foo']
48+
49+
self.assertEqual(0, key_depth(key, canonicals))
50+
51+
def test_key_depth_wildcard_end(self):
52+
canonicals = [['body', 'trace', 'frames', '*']]
53+
key = ['body', 'trace', 'frames', 5, 'locals', 'foo', 'bar']
54+
55+
self.assertEqual(4, key_depth(key, canonicals))
56+
57+
def test_dict_merge_not_dict(self):
58+
a = {'a': {'b': 42}}
59+
b = 99
60+
result = dict_merge(a, b)
61+
62+
self.assertEqual(99, result)
63+
2264
def test_dict_merge_dicts_independent(self):
2365
a = {'a': {'b': 42}}
2466
b = {'x': {'y': 99}}

rollbar/test/test_shortener_transform.py

Lines changed: 96 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,27 +37,22 @@ def setUp(self):
3737
'frozenset': frozenset([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
3838
'array': array('l', [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]),
3939
'deque': deque([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], 15),
40-
'other': TestClassWithAVeryVeryVeryVeryVeryVeryVeryLongName(),
41-
'list_max_level': [1, [2, [3, [4, ["good_5", ["bad_6", ["bad_7"]]]]]]],
42-
'dict_max_level': {1: 1, 2: {3: {4: {"level4": "good", "level5": {"toplevel": "ok", 6: {7: {}}}}}}},
43-
'list_multi_level': [1, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]]
40+
'other': TestClassWithAVeryVeryVeryVeryVeryVeryVeryLongName()
4441
}
4542

4643
def _assert_shortened(self, key, expected):
4744
shortener = ShortenerTransform(keys=[(key,)], **DEFAULT_LOCALS_SIZES)
4845
result = transforms.transform(self.data, shortener)
4946

5047
if key == 'dict':
51-
self.assertEqual(expected, len(result[key]))
52-
elif key in ('list_max_level', 'dict_max_level', 'list_multi_level'):
53-
self.assertEqual(expected, result[key])
48+
self.assertEqual(expected, len(result))
5449
else:
5550
# the repr output can vary between Python versions
5651
stripped_result_key = result[key].strip("'\"u")
5752

5853
if key == 'other':
5954
self.assertIn(expected, stripped_result_key)
60-
elif key not in ('dict', 'list_max_level', 'dict_max_level', 'list_multi_level'):
55+
elif key != 'dict':
6156
self.assertEqual(expected, stripped_result_key)
6257

6358
# make sure nothing else was shortened
@@ -87,18 +82,6 @@ def test_shorten_list(self):
8782
expected = '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]'
8883
self._assert_shortened('list', expected)
8984

90-
def test_shorten_list_max_level(self):
91-
expected = [1, [2, [3, [4, ['good_5']]]]]
92-
self._assert_shortened('list_max_level', expected)
93-
94-
def test_shorten_list_multi_level(self):
95-
expected = [1, '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]']
96-
self._assert_shortened('list_multi_level', expected)
97-
98-
def test_shorten_dict_max_level(self):
99-
expected = {1: 1, 2: {3: {4: {'level4': 'good', 'level5': {'toplevel': 'ok'}}}}}
100-
self._assert_shortened('dict_max_level', expected)
101-
10285
def test_shorten_tuple(self):
10386
expected = '(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...)'
10487
self._assert_shortened('tuple', expected)
@@ -145,3 +128,96 @@ def test_shorten_object(self):
145128
self.assertEqual(type(result), dict)
146129
self.assertEqual(len(result['request']['POST']), 10)
147130

131+
def test_shorten_frame(self):
132+
data = {
133+
'body': {
134+
'trace': {
135+
'frames': [
136+
{
137+
"filename": "/path/to/app.py",
138+
"lineno": 82,
139+
"method": "sub_func",
140+
"code": "extra(**kwargs)",
141+
"keywordspec": "kwargs",
142+
"locals": {
143+
"kwargs": {
144+
"app": ["foo", "bar", "baz", "qux", "quux", "corge", "grault", "garply", "waldo",
145+
"fred", "plugh", "xyzzy", "thud"],
146+
"extra": {
147+
"request": "<class 'some.package.MyClass'>"
148+
}
149+
},
150+
"one": {
151+
"two": {
152+
"three": {
153+
"four": {
154+
"five": {
155+
"six": {
156+
"seven": 8,
157+
"eight": "nine"
158+
},
159+
"ten": "Yep! this should still be here, but it is a little on the "
160+
"long side, so we might want to cut it down a bit."
161+
}
162+
}
163+
}
164+
},
165+
"a": ["foo", "bar", "baz", "qux", 5, 6, 7, 8, 9, 10, 11, 12],
166+
"b": 14071504106566481658450568387453168916351054663,
167+
"app_id": 140715046161904,
168+
"bar": "im a bar",
169+
}
170+
}
171+
}
172+
]
173+
}
174+
}
175+
}
176+
keys = [('body', 'trace', 'frames', '*', 'locals', '*')]
177+
shortener = ShortenerTransform(keys=keys, **DEFAULT_LOCALS_SIZES)
178+
result = transforms.transform(data, shortener)
179+
expected = {
180+
'body': {
181+
'trace': {
182+
'frames': [
183+
{
184+
"filename": "/path/to/app.py",
185+
"lineno": 82,
186+
"method": "sub_func",
187+
"code": "extra(**kwargs)",
188+
"keywordspec": "kwargs",
189+
"locals": {
190+
"kwargs": {
191+
# Shortened
192+
"app": "['foo', 'bar', 'baz', 'qux', 'quux', 'corge', 'grault', 'garply', 'waldo', "
193+
"'fred', ...]",
194+
"extra": {
195+
"request": "<class 'some.package.MyClass'>"
196+
}
197+
},
198+
"one": {
199+
"two": {
200+
"three": {
201+
"four": {
202+
"five": {
203+
"six": '{...}', # Dropped because it is past the maxlevel.
204+
# Shortened
205+
"ten": "'Yep! this should still be here, but it is a lit...ong "
206+
"side, so we might want to cut it down a bit.'"
207+
}
208+
}
209+
}
210+
},
211+
"a": "['foo', 'bar', 'baz', 'qux', 5, 6, 7, 8, 9, 10, ...]", # Shortened
212+
"b": '140715041065664816...7453168916351054663', # Shortened
213+
"app_id": 140715046161904,
214+
"bar": "im a bar",
215+
}
216+
}
217+
}
218+
]
219+
}
220+
}
221+
}
222+
223+
self.assertEqual(result, expected)

0 commit comments

Comments
 (0)