diff --git a/tests/parametric/test_otel_api_interoperability.py b/tests/parametric/test_otel_api_interoperability.py index f03dc7e90ed..2e1e3f4858f 100644 --- a/tests/parametric/test_otel_api_interoperability.py +++ b/tests/parametric/test_otel_api_interoperability.py @@ -479,7 +479,7 @@ def test_set_attribute_from_datadog(self, test_agent: TestAgentAPI, test_library dd_span.set_metric("int_array", [1, 2, 3]) traces = test_agent.wait_for_num_traces(1, sort_by_start=False) - trace = find_trace(traces, otel_span.span_id) + trace = find_trace(traces, otel_span.trace_id) assert len(trace) == 1 root = find_root_span(trace) diff --git a/tests/parametric/test_otel_span_methods.py b/tests/parametric/test_otel_span_methods.py index 9eeedc58aea..5c448fcac0e 100644 --- a/tests/parametric/test_otel_span_methods.py +++ b/tests/parametric/test_otel_span_methods.py @@ -7,6 +7,7 @@ from utils.docker_fixtures.parametric import Link from utils.docker_fixtures.spec.trace import find_span from utils.docker_fixtures.spec.trace import find_trace +from utils.docker_fixtures.spec.trace import id_to_int from utils.docker_fixtures.spec.trace import retrieve_span_events from utils.docker_fixtures.spec.trace import retrieve_span_links from utils.docker_fixtures.spec.trace import find_first_span_in_trace_payload @@ -333,17 +334,7 @@ def test_otel_get_span_context(self, test_agent: TestAgentAPI, test_library: APM span.end_span() context = span.span_context() assert context.get("trace_id") == parent.span_context().get("trace_id") - if ( - isinstance(span.span_id, str) - and len(span.span_id) == 16 - and all(c in "0123456789abcdef" for c in span.span_id) - ): - # Some languages e.g. PHP return a hexadecimal span id - assert context.get("span_id") == span.span_id - else: - # Some languages e.g. Node.js using express need to return as a string value - # due to 64-bit integers being too large. - assert context.get("span_id") == f"{int(span.span_id):016x}" + assert context.get("span_id") == span.span_id assert context.get("trace_flags") == "01" # compare the values of the span context with the values of the trace sent to the agent @@ -351,7 +342,7 @@ def test_otel_get_span_context(self, test_agent: TestAgentAPI, test_library: APM trace = find_trace(traces, span.trace_id) op2 = find_span(trace, span.span_id) assert op2["resource"] == "op2" - assert op2["span_id"] == int(context["span_id"], 16) + assert id_to_int(op2["span_id"]) == id_to_int(context["span_id"]) first_span = find_first_span_in_trace_payload(trace) op2_tidhex = first_span["meta"].get("_dd.p.tid", "") + "{:016x}".format(first_span["trace_id"]) assert int(op2_tidhex, 16) == int(context["trace_id"], 16) diff --git a/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApiOtel.cs b/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApiOtel.cs index a2f7e89b911..87f9fda9a97 100644 --- a/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApiOtel.cs +++ b/utils/build/docker/dotnet/parametric/Endpoints/ApmTestApiOtel.cs @@ -218,7 +218,7 @@ private static async Task OtelSpanContext(HttpRequest request) var result = JsonConvert.SerializeObject(new { trace_id = activity.TraceId.ToString(), - span_id = activity.SpanId.ToString(), + span_id = ulong.Parse(activity.SpanId.ToString(), NumberStyles.HexNumber), trace_flags = ((int)activity.ActivityTraceFlags).ToString("x2"), trace_state = activity.TraceStateString ?? "", remote = activity.HasRemoteParent diff --git a/utils/build/docker/golang/parametric/helpers.go b/utils/build/docker/golang/parametric/helpers.go index 8501739b229..b4958d24186 100644 --- a/utils/build/docker/golang/parametric/helpers.go +++ b/utils/build/docker/golang/parametric/helpers.go @@ -151,7 +151,7 @@ type OtelSpanContextArgs struct { } type OtelSpanContextReturn struct { - SpanId string `json:"span_id"` + SpanId uint64 `json:"span_id"` TraceId string `json:"trace_id"` TraceFlags string `json:"trace_flags"` TraceState string `json:"trace_state"` diff --git a/utils/build/docker/golang/parametric/otel.go b/utils/build/docker/golang/parametric/otel.go index 38d7c33056d..19771ec9dc0 100644 --- a/utils/build/docker/golang/parametric/otel.go +++ b/utils/build/docker/golang/parametric/otel.go @@ -218,7 +218,7 @@ func (s *apmClientServer) otelSpanContextHandler(w http.ResponseWriter, r *http. sc := sctx.span.SpanContext() w.Header().Set("Content-Type", "application/json") if err := json.NewEncoder(w).Encode(&OtelSpanContextReturn{ - SpanId: sc.SpanID().String(), + SpanId: func() uint64 { id := sc.SpanID(); return binary.BigEndian.Uint64(id[:]) }(), TraceId: sc.TraceID().String(), TraceFlags: sc.TraceFlags().String(), TraceState: sc.TraceState().String(), diff --git a/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/opentelemetry/controller/OpenTelemetryTraceController.java b/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/opentelemetry/controller/OpenTelemetryTraceController.java index 71c53ac5fdb..872fb809273 100644 --- a/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/opentelemetry/controller/OpenTelemetryTraceController.java +++ b/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/opentelemetry/controller/OpenTelemetryTraceController.java @@ -102,7 +102,7 @@ public SpanContextResult getSpanContext(@RequestBody SpanContextArgs args) { } SpanContext spanContext = span.getSpanContext(); return new SpanContextResult( - spanContext.getSpanId(), + Long.parseUnsignedLong(spanContext.getSpanId(), 16), spanContext.getTraceId(), spanContext.getTraceFlags().asHex(), formatTraceState(spanContext.getTraceState()), diff --git a/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/opentelemetry/dto/SpanContextResult.java b/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/opentelemetry/dto/SpanContextResult.java index c5fcec6d164..ac27e0f2d42 100644 --- a/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/opentelemetry/dto/SpanContextResult.java +++ b/utils/build/docker/java/parametric/src/main/java/com/datadoghq/trace/opentelemetry/dto/SpanContextResult.java @@ -3,12 +3,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; public record SpanContextResult( - @JsonProperty("span_id") String spanId, + @JsonProperty("span_id") long spanId, @JsonProperty("trace_id") String traceId, @JsonProperty("trace_flags") String traceFlags, @JsonProperty("trace_state") String traceState, boolean remote) { public static SpanContextResult error() { - return new SpanContextResult("0000000000000000", "00000000000000000000000000000000", "00", "", false); + return new SpanContextResult(0L, "00000000000000000000000000000000", "00", "", false); } } diff --git a/utils/build/docker/nodejs/parametric/server.js b/utils/build/docker/nodejs/parametric/server.js index 90a63c84ec8..ee103fe6e10 100644 --- a/utils/build/docker/nodejs/parametric/server.js +++ b/utils/build/docker/nodejs/parametric/server.js @@ -276,7 +276,7 @@ app.post('/trace/otel/start_span', (req, res) => { startTime: microLongToHrTime(request.timestamp) }, parentContext) const ctx = span._ddSpan.context() - const span_id = ctx._spanId.toString(10) + const span_id = "0x" + ctx._spanId.toString(16) const trace_id = ctx._traceId.toString(10) otelSpans[span_id] = span @@ -321,7 +321,7 @@ app.post('/trace/otel/span_context', (req, res) => { const span = otelSpans[span_id] const ctx = span.spanContext() res.json({ - span_id: ctx.spanId, + span_id: `0x${ctx.spanId}`, trace_id: ctx.traceId, // Node.js official OTel API uses a number, not a string trace_flags: `0${ctx.traceFlags}`, diff --git a/utils/build/docker/php/parametric/server.php b/utils/build/docker/php/parametric/server.php index 5d87f161db5..50c4973ccf8 100644 --- a/utils/build/docker/php/parametric/server.php +++ b/utils/build/docker/php/parametric/server.php @@ -52,8 +52,11 @@ function arg($req, $arg) { } // Source: https://magp.ie/2015/09/30/convert-large-integer-to-hexadecimal-without-php-math-extension/ -function largeBaseConvert($numString, $fromBase, $toBase) +function convertBase16ToBase10($numString) { + // convert a base 16 string to a base 10 string + $fromBase = 16; + $toBase = 10; $chars = "0123456789abcdefghijklmnopqrstuvwxyz"; $toString = substr($chars, 0, $toBase); @@ -336,8 +339,8 @@ function remappedSpanKind($spanKind) { /** @var SDK\Span $span */ $span = $spanBuilder->startSpan(); - $spanId = largeBaseConvert($span->getContext()->getSpanId(), 16, 10); - $traceId = largeBaseConvert($span->getContext()->getTraceId(), 16, 10); + $spanId = "0x{$span->getContext()->getSpanId()}"; + $traceId = convertBase16ToBase10($span->getContext()->getTraceId()); $scopes[$spanId] = $span->activate(); $otelSpans[$spanId] = $span; $spans[$spanId] = $span->getDDSpan(); @@ -443,7 +446,7 @@ function remappedSpanKind($spanKind) { return jsonResponse([ 'trace_id' => $spanContext->getTraceId(), - 'span_id' => $spanContext->getSpanId(), + 'span_id' => "0x{$spanContext->getSpanId()}", 'trace_flags' => $spanContext->getTraceFlags() ? '01' : '00', 'trace_state' => (string) $spanContext->getTraceState(), // Implements __toString() 'remote' => $spanContext->isRemote() @@ -457,8 +460,8 @@ function remappedSpanKind($spanKind) { $span = Span::getCurrent(); $otelSpanId = $span->getContext()->getSpanId(); $otelTraceId = $span->getContext()->getTraceId(); - $spanId = largeBaseConvert($otelSpanId, 16, 10); - $traceId = largeBaseConvert($otelTraceId, 16, 10); + $spanId = "0x$otelSpanId"; + $traceId = convertBase16ToBase10($otelTraceId); if ($otelSpanId !== \OpenTelemetry\API\Trace\SpanContextValidator::INVALID_SPAN && $otelTraceId !== \OpenTelemetry\API\Trace\SpanContextValidator::INVALID_TRACE) { $otelSpans[$spanId] = $span; diff --git a/utils/build/docker/python/parametric/apm_test_client/server.py b/utils/build/docker/python/parametric/apm_test_client/server.py index dd695c23693..bf950b4a055 100644 --- a/utils/build/docker/python/parametric/apm_test_client/server.py +++ b/utils/build/docker/python/parametric/apm_test_client/server.py @@ -713,7 +713,7 @@ class OtelSpanContextArgs(BaseModel): class OtelSpanContextReturn(BaseModel): - span_id: str + span_id: int trace_id: str trace_flags: str trace_state: str @@ -730,7 +730,7 @@ def otel_span_context(args: OtelSpanContextArgs): # as integers and are converted to hex when the trace is submitted to the collector. # https://github.com/open-telemetry/opentelemetry-python/blob/v1.17.0/opentelemetry-api/src/opentelemetry/trace/span.py#L424-L425 return OtelSpanContextReturn( - span_id="{:016x}".format(ctx.span_id), + span_id=ctx.span_id, trace_id="{:032x}".format(ctx.trace_id), trace_flags="{:02x}".format(ctx.trace_flags), trace_state=ctx.trace_state.to_header(), diff --git a/utils/build/docker/ruby/parametric/server.rb b/utils/build/docker/ruby/parametric/server.rb index b560c14a891..a1e9bc5a288 100644 --- a/utils/build/docker/ruby/parametric/server.rb +++ b/utils/build/docker/ruby/parametric/server.rb @@ -1208,7 +1208,7 @@ def handle_trace_otel_span_context(req, res) ctx = span.context res.write(OtelSpanContextReturn.new( - format('%016x', ctx.hex_span_id.to_i(16)), + ctx.hex_span_id.to_i(16), format('%032x', ctx.hex_trace_id.to_i(16)), ctx.trace_flags.sampled? ? '01' : '00', ctx.tracestate.to_s, diff --git a/utils/build/docker/rust/parametric/src/opentelemetry/dto.rs b/utils/build/docker/rust/parametric/src/opentelemetry/dto.rs index 0034b1d47ce..6a10a79a498 100644 --- a/utils/build/docker/rust/parametric/src/opentelemetry/dto.rs +++ b/utils/build/docker/rust/parametric/src/opentelemetry/dto.rs @@ -118,7 +118,7 @@ pub struct SpanContextArgs { // --- SpanContextResult --- #[derive(Debug, Serialize, Deserialize)] pub struct SpanContextResult { - pub span_id: String, + pub span_id: u64, pub trace_id: String, pub trace_flags: Option, pub trace_state: Option, diff --git a/utils/build/docker/rust/parametric/src/opentelemetry/mod.rs b/utils/build/docker/rust/parametric/src/opentelemetry/mod.rs index 3329c298f88..7d6c0384311 100644 --- a/utils/build/docker/rust/parametric/src/opentelemetry/mod.rs +++ b/utils/build/docker/rust/parametric/src/opentelemetry/mod.rs @@ -189,7 +189,7 @@ async fn get_span_context( span_context.span_id().to_string() ); SpanContextResult { - span_id: span_context.span_id().to_string(), + span_id: u64::from_be_bytes(span_context.span_id().to_bytes()), trace_id: span_context.trace_id().to_string(), trace_flags: Some(if span_context.trace_flags().to_u8() == 1 { "01".to_string() @@ -203,7 +203,7 @@ async fn get_span_context( let span_id = args.span_id; debug!("get_span_context span NOT found: {span_id}"); SpanContextResult { - span_id: "0000000000000000".to_string(), + span_id: 0, trace_id: "00000000000000000000000000000000".to_string(), trace_flags: Some("00".to_string()), trace_state: Some("".to_string()), diff --git a/utils/docker_fixtures/_test_clients/_test_client_parametric.py b/utils/docker_fixtures/_test_clients/_test_client_parametric.py index 5f165e22a4d..a1aed4b980f 100644 --- a/utils/docker_fixtures/_test_clients/_test_client_parametric.py +++ b/utils/docker_fixtures/_test_clients/_test_client_parametric.py @@ -182,10 +182,13 @@ def finish(self): class _TestOtelSpan: - def __init__(self, client: "ParametricTestClientApi", span_id: int, trace_id: int): + def __init__(self, client: "ParametricTestClientApi", span_id: int | str, trace_id: int): self._client = client - self.span_id = span_id self.trace_id = trace_id + self.span_id = span_id + + if isinstance(span_id, str): + assert span_id.startswith("0x") # API methods @@ -521,7 +524,7 @@ def write_log( level: LogLevel, message: str, *, - span_id: int | None = None, + span_id: str | int | None = None, ) -> bool: """Generate a log message with the specified parameters. @@ -604,7 +607,7 @@ def otel_start_span( name: str, timestamp: int | None = None, span_kind: SpanKind | None = None, - parent_id: int | None = None, + parent_id: str | int | None = None, links: list[Link] | None = None, events: list[Event] | None = None, attributes: dict | None = None, @@ -630,12 +633,12 @@ def _otel_trace_start_span( name: str, timestamp: int | None, span_kind: SpanKind | None, - parent_id: int | None, + parent_id: str | int | None, links: list[Link] | None, events: list[Event] | None, attributes: dict | None, ) -> StartSpanResponse: - resp = self._session.post( + response = self._session.post( self._url("/trace/otel/start_span"), json={ "name": name, @@ -646,33 +649,36 @@ def _otel_trace_start_span( "events": events or [], "attributes": attributes or {}, }, - ).json() + ) + response.raise_for_status() + + data = response.json() # TODO: Some http endpoints return span_id and trace_id as strings (ex: dotnet), some as uint64 (ex: go) # and others with bignum trace_ids and uint64 span_ids (ex: python). We should standardize this. - return StartSpanResponse(span_id=resp["span_id"], trace_id=resp["trace_id"]) + return StartSpanResponse(span_id=data["span_id"], trace_id=data["trace_id"]) - def otel_end_span(self, span_id: int, timestamp: int | None) -> None: + def otel_end_span(self, span_id: str | int, timestamp: int | None) -> None: self._session.post( self._url("/trace/otel/end_span"), json={"id": span_id, "timestamp": timestamp}, ) - def otel_set_attributes(self, span_id: int, attributes: dict) -> None: + def otel_set_attributes(self, span_id: str | int, attributes: dict) -> None: self._session.post( self._url("/trace/otel/set_attributes"), json={"span_id": span_id, "attributes": attributes}, ) - def otel_set_name(self, span_id: int, name: str) -> None: + def otel_set_name(self, span_id: str | int, name: str) -> None: self._session.post(self._url("/trace/otel/set_name"), json={"span_id": span_id, "name": name}) - def otel_set_status(self, span_id: int, code: StatusCode, description: str) -> None: + def otel_set_status(self, span_id: str | int, code: StatusCode, description: str) -> None: self._session.post( self._url("/trace/otel/set_status"), json={"span_id": span_id, "code": code.name, "description": description}, ) - def otel_add_event(self, span_id: int, name: str, timestamp: int | None, attributes: dict | None) -> None: + def otel_add_event(self, span_id: str | int, name: str, timestamp: int | None, attributes: dict | None) -> None: self._session.post( self._url("/trace/otel/add_event"), json={ @@ -683,31 +689,35 @@ def otel_add_event(self, span_id: int, name: str, timestamp: int | None, attribu }, ) - def otel_record_exception(self, span_id: int, message: str, attributes: dict | None) -> None: + def otel_record_exception(self, span_id: str | int, message: str, attributes: dict | None) -> None: self._session.post( self._url("/trace/otel/record_exception"), json={"span_id": span_id, "message": message, "attributes": attributes}, ) - def otel_is_recording(self, span_id: int) -> bool: + def otel_is_recording(self, span_id: str | int) -> bool: resp = self._session.post(self._url("/trace/otel/is_recording"), json={"span_id": span_id}).json() return resp["is_recording"] - def otel_get_span_context(self, span_id: int) -> OtelSpanContext: - resp = self._session.post(self._url("/trace/otel/span_context"), json={"span_id": span_id}).json() + def otel_get_span_context(self, span_id: str | int) -> OtelSpanContext: + response = self._session.post(self._url("/trace/otel/span_context"), json={"span_id": span_id}) + + response.raise_for_status() + + data = response.json() return OtelSpanContext( - trace_id=resp["trace_id"], - span_id=resp["span_id"], - trace_flags=resp["trace_flags"], - trace_state=resp["trace_state"], - remote=resp["remote"], + trace_id=data["trace_id"], + span_id=data["span_id"], + trace_flags=data["trace_flags"], + trace_state=data["trace_state"], + remote=data["remote"], ) def otel_flush(self, timeout_sec: int) -> bool: resp = self._session.post(self._url("/trace/otel/flush"), json={"seconds": timeout_sec}).json() return resp["success"] - def otel_set_baggage(self, span_id: int, key: str, value: str): + def otel_set_baggage(self, span_id: str | int, key: str, value: str): resp = self._session.post( self._url("/trace/otel/otel_set_baggage"), json={"span_id": span_id, "key": key, "value": value}, diff --git a/utils/docker_fixtures/parametric.py b/utils/docker_fixtures/parametric.py index 760e33e2dfc..8c3ed933cf6 100644 --- a/utils/docker_fixtures/parametric.py +++ b/utils/docker_fixtures/parametric.py @@ -10,5 +10,5 @@ class LogLevel(Enum): class Link(TypedDict): - parent_id: int + parent_id: str | int attributes: NotRequired[dict] diff --git a/utils/docker_fixtures/spec/trace.py b/utils/docker_fixtures/spec/trace.py index 1347d8a46bc..f3ebbc077b5 100644 --- a/utils/docker_fixtures/spec/trace.py +++ b/utils/docker_fixtures/spec/trace.py @@ -165,7 +165,7 @@ def find_trace(traces: list[Trace], trace_id: int) -> Trace: raise AssertionError(f"Trace with 64bit trace_id={trace_id} not found. Traces={traces}") -def find_span(trace: Trace, span_id: int) -> Span: +def find_span(trace: Trace, span_id: str | int) -> Span: """Return a span from the trace matches a `span_id`.""" assert len(trace) > 0 # TODO: Ensure all parametric applications return uint64 span ids (not strings) @@ -330,6 +330,9 @@ def id_to_int(value: str | int) -> int: if isinstance(value, int): return value + if value.startswith("0x"): + return int(value, 16) + try: # This is a best effort to convert hex span/trace id to an integer. # This is temporary solution until all parametric applications return trace/span ids