From 0a24ac3c5265bf4dcbc73c0347ca16304d575c2b Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 30 Mar 2026 12:22:52 +0200 Subject: [PATCH 01/13] Explicit hexadecimal span_id --- .../test_otel_api_interoperability.py | 2 +- tests/parametric/test_otel_span_methods.py | 15 +---- .../parametric/Endpoints/ApmTestApiOtel.cs | 2 +- .../build/docker/nodejs/parametric/server.js | 4 +- .../_test_clients/_test_client_parametric.py | 56 +++++++++++-------- utils/docker_fixtures/parametric.py | 2 +- utils/docker_fixtures/spec/trace.py | 5 +- 7 files changed, 45 insertions(+), 41 deletions(-) diff --git a/tests/parametric/test_otel_api_interoperability.py b/tests/parametric/test_otel_api_interoperability.py index f03dc7e90ed..b448c16642c 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) # why is this messing span_id and 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/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/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 From 3c4386776fd953ac273482734a26d6454e25df7c Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 30 Mar 2026 17:51:47 +0200 Subject: [PATCH 02/13] Fix go --- utils/build/docker/golang/parametric/helpers.go | 2 +- utils/build/docker/golang/parametric/otel.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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(), From d421740f97ccd91280ff964539d8691e7b2ada58 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 30 Mar 2026 18:04:20 +0200 Subject: [PATCH 03/13] fix java --- .../controller/OpenTelemetryTraceController.java | 2 +- .../datadoghq/trace/opentelemetry/dto/SpanContextResult.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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); } } From 0270ed31e9f9cf30f5f02e8c3f7023b97108d5a2 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 30 Mar 2026 18:09:11 +0200 Subject: [PATCH 04/13] fix python --- .../build/docker/python/parametric/apm_test_client/server.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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(), From 130e66d7b0b017f2c25cd63533cb058c7390b73d Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 30 Mar 2026 18:13:21 +0200 Subject: [PATCH 05/13] Fix ruby --- utils/build/docker/ruby/parametric/server.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From 7a2d1e4fa5eeb62c8e55a50b1f0a9ff93d9ff343 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 30 Mar 2026 18:25:53 +0200 Subject: [PATCH 06/13] Fix rust --- utils/build/docker/rust/parametric/src/opentelemetry/dto.rs | 2 +- utils/build/docker/rust/parametric/src/opentelemetry/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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()), From 213a48fb9b5018b35fd981f19d8e6ae2f4d74d2c Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 30 Mar 2026 18:40:38 +0200 Subject: [PATCH 07/13] Update tests/parametric/test_otel_api_interoperability.py Co-authored-by: Munir Abdinur --- tests/parametric/test_otel_api_interoperability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parametric/test_otel_api_interoperability.py b/tests/parametric/test_otel_api_interoperability.py index b448c16642c..e48dbb4bcbf 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.trace_id) # why is this messing span_id and trace_id ?? + trace = find_trace(traces, otel_span.trace_id) assert len(trace) == 1 root = find_root_span(trace) From 4c1592570e09549e5a89db92173d281274458bc4 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 30 Mar 2026 19:03:39 +0200 Subject: [PATCH 08/13] fix php --- utils/build/docker/php/parametric/server.php | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) 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; From 224bc53bc8e59d43b2a9788789133b067f368c0d Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Mon, 30 Mar 2026 19:14:16 +0200 Subject: [PATCH 09/13] lint --- tests/parametric/test_otel_api_interoperability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parametric/test_otel_api_interoperability.py b/tests/parametric/test_otel_api_interoperability.py index e48dbb4bcbf..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.trace_id) + trace = find_trace(traces, otel_span.trace_id) assert len(trace) == 1 root = find_root_span(trace) From e520f1a07f4319c2bbd035322716a23b8b11cff2 Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Tue, 31 Mar 2026 09:32:27 +0200 Subject: [PATCH 10/13] fix php --- utils/build/docker/php/parametric/server.php | 6 ++-- .../_test_clients/_test_client_parametric.py | 36 ++++++++++++++++--- utils/docker_fixtures/spec/trace.py | 8 +---- 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/utils/build/docker/php/parametric/server.php b/utils/build/docker/php/parametric/server.php index 50c4973ccf8..ed38a766aaa 100644 --- a/utils/build/docker/php/parametric/server.php +++ b/utils/build/docker/php/parametric/server.php @@ -339,7 +339,7 @@ function remappedSpanKind($spanKind) { /** @var SDK\Span $span */ $span = $spanBuilder->startSpan(); - $spanId = "0x{$span->getContext()->getSpanId()}"; + $spanId = convertBase16ToBase10($span->getContext()->getSpanId()); $traceId = convertBase16ToBase10($span->getContext()->getTraceId()); $scopes[$spanId] = $span->activate(); $otelSpans[$spanId] = $span; @@ -446,7 +446,7 @@ function remappedSpanKind($spanKind) { return jsonResponse([ 'trace_id' => $spanContext->getTraceId(), - 'span_id' => "0x{$spanContext->getSpanId()}", + 'span_id' => convertBase16ToBase10($spanContext->getSpanId()), 'trace_flags' => $spanContext->getTraceFlags() ? '01' : '00', 'trace_state' => (string) $spanContext->getTraceState(), // Implements __toString() 'remote' => $spanContext->isRemote() @@ -460,7 +460,7 @@ function remappedSpanKind($spanKind) { $span = Span::getCurrent(); $otelSpanId = $span->getContext()->getSpanId(); $otelTraceId = $span->getContext()->getTraceId(); - $spanId = "0x$otelSpanId"; + $spanId = convertBase16ToBase10($otelSpanId); $traceId = convertBase16ToBase10($otelTraceId); if ($otelSpanId !== \OpenTelemetry\API\Trace\SpanContextValidator::INVALID_SPAN && $otelTraceId !== \OpenTelemetry\API\Trace\SpanContextValidator::INVALID_TRACE) { diff --git a/utils/docker_fixtures/_test_clients/_test_client_parametric.py b/utils/docker_fixtures/_test_clients/_test_client_parametric.py index a1aed4b980f..4b8da7225d1 100644 --- a/utils/docker_fixtures/_test_clients/_test_client_parametric.py +++ b/utils/docker_fixtures/_test_clients/_test_client_parametric.py @@ -133,10 +133,26 @@ class Event(TypedDict): class _TestSpan: - 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 + """ + span id can be: + * an integer + * a string starting with a 0x -> an hexadecimal integer + * a string not starting with a 0x -> an decimal integer + """ + + if isinstance(span_id, str): + # check that if a string is sent, then either : + # it startss with 0x and it's hexadicmal + # or it's an decimal integer + if span_id.startswith("0x"): + assert all(c in "0123456789abcdefABCDEF" for c in span_id[2:]), f"{span_id} is not hexadecimal" + else: + assert span_id.isdigit(), f"{span_id} is not decimal" + def set_resource(self, resource: str): self._client.span_set_resource(self.span_id, resource) @@ -186,11 +202,21 @@ def __init__(self, client: "ParametricTestClientApi", span_id: int | str, trace_ self._client = client self.trace_id = trace_id self.span_id = span_id + """ + span id can be: + * an integer + * a string starting with a 0x -> an hexadecimal integer + * a string not starting with a 0x -> an decimal integer + """ if isinstance(span_id, str): - assert span_id.startswith("0x") - - # API methods + # check that if a string is sent, then either : + # it startss with 0x and it's hexadicmal + # or it's an decimal integer + if span_id.startswith("0x"): + assert all(c in "0123456789abcdefABCDEF" for c in span_id[2:]), f"{span_id} is not hexadecimal" + else: + assert span_id.isdigit(), f"{span_id} is not decimal" def set_attributes(self, attributes: dict): self._client.otel_set_attributes(self.span_id, attributes) diff --git a/utils/docker_fixtures/spec/trace.py b/utils/docker_fixtures/spec/trace.py index f3ebbc077b5..6de0231d80c 100644 --- a/utils/docker_fixtures/spec/trace.py +++ b/utils/docker_fixtures/spec/trace.py @@ -333,13 +333,7 @@ def id_to_int(value: str | int) -> int: 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 - # as stringified integers (ids will be stringified to workaround percision issues in some languages) - return int(value) - except ValueError: - return int(value, 16) + return int(value) def extract_trace_id_from_otel_span(span: dict) -> str: From 7b8ef1faac531dd8cd9cd9faa685e40be6fb397a Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Tue, 31 Mar 2026 09:33:31 +0200 Subject: [PATCH 11/13] fix typings --- tests/parametric/test_trace_sampling.py | 4 +-- .../_test_clients/_test_client_parametric.py | 32 +++++++++---------- utils/docker_fixtures/spec/trace.py | 2 +- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/tests/parametric/test_trace_sampling.py b/tests/parametric/test_trace_sampling.py index dc1d179e26e..26f4f112803 100644 --- a/tests/parametric/test_trace_sampling.py +++ b/tests/parametric/test_trace_sampling.py @@ -450,7 +450,7 @@ def tag_sampling_env(tag_glob_pattern: str) -> dict: @features.adaptive_sampling class Test_Trace_Sampling_Tags_Feb2024_Revision: def assert_matching_span( - self, test_agent: TestAgentAPI, trace_id: int, span_id: int, name: str | None = None, service: str | None = None + self, test_agent: TestAgentAPI, trace_id: int, span_id: int | str, name: str | None = None, service: str | None = None ): matching_span = find_span_in_traces(test_agent.wait_for_num_traces(1), trace_id, span_id) @@ -464,7 +464,7 @@ def assert_matching_span( assert matching_span["service"] == service def assert_mismatching_span( - self, test_agent: TestAgentAPI, trace_id: int, span_id: int, name: str | None = None, service: str | None = None + self, test_agent: TestAgentAPI, trace_id: int, span_id: int | str, name: str | None = None, service: str | None = None ): mismatching_span = find_span_in_traces(test_agent.wait_for_num_traces(1), trace_id, span_id) diff --git a/utils/docker_fixtures/_test_clients/_test_client_parametric.py b/utils/docker_fixtures/_test_clients/_test_client_parametric.py index 4b8da7225d1..7de52153d3c 100644 --- a/utils/docker_fixtures/_test_clients/_test_client_parametric.py +++ b/utils/docker_fixtures/_test_clients/_test_client_parametric.py @@ -181,7 +181,7 @@ def remove_all_baggage(self): def set_error(self, typestr: str = "", message: str = "", stack: str = ""): self._client.span_set_error(self.span_id, typestr, message, stack) - def add_link(self, parent_id: int, attributes: dict | None = None): + def add_link(self, parent_id: int | str, attributes: dict | None = None): self._client.span_add_link(self.span_id, parent_id, attributes) def add_event(self, name: str, time_unix_nano: int, attributes: dict | None = None): @@ -425,52 +425,52 @@ def current_span(self) -> SpanResponse | None: return None return SpanResponse(span_id=resp_json["span_id"], trace_id=resp_json["trace_id"]) - def finish_span(self, span_id: int) -> None: + def finish_span(self, span_id: int | str) -> None: self._session.post(self._url("/trace/span/finish"), json={"span_id": span_id}) - def span_set_resource(self, span_id: int, resource: str) -> None: + def span_set_resource(self, span_id: int | str, resource: str) -> None: self._session.post( self._url("/trace/span/set_resource"), json={"span_id": span_id, "resource": resource}, ) - def span_set_meta(self, span_id: int, key: str, *, value: str | bool | list[str | list[str]] | None) -> None: + def span_set_meta(self, span_id: int | str, key: str, *, value: str | bool | list[str | list[str]] | None) -> None: self._session.post( self._url("/trace/span/set_meta"), json={"span_id": span_id, "key": key, "value": value}, ) - def span_set_baggage(self, span_id: int, key: str, value: str) -> None: + def span_set_baggage(self, span_id: int | str, key: str, value: str) -> None: self._session.post( self._url("/trace/span/set_baggage"), json={"span_id": span_id, "key": key, "value": value}, ) - def span_remove_baggage(self, span_id: int, key: str) -> None: + def span_remove_baggage(self, span_id: int | str, key: str) -> None: self._session.post( self._url("/trace/span/remove_baggage"), json={"span_id": span_id, "key": key}, ) - def span_remove_all_baggage(self, span_id: int) -> None: + def span_remove_all_baggage(self, span_id: int | str) -> None: self._session.post(self._url("/trace/span/remove_all_baggage"), json={"span_id": span_id}) - def span_set_metric(self, span_id: int, key: str, value: float | list[int] | None) -> None: + def span_set_metric(self, span_id: int | str, key: str, value: float | list[int] | None) -> None: self._session.post(self._url("/trace/span/set_metric"), json={"span_id": span_id, "key": key, "value": value}) - def span_manual_keep(self, span_id: int) -> None: + def span_manual_keep(self, span_id: int | str) -> None: self._session.post( self._url("/trace/span/manual_keep"), json={"span_id": span_id}, ) - def span_manual_drop(self, span_id: int) -> None: + def span_manual_drop(self, span_id: int | str) -> None: self._session.post( self._url("/trace/span/manual_drop"), json={"span_id": span_id}, ) - def span_set_error(self, span_id: int, typestr: str, message: str, stack: str) -> None: + def span_set_error(self, span_id: int | str, typestr: str, message: str, stack: str) -> None: self._session.post( self._url("/trace/span/error"), json={ @@ -481,7 +481,7 @@ def span_set_error(self, span_id: int, typestr: str, message: str, stack: str) - }, ) - def span_add_link(self, span_id: int, parent_id: int, attributes: dict | None = None): + def span_add_link(self, span_id: int | str, parent_id: int | str, attributes: dict | None = None): self._session.post( self._url("/trace/span/add_link"), json={ @@ -491,7 +491,7 @@ def span_add_link(self, span_id: int, parent_id: int, attributes: dict | None = }, ) - def span_add_event(self, span_id: int, name: str, time_unix_nano: int, attributes: dict | None = None): + def span_add_event(self, span_id: int | str, name: str, time_unix_nano: int, attributes: dict | None = None): self._session.post( self._url("/trace/span/add_event"), json={ @@ -502,12 +502,12 @@ def span_add_event(self, span_id: int, name: str, time_unix_nano: int, attribute }, ) - def span_get_baggage(self, span_id: int, key: str) -> str: + def span_get_baggage(self, span_id: int | str, key: str) -> str: resp = self._session.get(self._url("/trace/span/get_baggage"), json={"span_id": span_id, "key": key}) data = resp.json() return data["baggage"] - def span_get_all_baggage(self, span_id: int) -> dict: + def span_get_all_baggage(self, span_id: int | str) -> dict: resp = self._session.get(self._url("/trace/span/get_all_baggage"), json={"span_id": span_id}) data = resp.json() return data["baggage"] @@ -521,7 +521,7 @@ def dd_make_child_span_and_get_headers(self, headers: Iterable[tuple[str, str]]) headers = self.dd_inject_headers(span.span_id) return {k.lower(): v for k, v in headers} - def dd_inject_headers(self, span_id: int): + def dd_inject_headers(self, span_id: int | str): resp = self._session.post(self._url("/trace/span/inject_headers"), json={"span_id": span_id}) # TODO: translate json into list within list # so server.xx do not have to diff --git a/utils/docker_fixtures/spec/trace.py b/utils/docker_fixtures/spec/trace.py index 6de0231d80c..e55fffe5f8e 100644 --- a/utils/docker_fixtures/spec/trace.py +++ b/utils/docker_fixtures/spec/trace.py @@ -176,7 +176,7 @@ def find_span(trace: Trace, span_id: str | int) -> Span: raise AssertionError(f"Span with id={span_id} not found. Trace={trace}") -def find_span_in_traces(traces: list[Trace], trace_id: int, span_id: int) -> Span: +def find_span_in_traces(traces: list[Trace], trace_id: int, span_id: int | str) -> Span: """Return a span from a list of traces by `trace_id` and `span_id`.""" trace = find_trace(traces, trace_id) return find_span(trace, span_id) From 9858237f3dad711d0b7bc9f3d5b8172ceedfce4f Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Tue, 31 Mar 2026 09:33:41 +0200 Subject: [PATCH 12/13] fix lints --- tests/parametric/test_trace_sampling.py | 14 ++++++++++++-- .../_test_clients/_test_client_parametric.py | 1 - 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/parametric/test_trace_sampling.py b/tests/parametric/test_trace_sampling.py index 26f4f112803..2fd68ca787c 100644 --- a/tests/parametric/test_trace_sampling.py +++ b/tests/parametric/test_trace_sampling.py @@ -450,7 +450,12 @@ def tag_sampling_env(tag_glob_pattern: str) -> dict: @features.adaptive_sampling class Test_Trace_Sampling_Tags_Feb2024_Revision: def assert_matching_span( - self, test_agent: TestAgentAPI, trace_id: int, span_id: int | str, name: str | None = None, service: str | None = None + self, + test_agent: TestAgentAPI, + trace_id: int, + span_id: int | str, + name: str | None = None, + service: str | None = None, ): matching_span = find_span_in_traces(test_agent.wait_for_num_traces(1), trace_id, span_id) @@ -464,7 +469,12 @@ def assert_matching_span( assert matching_span["service"] == service def assert_mismatching_span( - self, test_agent: TestAgentAPI, trace_id: int, span_id: int | str, name: str | None = None, service: str | None = None + self, + test_agent: TestAgentAPI, + trace_id: int, + span_id: int | str, + name: str | None = None, + service: str | None = None, ): mismatching_span = find_span_in_traces(test_agent.wait_for_num_traces(1), trace_id, span_id) diff --git a/utils/docker_fixtures/_test_clients/_test_client_parametric.py b/utils/docker_fixtures/_test_clients/_test_client_parametric.py index 7de52153d3c..b690f9166f8 100644 --- a/utils/docker_fixtures/_test_clients/_test_client_parametric.py +++ b/utils/docker_fixtures/_test_clients/_test_client_parametric.py @@ -153,7 +153,6 @@ def __init__(self, client: "ParametricTestClientApi", span_id: int | str, trace_ else: assert span_id.isdigit(), f"{span_id} is not decimal" - def set_resource(self, resource: str): self._client.span_set_resource(self.span_id, resource) From 93e7ebe99548aaeac921ca099cd0e48df4c556df Mon Sep 17 00:00:00 2001 From: Charles de Beauchesne Date: Tue, 31 Mar 2026 10:10:08 +0200 Subject: [PATCH 13/13] fix test_otel_start_after_datadog_span --- tests/parametric/test_otel_api_interoperability.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/parametric/test_otel_api_interoperability.py b/tests/parametric/test_otel_api_interoperability.py index 2e1e3f4858f..38258383c6a 100644 --- a/tests/parametric/test_otel_api_interoperability.py +++ b/tests/parametric/test_otel_api_interoperability.py @@ -53,7 +53,7 @@ def test_otel_start_after_datadog_span(self, test_agent: TestAgentAPI, test_libr # FIXME: The trace_id is encoded in hex while span_id is an int. Make this API consistent assert current_dd_span.trace_id == otel_context.get("trace_id") - assert f"{int(current_dd_span.span_id):016x}" == otel_context.get("span_id") + assert current_dd_span.span_id == otel_context.get("span_id") dd_span.finish() traces = test_agent.wait_for_num_traces(1, sort_by_start=False)