Skip to content

Commit e81b10d

Browse files
committed
added context processors and docs for RUM correlation (#322)
closes #322 fixes #318
1 parent cd8076a commit e81b10d

File tree

11 files changed

+191
-1
lines changed

11 files changed

+191
-1
lines changed

docs/django.asciidoc

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,48 @@ ELASTIC_APM['TRANSACTIONS_IGNORE_PATTERNS'] = ['^OPTIONS ', 'views.api.v2']
114114

115115
This example ignores any requests using the `OPTIONS` method and any requests containing `views.api.v2`.
116116

117+
[float]
118+
[[django-integrating-with-the-rum-agent]]
119+
==== Integrating with the RUM agent
120+
121+
To correlate performance measurement in the browser with measurements in your Django app,
122+
you can help the RUM (Real User Monitoring) agent by configuring it with the Trace ID and Span ID of the backend request.
123+
We provide a handy template context processor which adds all the necessary bits into the context of your templates.
124+
125+
To enable this feature, first add the `rum_tracing` context processor to your `TEMPLATES` setting.
126+
You most likely already have a list of `context_processors`, in which case you can simply append ours to the list.
127+
128+
[source,python]
129+
----
130+
TEMPLATES = [
131+
{
132+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
133+
'OPTIONS': {
134+
'context_processors': [
135+
# ...
136+
'elasticapm.contrib.django.context_processors.rum_tracing',
137+
],
138+
},
139+
},
140+
]
141+
142+
----
143+
144+
Then, update the call to initialize the RUM agent (which probably happens in your base template) like this:
145+
146+
[source,javascript]
147+
----
148+
elasticApm.init({
149+
serviceName: "my-frontend-service",
150+
pageLoadTraceId: "{{ apm.trace_id }}",
151+
pageLoadSpanId: "{{ apm.span_id }}",
152+
pageLoadSampled: {{ apm.is_sampled_js }}
153+
})
154+
155+
----
156+
157+
See the {apm-rum-ref}[JavaScript RUM agent documentation] for more information.
158+
117159
[float]
118160
[[django-enabling-and-disabling-the-agent]]
119161
=== Enabling and disabling the agent

docs/flask.asciidoc

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,3 +232,28 @@ app.config['ELASTIC_APM'] = {
232232

233233
This would ignore any requests using the `OPTIONS` method
234234
and any requests containing `/api/`.
235+
236+
237+
[float]
238+
[[flask-integrating-with-the-rum-agent]]
239+
==== Integrating with the RUM agent
240+
241+
To correlate performance measurement in the browser with measurements in your Flask app,
242+
you can help the RUM (Real User Monitoring) agent by configuring it with the Trace ID and Span ID of the backend request.
243+
We provide a handy template context processor which adds all the necessary bits into the context of your templates.
244+
245+
The context processor is installed automatically when you initialize `ElasticAPM`.
246+
All that is left to do is to update the call to initialize the RUM agent (which probably happens in your base template) like this:
247+
248+
[source,javascript]
249+
----
250+
elasticApm.init({
251+
serviceName: "my-frontend-service",
252+
pageLoadTraceId: "{{ apm["trace_id"] }}",
253+
pageLoadSpanId: "{{ apm["span_id"]() }}",
254+
pageLoadSampled: {{ apm["is_sampled_js"] }}
255+
})
256+
257+
----
258+
259+
See the {apm-rum-ref}[JavaScript RUM agent documentation] for more information.

elasticapm/contrib/django/apps.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from functools import partial
23

34
from django.apps import AppConfig
@@ -7,6 +8,8 @@
78
from elasticapm.contrib.django.client import get_client
89
from elasticapm.utils.disttracing import TraceParent
910

11+
logger = logging.getLogger("elasticapm.traces")
12+
1013
ERROR_DISPATCH_UID = "elasticapm-exceptions"
1114
REQUEST_START_DISPATCH_UID = "elasticapm-request-start"
1215
REQUEST_FINISH_DISPATCH_UID = "elasticapm-request-stop"
@@ -77,6 +80,7 @@ def _request_started_handler(client, sender, *args, **kwargs):
7780
traceparent_header = None
7881
if traceparent_header:
7982
trace_parent = TraceParent.from_string(traceparent_header)
83+
logger.debug("Read traceparent header %s", traceparent_header)
8084
else:
8185
trace_parent = None
8286
client.begin_transaction("request", trace_parent=trace_parent)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from elasticapm.traces import get_transaction
2+
3+
4+
def rum_tracing(request):
5+
transaction = get_transaction()
6+
if transaction and transaction.trace_parent:
7+
return {
8+
"apm": {
9+
"trace_id": transaction.trace_parent.trace_id,
10+
# only put the callable into the context to ensure that we only change the span_id if the value
11+
# is rendered
12+
"span_id": transaction.ensure_parent_id,
13+
"is_sampled": transaction.is_sampled,
14+
"is_sampled_js": "true" if transaction.is_sampled else "false",
15+
}
16+
}
17+
return {}

elasticapm/contrib/flask/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from elasticapm.conf import constants, setup_logging
2323
from elasticapm.contrib.flask.utils import get_data_from_request, get_data_from_response
2424
from elasticapm.handlers.logging import LoggingHandler
25+
from elasticapm.traces import get_transaction
2526
from elasticapm.utils import build_name_with_http_method_prefix
2627
from elasticapm.utils.disttracing import TraceParent
2728

@@ -131,6 +132,23 @@ def init_app(self, app, **defaults):
131132
else:
132133
logger.debug("Skipping instrumentation. INSTRUMENT is set to False.")
133134

135+
@app.context_processor
136+
def rum_tracing():
137+
"""
138+
Adds APM related IDs to the context used for correlating the backend transaction with the RUM transaction
139+
"""
140+
transaction = get_transaction()
141+
if transaction and transaction.trace_parent:
142+
return {
143+
"apm": {
144+
"trace_id": transaction.trace_parent.trace_id,
145+
"span_id": lambda: transaction.ensure_parent_id(),
146+
"is_sampled": transaction.is_sampled,
147+
"is_sampled_js": "true" if transaction.is_sampled else "false",
148+
}
149+
}
150+
return {}
151+
134152
def request_started(self, app):
135153
if not self.app.debug or self.client.config.debug:
136154
if constants.TRACEPARENT_HEADER_NAME in request.headers:

elasticapm/traces.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
__all__ = ("capture_span", "tag", "set_transaction_name", "set_custom_context", "set_user_context")
1414

1515
error_logger = logging.getLogger("elasticapm.errors")
16+
logger = logging.getLogger("elasticapm.traces")
1617

1718
_time_func = timeit.default_timer
1819

@@ -84,6 +85,17 @@ def end_span(self, skip_frames):
8485
self._tracer.queue_func(SPAN, span.to_dict())
8586
return span
8687

88+
def ensure_parent_id(self):
89+
"""If current trace_parent has no span_id, generate one, then return it
90+
91+
This is used to generate a span ID which the RUM agent will use to correlate
92+
the RUM transaction with the backend transaction.
93+
"""
94+
if self.trace_parent.span_id == self.id:
95+
self.trace_parent.span_id = "%016x" % random.getrandbits(64)
96+
logger.debug("Set parent id to generated %s", self.trace_parent.span_id)
97+
return self.trace_parent.span_id
98+
8799
def to_dict(self):
88100
self.context["tags"] = self.tags
89101
result = {

tests/DockerfileDocs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ RUN for server in $(shuf -e ha.pool.sks-keyservers.net \
99
hkp://p80.pool.sks-keyservers.net:80 \
1010
keyserver.ubuntu.com \
1111
hkp://keyserver.ubuntu.com:80 \
12-
pgp.mit.edu) ; do echo "Trying $server"; gpg --keyserver "$server" --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 && s=0 && break || s=$?; done; (exit $s)
12+
pgp.mit.edu) ; do echo "Trying $server"; gpg --no-tty --keyserver "$server" --recv-keys B42F6819007F00F88E364FD4036A9C25BF357DD4 && s=0 && break || s=$?; done; (exit $s)
1313

1414
RUN curl -o /usr/local/bin/gosu -SL "https://github.com/tianon/gosu/releases/download/1.10/gosu-$(dpkg --print-architecture)" \
1515
&& curl -o /usr/local/bin/gosu.asc -SL "https://github.com/tianon/gosu/releases/download/1.10/gosu-$(dpkg --print-architecture).asc" \

tests/client/client_tests.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -784,3 +784,18 @@ def test_trace_parent_not_set(elasticapm_client):
784784
data = transaction.to_dict()
785785
assert data["trace_id"] is not None
786786
assert "parent_id" not in data
787+
788+
789+
def test_ensure_parent_sets_new_id(elasticapm_client):
790+
transaction = elasticapm_client.begin_transaction("test", trace_parent=None)
791+
assert transaction.id == transaction.trace_parent.span_id
792+
span_id = transaction.ensure_parent_id()
793+
assert span_id == transaction.trace_parent.span_id
794+
795+
796+
def test_ensure_parent_doesnt_change_existing_id(elasticapm_client):
797+
transaction = elasticapm_client.begin_transaction("test", trace_parent=None)
798+
assert transaction.id == transaction.trace_parent.span_id
799+
span_id = transaction.ensure_parent_id()
800+
span_id_2 = transaction.ensure_parent_id()
801+
assert span_id == span_id_2

tests/contrib/django/django_tests.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
import mock
2626

27+
from conftest import BASE_TEMPLATE_DIR
2728
from elasticapm.base import Client
2829
from elasticapm.conf import constants
2930
from elasticapm.conf.constants import ERROR, SPAN, TRANSACTION
@@ -1351,3 +1352,29 @@ def test_options_request(client, django_elasticapm_client):
13511352
client.options("/")
13521353
transactions = django_elasticapm_client.events[TRANSACTION]
13531354
assert transactions[0]["context"]["request"]["method"] == "OPTIONS"
1355+
1356+
1357+
def test_rum_tracing_context_processor(client, django_elasticapm_client):
1358+
with override_settings(
1359+
TEMPLATES=[
1360+
{
1361+
"BACKEND": "django.template.backends.django.DjangoTemplates",
1362+
"DIRS": [BASE_TEMPLATE_DIR],
1363+
"OPTIONS": {
1364+
"context_processors": [
1365+
"django.contrib.auth.context_processors.auth",
1366+
"elasticapm.contrib.django.context_processors.rum_tracing",
1367+
],
1368+
"loaders": ["django.template.loaders.filesystem.Loader"],
1369+
"debug": False,
1370+
},
1371+
}
1372+
],
1373+
**middleware_setting(django.VERSION, ["elasticapm.contrib.django.middleware.TracingMiddleware"])
1374+
):
1375+
response = client.get(reverse("render-heavy-template"))
1376+
transactions = django_elasticapm_client.events[TRANSACTION]
1377+
assert response.context["apm"]["trace_id"] == transactions[0]["trace_id"]
1378+
assert response.context["apm"]["is_sampled"]
1379+
assert response.context["apm"]["is_sampled_js"] == "true"
1380+
assert callable(response.context["apm"]["span_id"])

tests/contrib/flask/flask_tests.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from elasticapm.conf.constants import ERROR, TRANSACTION
99
from elasticapm.contrib.flask import ElasticAPM
1010
from elasticapm.utils import compat
11+
from tests.contrib.flask.utils import captured_templates
1112

1213
try:
1314
from urllib.request import urlopen
@@ -284,3 +285,15 @@ def test_set_transaction_name(flask_apm_client):
284285
transaction = flask_apm_client.client.events[TRANSACTION][0]
285286
assert transaction["name"] == "foo"
286287
assert transaction["result"] == "okydoky"
288+
289+
290+
def test_rum_tracing_context_processor(flask_apm_client):
291+
with captured_templates(flask_apm_client.app) as templates:
292+
resp = flask_apm_client.app.test_client().post("/users/", data={"foo": "bar"})
293+
resp.close()
294+
transaction = flask_apm_client.client.events[TRANSACTION][0]
295+
template, context = templates[0]
296+
assert context["apm"]["trace_id"] == transaction["trace_id"]
297+
assert context["apm"]["is_sampled"]
298+
assert context["apm"]["is_sampled_js"] == "true"
299+
assert callable(context["apm"]["span_id"])

0 commit comments

Comments
 (0)