Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion tests/parametric/test_otel_api_interoperability.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 3 additions & 12 deletions tests/parametric/test_otel_span_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -333,25 +334,15 @@ 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
traces = test_agent.wait_for_num_traces(1, sort_by_start=False)
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ private static async Task<string> 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
Expand Down
2 changes: 1 addition & 1 deletion utils/build/docker/golang/parametric/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
2 changes: 1 addition & 1 deletion utils/build/docker/golang/parametric/otel.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
4 changes: 2 additions & 2 deletions utils/build/docker/nodejs/parametric/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}`,
Expand Down
15 changes: 9 additions & 6 deletions utils/build/docker/php/parametric/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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()
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion utils/build/docker/ruby/parametric/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
pub trace_state: Option<String>,
Expand Down
4 changes: 2 additions & 2 deletions utils/build/docker/rust/parametric/src/opentelemetry/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()),
Expand Down
56 changes: 33 additions & 23 deletions utils/docker_fixtures/_test_clients/_test_client_parametric.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Comment on lines +190 to +191
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Remove strict 0x check until all clients emit new format

This assertion now rejects every string span_id that does not start with 0x, but the PHP parametric server still returns decimal string IDs for /trace/otel/start_span (it sends span_id => $spanId as a string), and PHP OTel tests are active (e.g., tests/parametric/test_otel_span_methods.py::Test_Otel_Span_Methods is enabled in manifests/php.yml). In that environment, _TestOtelSpan construction fails immediately, so many PHP OTel tests regress before reaching their assertions.

Useful? React with 👍 / 👎.


# API methods

Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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={
Expand All @@ -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},
Expand Down
2 changes: 1 addition & 1 deletion utils/docker_fixtures/parametric.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ class LogLevel(Enum):


class Link(TypedDict):
parent_id: int
parent_id: str | int
attributes: NotRequired[dict]
5 changes: 4 additions & 1 deletion utils/docker_fixtures/spec/trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
Loading