Skip to content

Commit 6fe3cf6

Browse files
committed
Add support for truncating dicts in local vars (#638)
* Add support for truncating dicts in local vars * Update changelog * Update CONTRIBUTING.md with new changelog name * Match style in changelog * Update CHANGELOG.asciidoc Co-Authored-By: Brandon Morelli <[email protected]> * Add reviewer suggestions * Rename metadata key for dicts to be clearer * Change all refs to "..." * I blame newborn sleep deprivation. * Fix one more instance of "..."
1 parent 0efdaea commit 6fe3cf6

File tree

10 files changed

+63
-4
lines changed

10 files changed

+63
-4
lines changed

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ should "Squash and merge".
101101
If you have commit access, the process is as follows:
102102

103103
1. Update the version in `elasticapm/version.py` according to the scale of the change. (major, minor or patch)
104-
1. Update `CHANGELOG.md`
104+
1. Update `CHANGELOG.asciidoc`
105105
1. Commit changes with message `update CHANGELOG and bump version to X.Y.Z` where `X.Y.Z` is the version in `elasticapm/version.py`
106106
1. Tag the commit with `git tag -a vX.Y.Z`, for example `git tag -a v1.2.3`.
107107
Copy the changelog for the release to the tag message, removing any leading `#`.

docs/configuration.asciidoc

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,18 @@ With this setting, you can limit the length of resulting string.
327327
With this setting, you can limit the length of lists in local variables.
328328

329329

330+
[float]
331+
[[config-local-dict-var-max-length]]
332+
==== `local_var_dict_max_length`
333+
334+
|============
335+
| Environment | Django/Flask | Default
336+
| `ELASTIC_APM_LOCAL_VAR_DICT_MAX_LENGTH` | `LOCAL_VAR_DICT_MAX_LENGTH` | `10`
337+
|============
338+
339+
With this setting, you can limit the length of dicts in local variables.
340+
341+
330342
[float]
331343
[[config-source-lines-error-app-frames]]
332344
==== `source_lines_error_app_frames`

elasticapm/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,7 @@ def __init__(self, config=None, **inline):
172172
v,
173173
list_length=self.config.local_var_list_max_length,
174174
string_length=self.config.local_var_max_length,
175+
dict_length=self.config.local_var_dict_max_length,
175176
),
176177
local_var,
177178
),
@@ -431,6 +432,7 @@ def _build_msg_for_logging(
431432
v,
432433
list_length=self.config.local_var_list_max_length,
433434
string_length=self.config.local_var_max_length,
435+
dict_length=self.config.local_var_dict_max_length,
434436
),
435437
local_var,
436438
),

elasticapm/conf/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ class Config(_ConfigBase):
318318
source_lines_span_library_frames = _ConfigValue("SOURCE_LINES_SPAN_LIBRARY_FRAMES", type=int, default=0)
319319
local_var_max_length = _ConfigValue("LOCAL_VAR_MAX_LENGTH", type=int, default=200)
320320
local_var_list_max_length = _ConfigValue("LOCAL_VAR_LIST_MAX_LENGTH", type=int, default=10)
321+
local_var_dict_max_length = _ConfigValue("LOCAL_VAR_DICT_MAX_LENGTH", type=int, default=10)
321322
capture_body = _ConfigValue("CAPTURE_BODY", default="off")
322323
async_mode = _BoolConfigValue("ASYNC_MODE", default=True)
323324
instrument_django_middleware = _BoolConfigValue("INSTRUMENT_DJANGO_MIDDLEWARE", default=True)

elasticapm/events.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def capture(client, exc_info=None, **kwargs):
105105
val,
106106
list_length=client.config.local_var_list_max_length,
107107
string_length=client.config.local_var_max_length,
108+
dict_length=client.config.local_var_dict_max_length,
108109
),
109110
local_var,
110111
),

elasticapm/processors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,11 @@ def _sanitize(key, value):
245245
if isinstance(value, compat.string_types) and any(pattern.match(value) for pattern in SANITIZE_VALUE_PATTERNS):
246246
return MASK
247247

248+
if isinstance(value, dict):
249+
# varmap will call _sanitize on each k:v pair of the dict, so we don't
250+
# have to do anything with dicts here
251+
return value
252+
248253
if not key: # key can be a NoneType
249254
return value
250255

elasticapm/utils/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def varmap(func, var, context=None, name=None):
6161
return func(name, "<...>")
6262
context.add(objid)
6363
if isinstance(var, dict):
64-
ret = dict((k, varmap(func, v, context, k)) for k, v in compat.iteritems(var))
64+
ret = func(name, dict((k, varmap(func, v, context, k)) for k, v in compat.iteritems(var)))
6565
elif isinstance(var, (list, tuple)):
6666
ret = func(name, [varmap(func, f, context, name) for f in var])
6767
else:

elasticapm/utils/encoding.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333

3434
import datetime
35+
import itertools
3536
import uuid
3637
from decimal import Decimal
3738

@@ -178,15 +179,41 @@ def to_string(value):
178179
return to_unicode(value).encode("utf-8")
179180

180181

181-
def shorten(var, list_length=50, string_length=200):
182+
def shorten(var, list_length=50, string_length=200, dict_length=50):
183+
"""
184+
Shorten a given variable based on configurable maximum lengths, leaving
185+
breadcrumbs in the object to show that it was shortened.
186+
187+
For strings, truncate the string to the max length, and append "..." so
188+
the user knows data was lost.
189+
190+
For lists, truncate the list to the max length, and append two new strings
191+
to the list: "..." and "(<x> more elements)" where <x> is the number of
192+
elements removed.
193+
194+
For dicts, truncate the dict to the max length (based on number of key/value
195+
pairs) and add a new (key, value) pair to the dict:
196+
("...", "(<x> more elements)") where <x> is the number of key/value pairs
197+
removed.
198+
199+
:param var: Variable to be shortened
200+
:param list_length: Max length (in items) of lists
201+
:param string_length: Max length (in characters) of strings
202+
:param dict_length: Max length (in key/value pairs) of dicts
203+
:return: Shortened variable
204+
"""
182205
var = transform(var)
183206
if isinstance(var, compat.string_types) and len(var) > string_length:
184207
var = var[: string_length - 3] + "..."
185208
elif isinstance(var, (list, tuple, set, frozenset)) and len(var) > list_length:
186209
# TODO: we should write a real API for storing some metadata with vars when
187210
# we get around to doing ref storage
188-
# TODO: when we finish the above, we should also implement this for dicts
189211
var = list(var)[:list_length] + ["...", "(%d more elements)" % (len(var) - list_length,)]
212+
elif isinstance(var, dict) and len(var) > dict_length:
213+
trimmed_tuples = [(k, v) for (k, v) in itertools.islice(compat.iteritems(var), dict_length)]
214+
if "<truncated>" not in var:
215+
trimmed_tuples += [("<truncated>", "(%d more elements)" % (len(var) - dict_length))]
216+
var = dict(trimmed_tuples)
190217
return var
191218

192219

tests/processors/tests.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,11 @@ def test_sanitize_credit_card_with_spaces():
231231
assert result == processors.MASK
232232

233233

234+
def test_sanitize_dict():
235+
result = processors._sanitize("foo", {1: 2})
236+
assert result == {1: 2}
237+
238+
234239
def test_non_utf8_encoding(http_test_data):
235240
broken = compat.b("broken=") + u"aéöüa".encode("latin-1")
236241
http_test_data["context"]["request"]["url"]["search"] = broken

tests/utils/encoding/tests.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,12 @@ def test_shorten_tuple():
227227
# assert result[-1] == '(450 more elements)'
228228

229229

230+
def test_shorten_dict():
231+
result = shorten({k: k for k in range(500)}, dict_length=50)
232+
assert len(result) == 51
233+
assert result["<truncated>"] == "(450 more elements)"
234+
235+
230236
def test_enforce_label_format():
231237
class MyObj(object):
232238
def __str__(self):

0 commit comments

Comments
 (0)