def test_traceparent_from_header(): header = "00-0000651916cd43dd8448eb211c80319c-b7ad6b7169203331-03" legacy_header = "00-1111651916cd43dd8448eb211c80319c-b7ad6b7169203331-03" tp1 = TraceParent.from_headers({"traceparent": header}) tp2 = TraceParent.from_headers({"elastic-apm-traceparent": legacy_header}) tp3 = TraceParent.from_headers({ "traceparent": header, "elastic-apm-traceparent": legacy_header }) assert tp1.to_string() == header assert tp2.to_string() == legacy_header # traceparent has precedence over elastic-apm-traceparent assert tp3.to_string() == header
def get_trace_parent(celery_task): """ Return a trace parent contained in the request headers of a Celery Task object or None """ trace_parent = None with suppress(AttributeError, KeyError, TypeError): if celery_task.request.headers is not None: trace_parent_string = celery_task.request.headers["elasticapm"][ "trace_parent_string"] trace_parent = TraceParent.from_string(trace_parent_string) else: trace_parent_string = celery_task.request.elasticapm[ "trace_parent_string"] trace_parent = TraceParent.from_string(trace_parent_string) return trace_parent
def test_span_only_dropped(instrument, elasticapm_client, waiting_httpserver): """test that urllib instrumentation does not fail if no parent span can be found""" waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" transaction_object = elasticapm_client.begin_transaction("transaction") for i in range(2): with capture_span("test", "test"): urlopen(url) elasticapm_client.end_transaction("bla", "OK") trace_parent_1 = TraceParent.from_string(waiting_httpserver.requests[0].headers[constants.TRACEPARENT_HEADER_NAME]) trace_parent_2 = TraceParent.from_string(waiting_httpserver.requests[1].headers[constants.TRACEPARENT_HEADER_NAME]) assert trace_parent_1.span_id != transaction_object.id # second request should use transaction id as span id because there is no span assert trace_parent_2.span_id == transaction_object.id
def test_trace_parent_wrong_version_255(caplog): """Version FF or 255 is explicitly forbidden""" header = "ff-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-03" with caplog.at_level("DEBUG", "elasticapm.utils"): trace_parent = TraceParent.from_string(header) assert trace_parent is None assert_any_record_contains(caplog.records, "Invalid version field, value 255")
def test_httpx_instrumentation(instrument, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" parsed_url = compat.urlparse.urlparse(url) elasticapm_client.begin_transaction("transaction.test") with capture_span("test_request", "test"): httpx.get(url, **allow_redirects) elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) assert spans[0]["name"].startswith("GET 127.0.0.1:") assert spans[0]["type"] == "external" assert spans[0]["subtype"] == "http" assert url == spans[0]["context"]["http"]["url"] assert spans[0]["context"]["destination"]["service"] == { "name": "", "resource": "127.0.0.1:%d" % parsed_url.port, "type": "", } headers = waiting_httpserver.requests[0].headers assert constants.TRACEPARENT_HEADER_NAME in headers trace_parent = TraceParent.from_string( headers[constants.TRACEPARENT_HEADER_NAME], tracestate_string=headers[constants.TRACESTATE_HEADER_NAME] ) assert trace_parent.trace_id == transactions[0]["trace_id"] # Check that sample_rate was correctly placed in the tracestate assert constants.TRACESTATE.SAMPLE_RATE in trace_parent.tracestate_dict # this should be the span id of `httpx`, not of httpcore assert trace_parent.span_id == spans[0]["id"] assert trace_parent.trace_options.recorded
def test_trace_parent_wrong_format(caplog): header = "00" with caplog.at_level("DEBUG", "elasticapm.utils"): trace_parent = TraceParent.from_string(header) record = caplog.records[0] assert trace_parent is None assert record.message == "Invalid traceparent header format, value 00"
async def test_trace_parent_propagation_unsampled(instrument, event_loop, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" transaction_object = elasticapm_client.begin_transaction("transaction") transaction_object.is_sampled = False async with aiohttp.ClientSession() as session: async with session.get(waiting_httpserver.url) as resp: status = resp.status text = await resp.text() elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[constants.TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) assert not spans headers = waiting_httpserver.requests[0].headers assert constants.TRACEPARENT_HEADER_NAME in headers trace_parent = TraceParent.from_string( headers[constants.TRACEPARENT_HEADER_NAME], tracestate_string=headers[constants.TRACESTATE_HEADER_NAME]) assert trace_parent.trace_id == transactions[0]["trace_id"] assert trace_parent.span_id == transaction_object.id assert not trace_parent.trace_options.recorded # Check that sample_rate was correctly placed in the tracestate assert constants.TRACESTATE.SAMPLE_RATE in trace_parent.tracestate_dict if elasticapm_client.config.use_elastic_traceparent_header: assert constants.TRACEPARENT_LEGACY_HEADER_NAME in headers assert headers[constants.TRACEPARENT_HEADER_NAME] == headers[ constants.TRACEPARENT_LEGACY_HEADER_NAME] else: assert constants.TRACEPARENT_LEGACY_HEADER_NAME not in headers
def test_trace_parent(elasticapm_client): trace_parent = TraceParent.from_string("00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-03") elasticapm_client.begin_transaction("test", trace_parent=trace_parent) transaction = elasticapm_client.end_transaction("test", "OK") data = transaction.to_dict() assert data["trace_id"] == "0af7651916cd43dd8448eb211c80319c" assert data["parent_id"] == "b7ad6b7169203331"
def begin_transaction(self, transaction_type, trace_parent=None): """ Start a new transactions and bind it in a thread-local variable :returns the Transaction object """ if trace_parent: is_sampled = bool(trace_parent.trace_options.recorded) else: is_sampled = ( self.config.transaction_sample_rate == 1.0 or self.config.transaction_sample_rate > random.random()) transaction = Transaction(self, transaction_type, trace_parent=trace_parent, is_sampled=is_sampled) if trace_parent is None: transaction.trace_parent = TraceParent( constants.TRACE_CONTEXT_VERSION, "%032x" % random.getrandbits(128), transaction.id, TracingOptions(recorded=is_sampled), ) execution_context.set_transaction(transaction) return transaction
def test_get_trace_parent_header(elasticapm_client): trace_parent = TraceParent.from_string( "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-03") transaction = elasticapm_client.begin_transaction( "test", trace_parent=trace_parent) assert transaction.trace_parent.to_string( ) == elasticapm.get_trace_parent_header()
def request_started(self, app): if not self.app.debug or self.client.config.debug: if constants.TRACEPARENT_HEADER_NAME in request.headers: trace_parent = TraceParent.from_string(request.headers[constants.TRACEPARENT_HEADER_NAME]) else: trace_parent = None self.client.begin_transaction("request", trace_parent=trace_parent)
def test_trace_parent_wrong_trace_options_field(caplog): header = "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-xx" with caplog.at_level("DEBUG", "elasticapm.utils"): trace_parent = TraceParent.from_string(header) assert trace_parent is None assert_any_record_contains(caplog.records, "Invalid trace-options field, value xx")
def test_trace_parent_wrong_format(caplog): header = "00" with caplog.at_level("DEBUG", "elasticapm.utils"): trace_parent = TraceParent.from_string(header) assert trace_parent is None assert_any_record_contains(caplog.records, "Invalid traceparent header format, value 00")
def test_httplib2_instrumentation(instrument, elasticapm_client, waiting_httpserver, args): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" parsed_url = compat.urlparse.urlparse(url) elasticapm_client.begin_transaction("transaction.test") with capture_span("test_request", "test"): httplib2.Http().request(url, *args) elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) assert spans[0]["name"].startswith("GET 127.0.0.1:") assert spans[0]["type"] == "external" assert spans[0]["subtype"] == "http" assert url == spans[0]["context"]["http"]["url"] assert 200 == spans[0]["context"]["http"]["status_code"] assert spans[0]["context"]["destination"]["service"] == { "name": "http://127.0.0.1:%d" % parsed_url.port, "resource": "127.0.0.1:%d" % parsed_url.port, "type": "external", } assert spans[0]["outcome"] == "success" assert constants.TRACEPARENT_HEADER_NAME in waiting_httpserver.requests[ 0].headers trace_parent = TraceParent.from_string( waiting_httpserver.requests[0].headers[ constants.TRACEPARENT_HEADER_NAME], tracestate_string=waiting_httpserver.requests[0].headers[ constants.TRACESTATE_HEADER_NAME], ) assert trace_parent.trace_id == transactions[0]["trace_id"] # Check that sample_rate was correctly placed in the tracestate assert constants.TRACESTATE.SAMPLE_RATE in trace_parent.tracestate_dict
def test_trace_parent_propagation_unsampled(instrument, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" transaction_object = elasticapm_client.begin_transaction("transaction") transaction_object.is_sampled = False urlopen(url) elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) assert not spans headers = waiting_httpserver.requests[0].headers assert constants.TRACEPARENT_HEADER_NAME in headers trace_parent = TraceParent.from_string( headers[constants.TRACEPARENT_HEADER_NAME]) assert trace_parent.trace_id == transactions[0]["trace_id"] assert trace_parent.span_id == transaction_object.id assert not trace_parent.trace_options.recorded if elasticapm_client.config.use_elastic_traceparent_header: assert constants.TRACEPARENT_LEGACY_HEADER_NAME in headers assert headers[constants.TRACEPARENT_HEADER_NAME] == headers[ constants.TRACEPARENT_LEGACY_HEADER_NAME] else: assert constants.TRACEPARENT_LEGACY_HEADER_NAME not in headers
async def call(self, module, method, wrapped, instance, args, kwargs): if not hasattr(instance.application, "elasticapm_client"): # If tornado was instrumented but not as the main framework # (i.e. in Flower), we should skip it. return await wrapped(*args, **kwargs) # Late import to avoid ImportErrors from elasticapm.contrib.tornado.utils import get_data_from_request, get_data_from_response request = instance.request trace_parent = TraceParent.from_headers(request.headers) client = instance.application.elasticapm_client client.begin_transaction("request", trace_parent=trace_parent) elasticapm.set_context( lambda: get_data_from_request(instance, request, client.config, constants.TRANSACTION), "request") # TODO: Can we somehow incorporate the routing rule itself here? elasticapm.set_transaction_name("{} {}".format( request.method, type(instance).__name__), override=False) ret = await wrapped(*args, **kwargs) elasticapm.set_context( lambda: get_data_from_response(instance, client.config, constants. TRANSACTION), "response") status = instance.get_status() result = "HTTP {}xx".format(status // 100) elasticapm.set_transaction_result(result, override=False) elasticapm.set_transaction_outcome(http_status_code=status) client.end_transaction() return ret
def begin_transaction(self, transaction_type, trace_parent=None, start=None): """ Start a new transactions and bind it in a thread-local variable :param transaction_type: type of the transaction, e.g. "request" :param trace_parent: an optional TraceParent object :param start: override the start timestamp, mostly useful for testing :returns the Transaction object """ if trace_parent: is_sampled = bool(trace_parent.trace_options.recorded) else: is_sampled = ( self.config.transaction_sample_rate == 1.0 or self.config.transaction_sample_rate > random.random() ) transaction = Transaction(self, transaction_type, trace_parent=trace_parent, is_sampled=is_sampled, start=start) if trace_parent is None: transaction.trace_parent = TraceParent( constants.TRACE_CONTEXT_VERSION, "%032x" % random.getrandbits(128), transaction.id, TracingOptions(recorded=is_sampled), ) execution_context.set_transaction(transaction) return transaction
def test_trace_parent_propagation_sampled(instrument, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" elasticapm_client.begin_transaction("transaction") urlopen(url) elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) headers = waiting_httpserver.requests[0].headers assert constants.TRACEPARENT_HEADER_NAME in headers trace_parent = TraceParent.from_string( headers[constants.TRACEPARENT_HEADER_NAME], tracestate_string=headers[constants.TRACESTATE_HEADER_NAME]) assert trace_parent.trace_id == transactions[0]["trace_id"] assert trace_parent.span_id == spans[0]["id"] assert trace_parent.trace_options.recorded # Check that sample_rate was correctly placed in the tracestate assert constants.TRACESTATE.SAMPLE_RATE in trace_parent.tracestate_dict if elasticapm_client.config.use_elastic_traceparent_header: assert constants.TRACEPARENT_LEGACY_HEADER_NAME in headers assert headers[constants.TRACEPARENT_HEADER_NAME] == headers[ constants.TRACEPARENT_LEGACY_HEADER_NAME] else: assert constants.TRACEPARENT_LEGACY_HEADER_NAME not in headers
def test_requests_instrumentation_via_prepared_request(instrument, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" elasticapm_client.begin_transaction("transaction.test") with capture_span("test_request", "test"): r = requests.Request("get", url) pr = r.prepare() s = requests.Session() s.send(pr, allow_redirects=False) elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) assert spans[0]["name"].startswith("GET 127.0.0.1:") assert url == spans[0]["context"]["http"]["url"] assert constants.TRACEPARENT_HEADER_NAME in waiting_httpserver.requests[ 0].headers trace_parent = TraceParent.from_string( waiting_httpserver.requests[0].headers[ constants.TRACEPARENT_HEADER_NAME]) assert trace_parent.trace_id == transactions[0]["trace_id"] # this should be the span id of `requests`, not of urllib3 assert trace_parent.span_id == spans[0]["id"] assert trace_parent.trace_options.recorded
def test_trace_parent_propagation_unsampled(instrument, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" transaction_object = elasticapm_client.begin_transaction("transaction") transaction_object.is_sampled = False pool = urllib3.PoolManager(timeout=0.1) r = pool.request("GET", url) elasticapm_client.end_transaction("MyView") spans = elasticapm_client.events[SPAN] assert not spans headers = waiting_httpserver.requests[0].headers assert constants.TRACEPARENT_HEADER_NAME in headers trace_parent = TraceParent.from_string( headers[constants.TRACEPARENT_HEADER_NAME], tracestate_string=headers[constants.TRACESTATE_HEADER_NAME]) assert trace_parent.trace_id == transaction_object.trace_parent.trace_id assert trace_parent.span_id == transaction_object.id assert not trace_parent.trace_options.recorded # Check that sample_rate was correctly placed in the tracestate assert constants.TRACESTATE.SAMPLE_RATE in trace_parent.tracestate_dict if elasticapm_client.config.use_elastic_traceparent_header: assert constants.TRACEPARENT_LEGACY_HEADER_NAME in headers assert headers[constants.TRACEPARENT_HEADER_NAME] == headers[ constants.TRACEPARENT_LEGACY_HEADER_NAME] else: assert constants.TRACEPARENT_LEGACY_HEADER_NAME not in headers
async def test_httpx_instrumentation_string_url(instrument, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" elasticapm_client.begin_transaction("transaction.test") async with async_capture_span("test_request", "test"): async with httpx.AsyncClient() as client: await client.get(url, allow_redirects=False) elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) assert spans[0]["name"].startswith("GET 127.0.0.1:") assert url == spans[0]["context"]["http"]["url"] headers = waiting_httpserver.requests[0].headers assert constants.TRACEPARENT_HEADER_NAME in headers trace_parent = TraceParent.from_string( headers[constants.TRACEPARENT_HEADER_NAME], tracestate_string=headers[constants.TRACESTATE_HEADER_NAME]) assert trace_parent.trace_id == transactions[0]["trace_id"] # Check that sample_rate was correctly placed in the tracestate assert constants.TRACESTATE.SAMPLE_RATE in trace_parent.tracestate_dict # this should be the span id of `httpx`, not of urllib3 assert trace_parent.span_id == spans[0]["id"] assert trace_parent.trace_options.recorded
def test_trace_parent_propagation_sampled(instrument, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" elasticapm_client.begin_transaction("transaction") pool = urllib3.PoolManager(timeout=0.1) r = pool.request("GET", url) elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) headers = waiting_httpserver.requests[0].headers assert constants.TRACEPARENT_HEADER_NAME in headers trace_parent = TraceParent.from_string( headers[constants.TRACEPARENT_HEADER_NAME]) assert trace_parent.trace_id == transactions[0]["trace_id"] assert trace_parent.span_id == spans[0]["id"] assert trace_parent.trace_options.recorded if elasticapm_client.config.use_elastic_traceparent_header: assert constants.TRACEPARENT_LEGACY_HEADER_NAME in headers assert headers[constants.TRACEPARENT_HEADER_NAME] == headers[ constants.TRACEPARENT_LEGACY_HEADER_NAME] else: assert constants.TRACEPARENT_LEGACY_HEADER_NAME not in headers
async def test_trace_parent_propagation_sampled(instrument, event_loop, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" elasticapm_client.begin_transaction("transaction") async with aiohttp.ClientSession() as session: async with session.get(waiting_httpserver.url) as resp: status = resp.status text = await resp.text() elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[constants.TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) headers = waiting_httpserver.requests[0].headers assert constants.TRACEPARENT_HEADER_NAME in headers trace_parent = TraceParent.from_string( headers[constants.TRACEPARENT_HEADER_NAME]) assert trace_parent.trace_id == transactions[0]["trace_id"] assert trace_parent.span_id == spans[0]["id"] assert trace_parent.trace_options.recorded if elasticapm_client.config.use_elastic_traceparent_header: assert constants.TRACEPARENT_LEGACY_HEADER_NAME in headers assert headers[constants.TRACEPARENT_HEADER_NAME] == headers[ constants.TRACEPARENT_LEGACY_HEADER_NAME] else: assert constants.TRACEPARENT_LEGACY_HEADER_NAME not in headers
def test_trace_parent_wrong_version(caplog): header = "xx-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-03" with caplog.at_level("DEBUG", "elasticapm.utils"): trace_parent = TraceParent.from_string(header) record = caplog.records[0] assert trace_parent is None assert record.message == "Invalid version field, value xx"
def test_tracer_inject_map(tracer): span_context = OTSpanContext(trace_parent=TraceParent.from_string( "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01")) carrier = {} tracer.inject(span_context, Format.TEXT_MAP, carrier) assert carrier[ "elastic-apm-traceparent"] == b"00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01"
async def call(self, module, method, wrapped, instance, args, kwargs): # Late import to avoid ImportErrors from elasticapm.contrib.tornado.utils import get_data_from_request, get_data_from_response request = instance.request trace_parent = TraceParent.from_headers(request.headers) client = instance.application.elasticapm_client client.begin_transaction("request", trace_parent=trace_parent) elasticapm.set_context( lambda: get_data_from_request(instance, request, client.config, constants.TRANSACTION), "request") # TODO: Can we somehow incorporate the routing rule itself here? elasticapm.set_transaction_name("{} {}".format( request.method, type(instance).__name__), override=False) ret = await wrapped(*args, **kwargs) elasticapm.set_context( lambda: get_data_from_response(instance, client.config, constants. TRANSACTION), "response") result = "HTTP {}xx".format(instance.get_status() // 100) elasticapm.set_transaction_result(result, override=False) client.end_transaction() return ret
async def test_trace_parent_propagation_sampled_headers_none( instrument, event_loop, elasticapm_client, waiting_httpserver, sampled): """ Test that we don't blow up if headers are explicitly set to None """ waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" transaction = elasticapm_client.begin_transaction("transaction") transaction.is_sampled = sampled async with aiohttp.ClientSession() as session: async with session.get(waiting_httpserver.url, headers=None) as resp: status = resp.status text = await resp.text() elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[constants.TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) headers = waiting_httpserver.requests[0].headers assert constants.TRACEPARENT_HEADER_NAME in headers trace_parent = TraceParent.from_string( headers[constants.TRACEPARENT_HEADER_NAME], tracestate_string=headers[constants.TRACESTATE_HEADER_NAME]) assert trace_parent.trace_id == transactions[0]["trace_id"] if sampled: assert trace_parent.span_id == spans[0]["id"] else: assert trace_parent.tracestate_dict[ constants.TRACESTATE.SAMPLE_RATE] == "0" assert trace_parent.span_id == transactions[0]["id"]
def _request_started_handler(client, sender, *args, **kwargs): if not _should_start_transaction(client): return # try to find trace id if "environ" in kwargs: trace_parent = TraceParent.from_headers( kwargs["environ"], TRACEPARENT_HEADER_NAME_WSGI, TRACEPARENT_LEGACY_HEADER_NAME_WSGI, TRACESTATE_HEADER_NAME_WSGI, ) elif "scope" in kwargs and "headers" in kwargs["scope"]: trace_parent = TraceParent.from_headers(kwargs["scope"]["headers"]) else: trace_parent = None client.begin_transaction("request", trace_parent=trace_parent)
async def _request_started(self, request: Request): """Captures the begin of the request processing to APM. Args: request (Request) """ # When we consume the body, we replace the streaming mechanism with # a mocked version -- this workaround came from # https://github.com/encode/starlette/issues/495#issuecomment-513138055 # and we call the workaround here to make sure that regardless of # `capture_body` settings, we will have access to the body if we need it. if self.client.config.capture_body != "off": await get_body(request) if not self.client.should_ignore_url(request.url.path): trace_parent = TraceParent.from_headers(dict(request.headers)) self.client.begin_transaction("request", trace_parent=trace_parent) await set_context( lambda: get_data_from_request(request, self.client.config, constants.TRANSACTION), "request") transaction_name = self.get_route_name(request) or request.url.path elasticapm.set_transaction_name("{} {}".format( request.method, transaction_name), override=False)
def test_requests_instrumentation(instrument, elasticapm_client, waiting_httpserver): waiting_httpserver.serve_content("") url = waiting_httpserver.url + "/hello_world" parsed_url = compat.urlparse.urlparse(url) elasticapm_client.begin_transaction("transaction.test") with capture_span("test_request", "test"): requests.get(url, allow_redirects=False) elasticapm_client.end_transaction("MyView") transactions = elasticapm_client.events[TRANSACTION] spans = elasticapm_client.spans_for_transaction(transactions[0]) assert spans[0]["name"].startswith("GET 127.0.0.1:") assert spans[0]["type"] == "external" assert spans[0]["subtype"] == "http" assert url == spans[0]["context"]["http"]["url"] assert spans[0]["context"]["destination"]["service"] == { "name": "http://127.0.0.1:%d" % parsed_url.port, "resource": "127.0.0.1:%d" % parsed_url.port, "type": "external", } assert constants.TRACEPARENT_HEADER_NAME in waiting_httpserver.requests[ 0].headers trace_parent = TraceParent.from_string( waiting_httpserver.requests[0].headers[ constants.TRACEPARENT_HEADER_NAME]) assert trace_parent.trace_id == transactions[0]["trace_id"] # this should be the span id of `requests`, not of urllib3 assert trace_parent.span_id == spans[0]["id"] assert trace_parent.trace_options.recorded