From cfbe667c64bd61bf36e1158069d3e5de70af8c4e Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Wed, 22 May 2024 12:37:56 -0400 Subject: [PATCH] add span links system tests for nodejs (#2065) * add span links system tests for nodejs --------- Co-authored-by: Munir Abdinur --- manifests/nodejs.yml | 3 +- tests/parametric/test_otel_span_methods.py | 8 +-- tests/parametric/test_span_links.py | 9 +++- .../build/docker/nodejs/parametric/server.js | 53 ++++++++++++++++--- 4 files changed, 60 insertions(+), 13 deletions(-) diff --git a/manifests/nodejs.yml b/manifests/nodejs.yml index edf652de17..eb0ffe0346 100644 --- a/manifests/nodejs.yml +++ b/manifests/nodejs.yml @@ -412,7 +412,8 @@ tests/: TestDynamicConfigV2: v4.23.0 test_otel_api_interoperability.py: missing_feature test_otel_sdk_interoperability.py: missing_feature - test_span_links.py: missing_feature + test_span_links.py: + Test_Span_Links: v5.3.0 test_telemetry.py: Test_Defaults: v5.6.0 Test_Environment: v5.6.0 diff --git a/tests/parametric/test_otel_span_methods.py b/tests/parametric/test_otel_span_methods.py index 905274c914..59a0b8c3fd 100644 --- a/tests/parametric/test_otel_span_methods.py +++ b/tests/parametric/test_otel_span_methods.py @@ -405,7 +405,7 @@ def test_otel_set_attributes_separately(self, test_agent, test_library): @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "java@1.26.0", reason="Implemented in 1.26.0") @missing_feature(context.library == "golang", reason="Not implemented") - @missing_feature(context.library == "nodejs", reason="Not implemented") + @missing_feature(context.library < "nodejs@5.3.0", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0") @missing_feature(context.library == "ruby", reason="Not implemented") @missing_feature(context.library == "php", reason="Not implemented") def test_otel_span_started_with_link_from_another_span(self, test_agent, test_library): @@ -448,7 +448,7 @@ def test_otel_span_started_with_link_from_another_span(self, test_agent, test_li @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "java@1.26.0", reason="Implemented in 1.26.0") @missing_feature(context.library == "golang", reason="Not implemented") - @missing_feature(context.library == "nodejs", reason="Not implemented") + @missing_feature(context.library < "nodejs@5.3.0", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0") @missing_feature(context.library < "ruby@2.0.0", reason="Not implemented") @missing_feature(context.library == "php", reason="Not implemented") def test_otel_span_started_with_link_from_datadog_headers(self, test_agent, test_library): @@ -499,7 +499,7 @@ def test_otel_span_started_with_link_from_datadog_headers(self, test_agent, test @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "java@1.28.0", reason="Implemented in 1.28.0") @missing_feature(context.library == "golang", reason="Not implemented") - @missing_feature(context.library == "nodejs", reason="Not implemented") + @missing_feature(context.library < "nodejs@5.3.0", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0") @missing_feature(context.library < "ruby@2.0.0", reason="Not implemented") @missing_feature(context.library == "php", reason="Not implemented") def test_otel_span_started_with_link_from_w3c_headers(self, test_agent, test_library): @@ -547,7 +547,7 @@ def test_otel_span_started_with_link_from_w3c_headers(self, test_agent, test_lib @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "java@1.26.0", reason="Implemented in 1.26.0") @missing_feature(context.library == "golang", reason="Not implemented") - @missing_feature(context.library == "nodejs", reason="Not implemented") + @missing_feature(context.library < "nodejs@5.3.0", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0") @missing_feature(context.library < "ruby@2.0.0", reason="Not implemented") @missing_feature(context.library == "php", reason="Not implemented") def test_otel_span_started_with_link_from_other_spans(self, test_agent, test_library): diff --git a/tests/parametric/test_span_links.py b/tests/parametric/test_span_links.py index 51ca13d3e9..ffdeb0b329 100644 --- a/tests/parametric/test_span_links.py +++ b/tests/parametric/test_span_links.py @@ -14,6 +14,7 @@ @scenarios.parametric class Test_Span_Links: @pytest.mark.parametrize("library_env", [{"DD_TRACE_API_VERSION": "v0.4"}]) + @missing_feature(library="nodejs", reason="only supports span links encoding through _dd.span_links tag") def test_span_started_with_link_v04(self, test_agent, test_library): """Test adding a span link created from another span and serialized in the expected v0.4 format. This tests the functionality of "create a direct link between two spans @@ -83,6 +84,9 @@ def test_span_started_with_link_v05(self, test_agent, test_library): assert link["attributes"].get("array.1") == "b" assert link["attributes"].get("array.2") == "c" + @missing_feature( + library="nodejs", reason="does not currently support creating a link from distributed datadog headers" + ) def test_span_link_from_distributed_datadog_headers(self, test_agent, test_library): """Properly inject datadog distributed tracing information into span links when trace_api is v0.4. Testing the conversion of x-datadog-* headers to tracestate for @@ -208,8 +212,9 @@ def test_span_with_attached_links(self, test_agent, test_library): assert link["attributes"].get("nested.0") == "1" assert link["attributes"].get("nested.1") == "2" - @missing_feature(library="python", reason="links do not influence the sampling decsion of spans") - @missing_feature(library="ruby", reason="links do not influence the sampling decsion of spans") + @missing_feature(library="python", reason="links do not influence the sampling decision of spans") + @missing_feature(library="nodejs", reason="links do not influence the sampling decision of spans") + @missing_feature(library="ruby", reason="links do not influence the sampling decision of spans") def test_span_link_propagated_sampling_decisions(self, test_agent, test_library): """Sampling decisions made by an upstream span should be propagated via span links to downstream spans. diff --git a/utils/build/docker/nodejs/parametric/server.js b/utils/build/docker/nodejs/parametric/server.js index 5b4b39b1c2..e840c3370d 100644 --- a/utils/build/docker/nodejs/parametric/server.js +++ b/utils/build/docker/nodejs/parametric/server.js @@ -75,21 +75,49 @@ app.post('/trace/span/start', (req, res) => { for (const [key, value] of http_headers) { convertedHeaders[key.toLowerCase()] = value } + const extracted = tracer.extract('http_headers', convertedHeaders) if (extracted !== null) parent = extracted const span = tracer.startSpan(request.name, { - type: request.type, - resource: request.resource, - childOf: parent, - tags: { - service: request.service - } + type: request.type, + resource: request.resource, + childOf: parent, + tags: { + service: request.service + } }) + + for (const link of request.links || []) { + const linkParentId = link.parent_id; + if (linkParentId) { + const linkParentSpan = spans[linkParentId]; + span.addLink(linkParentSpan.context(), link.attributes); + } else { + const linkHeaders = link.http_headers || {}; + const convertedLinkHeaders = {} + for (const [key, value] of linkHeaders) { + convertedLinkHeaders[key.toLowerCase()] = value + } + const linkExtracted = tracer.extract('http_headers', convertedLinkHeaders); + if (linkExtracted) { + span.addLink(linkExtracted, link.attributes); + } + } + } + spans[span.context().toSpanId()] = span res.json({ span_id: span.context().toSpanId(), trace_id:span.context().toTraceId(), service:request.service, resource:request.resource,}); }); +app.post('/trace/span/add_link', (req, res) => { + const request = req.body; + const span = spans[request.span_id] + const linked_span = spans[request.parent_id] + span.addLink(linked_span.context(), request.attributes) + res.json({}); +}); + app.post('/trace/span/finish', (req, res) => { const id = req.body.span_id const span = spans[id] @@ -147,10 +175,23 @@ app.post('/trace/otel/start_span', (req, res) => { const makeSpan = (parentContext) => { + const links = (request.links || []).map(link => { + let spanContext; + if (link.parent_id && link.parent_id !== 0) { + spanContext = otelSpans[link.parent_id].spanContext(); + } else { + const linkHeaders = Object.fromEntries(link.http_headers.map(([k, v]) => [k.toLowerCase(), v])); + const extractedContext = tracer.extract('http_headers', linkHeaders) + spanContext = new OtelSpanContext(extractedContext) + } + return {context: spanContext, attributes: link.attributes} + }); + const span = otelTracer.startSpan(request.name, { type: request.type, kind: request.kind, attributes: request.attributes, + links, startTime: nanoLongToHrTime(request.timestamp) }, parentContext) const ctx = span._ddSpan.context()