Skip to content

Commit d829eae

Browse files
authored
implement transaction_ignore_urls (#923)
* handle transaction_ignore_urls setting [WIP] * added framework specific tests (and fixed issues revealed by the tests...) * implement "outcome" property for transactions and spans This implements elastic/apm#299. Additionally, the "status_code" attribute has been added to HTTP spans. * fix some tests * change default outcome for spans to "unknown" * added API functions for setting transaction outcome * rework outcome API, and make sure it works for unsampled transactions * fix some tests * fix a test and add one for testing override behavior * add an override=False that went forgotten * expand docs a bit * implement transaction_ignore_urls [WIP] * do less work in aiohttp if we're not tracing a transaction * construct path to json tests in a platform independent way * fix merge issues * address review
1 parent ab679c8 commit d829eae

File tree

19 files changed

+294
-39
lines changed

19 files changed

+294
-39
lines changed

elasticapm/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,13 @@ def load_processors(self):
583583
# setdefault has the nice property that it returns the value that it just set on the dict
584584
return [seen.setdefault(path, import_string(path)) for path in processors if path not in seen]
585585

586+
def should_ignore_url(self, url):
587+
if self.config.transaction_ignore_urls:
588+
for pattern in self.config.transaction_ignore_urls:
589+
if pattern.match(url):
590+
return True
591+
return False
592+
586593
def check_python_version(self):
587594
v = tuple(map(int, platform.python_version_tuple()[:2]))
588595
if v == (2, 7):

elasticapm/conf/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,7 @@ class Config(_ConfigBase):
556556
instrument_django_middleware = _BoolConfigValue("INSTRUMENT_DJANGO_MIDDLEWARE", default=True)
557557
autoinsert_django_middleware = _BoolConfigValue("AUTOINSERT_DJANGO_MIDDLEWARE", default=True)
558558
transactions_ignore_patterns = _ListConfigValue("TRANSACTIONS_IGNORE_PATTERNS", default=[])
559+
transaction_ignore_urls = _ListConfigValue("TRANSACTION_IGNORE_URLS", type=starmatch_to_regex, default=[])
559560
service_version = _ConfigValue("SERVICE_VERSION")
560561
framework_name = _ConfigValue("FRAMEWORK_NAME")
561562
framework_version = _ConfigValue("FRAMEWORK_VERSION")

elasticapm/contrib/aiohttp/middleware.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ def tracing_middleware(app):
4848

4949
async def handle_request(request, handler):
5050
elasticapm_client = app.get(CLIENT_KEY)
51-
if elasticapm_client:
51+
should_trace = elasticapm_client and not elasticapm_client.should_ignore_url(request.path)
52+
if should_trace:
5253
request[CLIENT_KEY] = elasticapm_client
5354
trace_parent = AioHttpTraceParent.from_headers(request.headers)
5455
elasticapm_client.begin_transaction("request", trace_parent=trace_parent)
@@ -71,11 +72,13 @@ async def handle_request(request, handler):
7172

7273
try:
7374
response = await handler(request)
74-
elasticapm.set_transaction_result("HTTP {}xx".format(response.status // 100), override=False)
75-
elasticapm.set_transaction_outcome(http_status_code=response.status, override=False)
76-
elasticapm.set_context(
77-
lambda: get_data_from_response(response, elasticapm_client.config, constants.TRANSACTION), "response"
78-
)
75+
if should_trace:
76+
elasticapm.set_transaction_result("HTTP {}xx".format(response.status // 100), override=False)
77+
elasticapm.set_transaction_outcome(http_status_code=response.status, override=False)
78+
elasticapm.set_context(
79+
lambda: get_data_from_response(response, elasticapm_client.config, constants.TRANSACTION),
80+
"response",
81+
)
7982
return response
8083
except Exception as exc:
8184
if elasticapm_client:

elasticapm/contrib/django/apps.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from elasticapm.contrib.django.client import get_client
3838
from elasticapm.utils.disttracing import TraceParent
3939
from elasticapm.utils.logging import get_logger
40+
from elasticapm.utils.wsgi import get_current_url
4041

4142
logger = get_logger("elasticapm.traces")
4243

@@ -135,17 +136,27 @@ def _request_started_handler(client, sender, *args, **kwargs):
135136
if not _should_start_transaction(client):
136137
return
137138
# try to find trace id
139+
trace_parent = None
138140
if "environ" in kwargs:
141+
url = get_current_url(kwargs["environ"], strip_querystring=True, path_only=True)
142+
if client.should_ignore_url(url):
143+
logger.debug("Ignoring request due to %s matching transaction_ignore_urls")
144+
return
139145
trace_parent = TraceParent.from_headers(
140146
kwargs["environ"],
141147
TRACEPARENT_HEADER_NAME_WSGI,
142148
TRACEPARENT_LEGACY_HEADER_NAME_WSGI,
143149
TRACESTATE_HEADER_NAME_WSGI,
144150
)
145-
elif "scope" in kwargs and "headers" in kwargs["scope"]:
146-
trace_parent = TraceParent.from_headers(kwargs["scope"]["headers"])
147-
else:
148-
trace_parent = None
151+
elif "scope" in kwargs:
152+
scope = kwargs["scope"]
153+
fake_environ = {"SCRIPT_NAME": scope.get("root_path", ""), "PATH_INFO": scope["path"], "QUERY_STRING": ""}
154+
url = get_current_url(fake_environ, strip_querystring=True, path_only=True)
155+
if client.should_ignore_url(url):
156+
logger.debug("Ignoring request due to %s matching transaction_ignore_urls")
157+
return
158+
if "headers" in scope:
159+
trace_parent = TraceParent.from_headers(scope["headers"])
149160
client.begin_transaction("request", trace_parent=trace_parent)
150161

151162

elasticapm/contrib/flask/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ def rum_tracing():
178178
return {}
179179

180180
def request_started(self, app):
181-
if not self.app.debug or self.client.config.debug:
181+
if (not self.app.debug or self.client.config.debug) and not self.client.should_ignore_url(request.path):
182182
trace_parent = TraceParent.from_headers(request.headers)
183183
self.client.begin_transaction("request", trace_parent=trace_parent)
184184
elasticapm.set_context(

elasticapm/contrib/starlette/__init__.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,14 @@ async def _request_started(self, request: Request):
174174
Args:
175175
request (Request)
176176
"""
177-
trace_parent = TraceParent.from_headers(dict(request.headers))
178-
self.client.begin_transaction("request", trace_parent=trace_parent)
177+
if not self.client.should_ignore_url(request.url.path):
178+
trace_parent = TraceParent.from_headers(dict(request.headers))
179+
self.client.begin_transaction("request", trace_parent=trace_parent)
179180

180-
await set_context(lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request")
181-
elasticapm.set_transaction_name("{} {}".format(request.method, request.url.path), override=False)
181+
await set_context(
182+
lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request"
183+
)
184+
elasticapm.set_transaction_name("{} {}".format(request.method, request.url.path), override=False)
182185

183186
async def _request_finished(self, response: Response):
184187
"""Captures the end of the request processing to APM.

elasticapm/instrumentation/packages/tornado.py

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -52,25 +52,28 @@ async def call(self, module, method, wrapped, instance, args, kwargs):
5252
from elasticapm.contrib.tornado.utils import get_data_from_request, get_data_from_response
5353

5454
request = instance.request
55-
trace_parent = TraceParent.from_headers(request.headers)
5655
client = instance.application.elasticapm_client
57-
client.begin_transaction("request", trace_parent=trace_parent)
58-
elasticapm.set_context(
59-
lambda: get_data_from_request(instance, request, client.config, constants.TRANSACTION), "request"
60-
)
61-
# TODO: Can we somehow incorporate the routing rule itself here?
62-
elasticapm.set_transaction_name("{} {}".format(request.method, type(instance).__name__), override=False)
56+
should_ignore = client.should_ignore_url(request.path)
57+
if not should_ignore:
58+
trace_parent = TraceParent.from_headers(request.headers)
59+
client.begin_transaction("request", trace_parent=trace_parent)
60+
elasticapm.set_context(
61+
lambda: get_data_from_request(instance, request, client.config, constants.TRANSACTION), "request"
62+
)
63+
# TODO: Can we somehow incorporate the routing rule itself here?
64+
elasticapm.set_transaction_name("{} {}".format(request.method, type(instance).__name__), override=False)
6365

6466
ret = await wrapped(*args, **kwargs)
6567

66-
elasticapm.set_context(
67-
lambda: get_data_from_response(instance, client.config, constants.TRANSACTION), "response"
68-
)
69-
status = instance.get_status()
70-
result = "HTTP {}xx".format(status // 100)
71-
elasticapm.set_transaction_result(result, override=False)
72-
elasticapm.set_transaction_outcome(http_status_code=status)
73-
client.end_transaction()
68+
if not should_ignore:
69+
elasticapm.set_context(
70+
lambda: get_data_from_response(instance, client.config, constants.TRANSACTION), "response"
71+
)
72+
status = instance.get_status()
73+
result = "HTTP {}xx".format(status // 100)
74+
elasticapm.set_transaction_result(result, override=False)
75+
elasticapm.set_transaction_outcome(http_status_code=status)
76+
client.end_transaction()
7477

7578
return ret
7679

elasticapm/traces.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -617,12 +617,6 @@ def begin_transaction(self, transaction_type, trace_parent=None, start=None):
617617
execution_context.set_transaction(transaction)
618618
return transaction
619619

620-
def _should_ignore(self, transaction_name):
621-
for pattern in self._ignore_patterns:
622-
if pattern.search(transaction_name):
623-
return True
624-
return False
625-
626620
def end_transaction(self, result=None, transaction_name=None, duration=None):
627621
"""
628622
End the current transaction and queue it for sending
@@ -643,6 +637,12 @@ def end_transaction(self, result=None, transaction_name=None, duration=None):
643637
self.queue_func(TRANSACTION, transaction.to_dict())
644638
return transaction
645639

640+
def _should_ignore(self, transaction_name):
641+
for pattern in self._ignore_patterns:
642+
if pattern.search(transaction_name):
643+
return True
644+
return False
645+
646646

647647
class capture_span(object):
648648
__slots__ = (

elasticapm/utils/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ def read_pem_file(file_obj):
177177

178178

179179
def starmatch_to_regex(pattern):
180+
options = re.DOTALL
181+
# check if we are case sensitive
182+
if pattern.startswith("(?-i)"):
183+
pattern = pattern[5:]
184+
else:
185+
options |= re.IGNORECASE
180186
i, n = 0, len(pattern)
181187
res = []
182188
while i < n:
@@ -186,4 +192,4 @@ def starmatch_to_regex(pattern):
186192
res.append(".*")
187193
else:
188194
res.append(re.escape(c))
189-
return re.compile(r"(?:%s)\Z" % "".join(res), re.IGNORECASE | re.DOTALL)
195+
return re.compile(r"(?:%s)\Z" % "".join(res), options)

elasticapm/utils/wsgi.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def get_host(environ):
8888

8989

9090
# `get_current_url` comes from `werkzeug.wsgi`
91-
def get_current_url(environ, root_only=False, strip_querystring=False, host_only=False):
91+
def get_current_url(environ, root_only=False, strip_querystring=False, host_only=False, path_only=False):
9292
"""A handy helper function that recreates the full URL for the current
9393
request or parts of it. Here an example:
9494
@@ -107,8 +107,12 @@ def get_current_url(environ, root_only=False, strip_querystring=False, host_only
107107
:param root_only: set `True` if you only want the root URL.
108108
:param strip_querystring: set to `True` if you don't want the querystring.
109109
:param host_only: set to `True` if the host URL should be returned.
110+
:param path_only: set to `True` if only the path should be returned.
110111
"""
111-
tmp = [environ["wsgi.url_scheme"], "://", get_host(environ)]
112+
if path_only:
113+
tmp = []
114+
else:
115+
tmp = [environ["wsgi.url_scheme"], "://", get_host(environ)]
112116
cat = tmp.append
113117
if host_only:
114118
return "".join(tmp) + "/"

0 commit comments

Comments
 (0)