Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ignore frames from frame collection logic #167

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 7 additions & 4 deletions elasticapm/base.py
Expand Up @@ -129,9 +129,9 @@ def __init__(self, config=None, **defaults):

self.processors = [import_string(p) for p in self.config.processors] if self.config.processors else []

self.instrumentation_store = TransactionsStore(
lambda: self._get_stack_info_for_trace(
stacks.iter_stack_frames(),
def frames_collector_func():
return self._get_stack_info_for_trace(
stacks.iter_stack_frames(skip_top_modules=('elasticapm.',)),
library_frame_context_lines=self.config.source_lines_span_library_frames,
in_app_frame_context_lines=self.config.source_lines_span_app_frames,
with_locals=self.config.collect_local_variables in ('all', 'transactions'),
Expand All @@ -140,7 +140,10 @@ def __init__(self, config=None, **defaults):
list_length=self.config.local_var_list_max_length,
string_length=self.config.local_var_max_length,
), local_var)
),
)

self.instrumentation_store = TransactionsStore(
frames_collector_func=frames_collector_func,
collect_frequency=self.config.flush_interval,
sample_rate=self.config.transaction_sample_rate,
max_spans=self.config.transaction_max_spans,
Expand Down
11 changes: 1 addition & 10 deletions elasticapm/contrib/django/client.py
Expand Up @@ -179,7 +179,7 @@ def _get_stack_info_for_trace(self, frames,
locals_processor_func=None):
"""If the stacktrace originates within the elasticapm module, it will skip
frames until some other module comes up."""
frames = list(iterate_with_template_sources(
return list(iterate_with_template_sources(
frames,
with_locals=with_locals,
library_frame_context_lines=library_frame_context_lines,
Expand All @@ -188,15 +188,6 @@ def _get_stack_info_for_trace(self, frames,
exclude_paths_re=self.exclude_paths_re,
locals_processor_func=locals_processor_func,
))
i = 0
while len(frames) > i:
if 'module' in frames[i] and not (
frames[i]['module'].startswith('elasticapm.') or
frames[i]['module'] == 'contextlib'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happens with contextlib, don't we want to filter it out anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that's a relic from the olden days. We don't use contextlib anywhere in the code base

):
return frames[i:]
i += 1
return frames

def send(self, url, **kwargs):
"""
Expand Down
25 changes: 22 additions & 3 deletions elasticapm/utils/stacks.py
Expand Up @@ -147,24 +147,43 @@ def iter_traceback_frames(tb):
while tb:
# support for __traceback_hide__ which is used by a few libraries
# to hide internal frames.
f_locals = getattr(tb.tb_frame, 'f_locals', {})
frame = tb.tb_frame
f_locals = getattr(frame, 'f_locals', {})
if not _getitem_from_frame(f_locals, '__traceback_hide__'):
yield tb.tb_frame, getattr(tb, 'tb_lineno', None)
yield frame, getattr(tb, 'tb_lineno', None)
tb = tb.tb_next


def iter_stack_frames(frames=None, skip=0):
def iter_stack_frames(frames=None, skip=0, skip_top_modules=()):
"""
Given an optional list of frames (defaults to current stack),
iterates over all frames that do not contain the ``__traceback_hide__``
local variable.

Frames can be skipped by either providing a number, or a tuple
of module names. If the module of a frame matches one of the names
(using `.startswith`, that frame will be skipped. This matching will only
be done until the first frame doesn't match.
Copy link
Contributor

@jalvz jalvz Feb 21, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know you are doing this in a generic way, but for now you are only using it for the elasticapm.
in that case, don't you want to filter them out if even they are interleaved (like elasticapm.something -> database.something -> elasticapm.something_else)?
if that can not happen, why go through the troubles of implementing some logic you dont need?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Glad you asked :D I actually intend to filter more than just elasticapm.. In #164, I introduce a functools.partial call. In CPython, this doesn't create a frame (I suppose because it's implemented in C? Not sure). But in PyPy, this creates a frame, with the module _functools. So my intention is to rebase that PR after merging this one, and then adding _functools to that list. But we only want to kill that one frame at the top, and not any functool.partial calls that come later on.

Another argument for not filtering out all frames that match is that this should only be used to cut away the top frames of the stack trace that are introduced by stack collection itself. If you call into any elasticapm code in other parts of the stack, that should IMO not be suppressed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that makes perfect sense. i am wiser now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there it is:

if platform.python_implementation() == 'PyPy':
# PyPy introduces a `_functools.partial.__call__` frame due to our use
# of `partial` in AbstractInstrumentedModule
skip_modules = ('elasticapm.', '_functools')
else:
skip_modules = ('elasticapm.',)


This is useful to filter out frames that are caused by frame collection
itself.

:param frames: a list of frames, or None
:param skip: number of frames to skip from the beginning
:param skip_top_modules: tuple of strings

"""
if not frames:
frame = inspect.currentframe().f_back
frames = _walk_stack(frame)
stop_ignoring = False
for i, frame in enumerate(frames):
if i < skip:
continue
f_globals = getattr(frame, 'f_globals', {})
if not stop_ignoring and f_globals.get('__name__', '').startswith(skip_top_modules):
continue
stop_ignoring = True
f_locals = getattr(frame, 'f_locals', {})
if not _getitem_from_frame(f_locals, '__traceback_hide__'):
yield frame, frame.f_lineno
Expand Down
6 changes: 3 additions & 3 deletions tests/client/client_tests.py
Expand Up @@ -552,7 +552,7 @@ def test_collect_local_variables_transactions(should_collect, elasticapm_client)
pass
elasticapm_client.end_transaction('test', 'ok')
transaction = elasticapm_client.instrumentation_store.get_all()[0]
frame = transaction['spans'][0]['stacktrace'][5]
frame = transaction['spans'][0]['stacktrace'][0]
if mode in ('transactions', 'all'):
assert 'vars' in frame, mode
assert frame['vars']['a_local_var'] == 1
Expand All @@ -578,8 +578,8 @@ def test_collect_source_transactions(should_collect, elasticapm_client):
pass
elasticapm_client.end_transaction('test', 'ok')
transaction = elasticapm_client.instrumentation_store.get_all()[0]
in_app_frame = transaction['spans'][0]['stacktrace'][5]
library_frame = transaction['spans'][0]['stacktrace'][6]
in_app_frame = transaction['spans'][0]['stacktrace'][0]
library_frame = transaction['spans'][0]['stacktrace'][1]
assert not in_app_frame['library_frame']
assert library_frame['library_frame']
if library_frame_context:
Expand Down
2 changes: 1 addition & 1 deletion tests/contrib/django/django_tests.py
Expand Up @@ -966,7 +966,7 @@ def test_stacktrace_filtered_for_elasticapm(client, django_elasticapm_client):
assert spans[1]['name'] == 'list_users.html'

# Top frame should be inside django rendering
assert spans[1]['stacktrace'][0]['module'].startswith('django.template')
assert spans[1]['stacktrace'][0]['module'].startswith('django.template'), spans[1]['stacktrace'][0]['function']


@pytest.mark.parametrize('django_elasticapm_client', [{'_wait_to_first_send': 100}], indirect=True)
Expand Down
10 changes: 10 additions & 0 deletions tests/instrumentation/base_tests.py
Expand Up @@ -4,6 +4,7 @@
import mock
import pytest

import elasticapm
from elasticapm.instrumentation.packages.base import (AbstractInstrumentedModule,
OriginalNamesBoundFunctionWrapper)
from elasticapm.utils import compat
Expand Down Expand Up @@ -79,3 +80,12 @@ def test_skip_instrument_env_var():
with mock.patch.dict('os.environ', {'SKIP_INSTRUMENT_TEST_DUMMY_INSTRUMENT': 'foo'}):
instrumentation.instrument()
assert not instrumentation.instrumented


def test_skip_ignored_frames(elasticapm_client):
elasticapm_client.begin_transaction('test')
with elasticapm.capture_span('test'):
pass
transaction = elasticapm_client.end_transaction('test', 'test')
for frame in transaction.spans[0].frames:
assert not frame['module'].startswith('elasticapm')
27 changes: 27 additions & 0 deletions tests/utils/stacks/tests.py
Expand Up @@ -78,6 +78,33 @@ def get_me_a_filtered_frame(hide=True):
assert frames[0]['function'] == 'get_me_a_filtered_frame'


def test_iter_stack_frames_skip_frames():
frames = [
Mock(f_lineno=1, f_globals={}),
Mock(f_lineno=2, f_globals={}),
Mock(f_lineno=3, f_globals={}),
Mock(f_lineno=4, f_globals={}),
]

iterated_frames = list(stacks.iter_stack_frames(frames, skip=3))
assert len(iterated_frames) == 1
assert iterated_frames[0][1] == 4


def test_iter_stack_frames_skip_frames_by_module():
frames = [
Mock(f_lineno=1, f_globals={'__name__': 'foo.bar'}),
Mock(f_lineno=2, f_globals={'__name__': 'foo.bar'}),
Mock(f_lineno=3, f_globals={'__name__': 'baz.bar'}),
Mock(f_lineno=4, f_globals={'__name__': 'foo.bar'}),
]

iterated_frames = list(stacks.iter_stack_frames(frames, skip_top_modules=('foo.',)))
assert len(iterated_frames) == 2
assert iterated_frames[0][1] == 3
assert iterated_frames[1][1] == 4


@pytest.mark.parametrize('elasticapm_client', [{
'include_paths': ('/a/b/c/*', '/c/d/*'),
'exclude_paths': ('/c/*',)
Expand Down