diff --git a/tests/parametric/test_otel_span_methods.py b/tests/parametric/test_otel_span_methods.py index b2952c4440..0e2ee2aa1f 100644 --- a/tests/parametric/test_otel_span_methods.py +++ b/tests/parametric/test_otel_span_methods.py @@ -838,7 +838,7 @@ def test_otel_span_extended_reserved_attributes_overrides_analytics_event( @missing_feature(context.library == "php", reason="Not implemented") @missing_feature(context.library == "java", reason="Not implemented") @missing_feature(context.library == "ruby", reason="Not implemented") - @missing_feature(context.library == "nodejs", reason="Not implemented") + @missing_feature(context.library < "nodejs@5.17.0", reason="Implemented in v5.17.0 & v4.41.0") @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "python@2.9.0", reason="Not implemented") def test_otel_add_event_meta_serialization(self, test_agent, test_library): @@ -874,7 +874,7 @@ def test_otel_add_event_meta_serialization(self, test_agent, test_library): event2 = events[1] assert event2.get("name") == "second_event" - assert event2.get("time_unix_nano") == event2_timestamp_ns + assert event2.get("time_unix_nano") // 10000 == event2_timestamp_ns // 10000 # reduce the precision tested assert event2["attributes"].get("string_val") == "value" event3 = events[2] @@ -891,7 +891,7 @@ def test_otel_add_event_meta_serialization(self, test_agent, test_library): @missing_feature(context.library == "php", reason="Not implemented") @missing_feature(context.library == "java", reason="Not implemented") @missing_feature(context.library == "ruby", reason="Not implemented") - @missing_feature(context.library == "nodejs", reason="Not implemented") + @missing_feature(context.library < "nodejs@5.17.0", reason="Implemented in v5.17.0 & v4.41.0") @missing_feature(context.library < "python@2.9.0", reason="Not implemented") def test_otel_record_exception_does_not_set_error(self, test_agent, test_library): """ @@ -910,7 +910,7 @@ def test_otel_record_exception_does_not_set_error(self, test_agent, test_library @missing_feature(context.library == "php", reason="Not implemented") @missing_feature(context.library == "java", reason="Not implemented") @missing_feature(context.library == "ruby", reason="Not implemented") - @missing_feature(context.library == "nodejs", reason="Not implemented") + @missing_feature(context.library < "nodejs@5.17.0", reason="Implemented in v5.17.0 & v4.41.0") @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "python@2.9.0", reason="Not implemented") def test_otel_record_exception_meta_serialization(self, test_agent, test_library): @@ -933,29 +933,64 @@ def test_otel_record_exception_meta_serialization(self, test_agent, test_library events = json.loads(root_span.get("meta", {}).get("events")) assert len(events) == 3 + event1 = events[0] + assert ( + event1.get("name").lower() == "exception" or "error" + ) # node uses error objects instead of exception objects + assert event1.get("time_unix_nano") > 0 + + event2 = events[1] + assert event2.get("name") == "non_exception_event" + assert event2.get("time_unix_nano") > event1.get("time_unix_nano") + + event3 = events[2] + assert event3.get("name") == "exception" or "error" + assert event3.get("time_unix_nano") > event2.get("time_unix_nano") + + assert root_span["error"] == 1 + assert "error.type" in root_span["meta"] + assert "error.stack" in root_span["meta"] + + @missing_feature(context.library == "golang", reason="Not implemented") + @missing_feature(context.library == "php", reason="Not implemented") + @missing_feature(context.library == "java", reason="Not implemented") + @missing_feature(context.library == "ruby", reason="Not implemented") + @missing_feature(context.library == "nodejs", reason="Otel Node.js API does not support attributes") + @missing_feature(context.library == "dotnet", reason="Not implemented") + @missing_feature(context.library < "python@2.9.0", reason="Not implemented") + def test_otel_record_exception_attributes_serialization(self, test_agent, test_library): + """ + Tests the Span.RecordException API (requires Span.AddEvent API support) + and its serialization into the Datadog error tags and the 'events' tag + """ + with test_library: + with test_library.otel_start_span("operation") as span: + span.set_status(OTEL_ERROR_CODE, "error_desc") + span.record_exception( + message="woof1", attributes={"string_val": "value", "exception.stacktrace": "stacktrace1"} + ) + span.add_event(name="non_exception_event", attributes={"exception.stacktrace": "non-error"}) + span.record_exception(message="woof3", attributes={"exception.message": "message override"}) + span.end_span() + root_span = get_span(test_agent) + assert "events" in root_span["meta"] + + events = json.loads(root_span.get("meta", {}).get("events")) + assert len(events) == 3 event1 = events[0] - assert event1.get("name") == "exception" assert event1["attributes"].get("string_val") == "value" assert event1["attributes"].get("exception.message") == "woof1" assert event1["attributes"].get("exception.stacktrace") == "stacktrace1" - assert event1.get("time_unix_nano") > 0 event2 = events[1] - assert event2.get("name") == "non_exception_event" assert event2["attributes"].get("exception.stacktrace") == "non-error" - assert event2.get("time_unix_nano") > event1.get("time_unix_nano") event3 = events[2] - assert event3.get("name") == "exception" assert event3["attributes"].get("exception.message") == "message override" - assert event3.get("time_unix_nano") > event2.get("time_unix_nano") - assert root_span["error"] == 1 error_message = root_span["meta"].get("error.message") or root_span["meta"].get("error.msg") assert error_message == "message override" - assert "error.type" in root_span["meta"] - assert "error.stack" in root_span["meta"] def run_operation_name_test(expected_operation_name: str, span_kind: int, attributes: dict, test_library, test_agent): diff --git a/utils/build/docker/nodejs/parametric/server.js b/utils/build/docker/nodejs/parametric/server.js index 90731b192e..9d2db26d68 100644 --- a/utils/build/docker/nodejs/parametric/server.js +++ b/utils/build/docker/nodejs/parametric/server.js @@ -9,6 +9,7 @@ const SpanContext = require('dd-trace/packages/dd-trace/src/opentracing/span_con const OtelSpanContext = require('dd-trace/packages/dd-trace/src/opentelemetry/span_context') const { trace, ROOT_CONTEXT } = require('@opentelemetry/api') +const { millisToHrTime } = require('@opentelemetry/core') const { TracerProvider } = tracer const tracerProvider = new TracerProvider() @@ -306,6 +307,21 @@ app.get('/trace/config', (req, res) => { }); }); +app.post("/trace/otel/add_event", (req, res) => { + const { span_id, name, timestamp, attributes } = req.body; + const span = otelSpans[span_id] + // convert to TimeInput object using millisToHrTime + span.addEvent(name, attributes, millisToHrTime(timestamp / 1000)) + res.json({}) +}) + +app.post("/trace/otel/record_exception", (req, res) => { + const { span_id, message, attributes } = req.body; + const span = otelSpans[span_id] + span.recordException(new Error(message)) + res.json({}) +}) + // TODO: implement this endpoint correctly, current blockers: // 1. Fails on invalid url // 2. does not generate span, because http instrumentation turned off