system-tests icon indicating copy to clipboard operation
system-tests copied to clipboard

apm_tracing_e2e: Add tracecontext smoke test

Open zacharycmontoya opened this issue 2 years ago • 3 comments
trafficstars

Description

This PR adds a new e2e test that passes in a traceparent header and asserts that its trace context is used by the instrumented application.

Motivation

Implementing tracecontext is a feature that our tracers must support end-to-end, so this provides an assurance that it does.

Reviewer checklist

  • [ ] If this PR modifies anything else than strictly the default scenario, then add the run-all-scenarios label (more info).
  • [ ] CI is green
    • [ ] If not, failing jobs are not related to this change (and you are 100% sure about this statement)

Workflow

  1. ⚠️⚠️ Create your PR as draft
  2. Follow the style guidelines of this project (See how to easily lint the code)
  3. Work on you PR until the CI passes (if something not related to your task is failing, you can ignore it)
  4. Mark it as ready for review

Once your PR is reviewed, you can merge it! :heart:

zacharycmontoya avatar Jun 30 '23 01:06 zacharycmontoya

When testing against the Python library, I was able to get this to work locally. I'm not sure if there's a better, more reliable way to test this. While this PR is high priority, I don't think this is a blocker for GA

zacharycmontoya avatar Jul 28 '23 04:07 zacharycmontoya

I am going to leave here the progress I had, I opted in for a separate endpoint for the better clarity.


import json

from tests.apm_tracing_e2e.test_single_span import _get_spans_submitted, _assert_msg
from utils import weblog, scenarios, interfaces
from utils.parametric.spec.tracecontext import get_traceparent


@scenarios.apm_tracing_e2e_tracecontext
class Test_Tracecontext_Span:
    """This is a smoke test that exercises the W3C context propagation.
    '/make_tracecontext_call' request flow:
     - extracts context from the propagated headers,
     - creates a span with extracted context,
     - injects new span context into a map and returns it as text upon success
    As a summary, successful request produces a span with injected context and
    returns a map with newly generated W3C headers.
    """

    def setup_tracecontext_span(self):
        self.req = weblog.get(
            "/make_tracecontext_call",
            {
                "shouldIndex": 1,
                "name": "inherited_child",
                "tracestate": "dd=s:2;o:system-tests;t.usr.id:baz64~~,othervendor=t61rcWkgMzE",
                "traceparent": "00-00000000000000001111111111111111-2222222222222222-01",
            })

    def test_tracecontext_span(self):
        # Assert the weblog server span was sent by the agent.
        spans = _get_spans_submitted(self.req)
        # we are expecting two spans, one of which is generated by weblog
        assert 2 == len(spans), _assert_msg(2, len(spans), "Agent did not submit the spans we want!")

        # Assert received span has properties from the passed headers
        span = _get_span(spans, "inherited_child")
        # 00000000000000001111111111111111 in integer form
        assert span.get("traceID") == '1229782938247303441'
        # 2222222222222222 in integer form
        assert span.get("parentID") == '2459565876494606882'

        data = json.loads(self.req.text)
        # Assert only W3C headers were injected (no Datadog headers)
        assert "traceparent" in data
        assert "tracestate" in data
        assert "x-datadog-parent-id" not in data
        assert "x-datadog-sampling-priority" not in data
        assert "x-datadog-tags" not in data
        assert "x-datadog-trace-id" not in data

        # Assert injected headers have
        # - the same trace id, as passed through traceparent header before
        # - different parent id, that is equal to the spanId of the received span
        # - trace flags == 01,
        traceparent = get_traceparent(data)
        assert traceparent.trace_id == '00000000000000001111111111111111'
        assert int(traceparent.parent_id, 16) == int(span.get('spanID'))
        assert traceparent.trace_flags == '01'

        # Assert all spans in the distributed trace were received from the backend
        spans = interfaces.backend.assert_request_spans_exist(self.req, query_filter="")
        assert 1 == len(spans), _assert_msg(1, len(spans))


def _get_span(spans, span_name):
    for s in spans:
        if s["name"] == span_name:
            return s
    return {}

dianashevchenko avatar Jul 28 '23 12:07 dianashevchenko

mux.HandleFunc("/make_tracecontext_call", func(w http.ResponseWriter, r *http.Request) {
		tracestate := r.URL.Query().Get("tracestate")
		traceparent := r.URL.Query().Get("traceparent")
		spanName := r.URL.Query().Get("name")

		tags := []ddtracer.StartSpanOption{}
		// We need to propagate the user agent header to retain the mapping between the system-tests/weblog request id
		// and the traces/spans that will be generated below, so that we can reference to them in our tests.
		// See https://github.com/DataDog/system-tests/blob/2d6ae4d5bf87d55855afd36abf36ee710e7d8b3c/utils/interfaces/_core.py#L156
		userAgent := r.UserAgent()
		tags = append(tags, ddtracer.Tag("http.useragent", userAgent))

		if r.URL.Query().Get("shouldIndex") == "1" {
			tags = append(tags,
				ddtracer.Tag("_dd.filter.kept", 1),
				ddtracer.Tag("_dd.filter.id", "system_tests_e2e"),
			)
		}
		ctx, err := ddtracer.Extract(ddtracer.TextMapCarrier{
			"tracestate":  tracestate,
			"traceparent": traceparent,
		})
		if err != nil {
			log.Fatalln(err)
			w.WriteHeader(500)
		}

		span := ddtracer.StartSpan(spanName, append(tags, ddtracer.ChildOf(ctx))...)
		headers := ddtracer.TextMapCarrier{}
		err = ddtracer.Inject(span.Context(), headers)
		if err != nil {
			log.Fatalln(err)
			w.WriteHeader(500)
		}
		span.Finish()

		ddtracer.Flush()
		b, _ := json.Marshal(headers)
		w.Write(b)
	})

dianashevchenko avatar Jul 28 '23 12:07 dianashevchenko