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
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
final class ImmutableTraceFlags implements TraceFlags {
private static final ImmutableTraceFlags[] INSTANCES = buildInstances();
// Bit to represent whether trace is sampled or not.
private static final byte SAMPLED_BIT = 0x01;
static final byte SAMPLED_BIT = 0x01;
// Bit to indicate that the lower 56 bits of the trace id have been randomly generated with
// uniform distribution
static final byte RANDOM_TRACE_ID_BIT = 0x02;

static final ImmutableTraceFlags DEFAULT = fromByte((byte) 0x00);
static final ImmutableTraceFlags SAMPLED = fromByte(SAMPLED_BIT);
Expand Down Expand Up @@ -55,6 +58,11 @@ public boolean isSampled() {
return (this.byteRep & SAMPLED_BIT) != 0;
}

@Override
public boolean isTraceIdRandom() {
return (this.byteRep & RANDOM_TRACE_ID_BIT) != 0;
}

@Override
public String asHex() {
return this.hexRep;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@
/**
* A class that represents a span context. A span context contains the state that must propagate to
* child {@link Span}s and across process boundaries. It contains the identifiers (a {@link TraceId
* trace_id} and {@link SpanId span_id}) associated with the {@link Span} and a set of options
* (currently only whether the context is sampled or not), as well as the {@link TraceState
* traceState} and the {@link boolean remote} flag.
* trace_id} and {@link SpanId span_id}) associated with the {@link Span}, {@link TraceFlags}, as
* well as the {@link TraceState traceState} and the {@link boolean remote} flag.
*
* <p>Implementations of this interface *must* be immutable and have well-defined value-based
* equals/hashCode implementations. If an implementation does not strictly conform to these
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,18 @@ static TraceFlags fromByte(byte traceFlagsByte) {
*/
boolean isSampled();

/**
* Returns {@code true} if the TraceId accompanying this {@link TraceFlags} is known to be
* generated by a truly random Id generator, otherwise {@code false}. Providing default
* implementation just to maintain compatibility.
*
* @return {@code true} if the randomTraceId bit is on for this {@link TraceFlags}, otherwise
* {@code false}.
*/
default boolean isTraceIdRandom() {
return false;
}

/**
* Returns the lowercase hex (base16) representation of this {@link TraceFlags}.
*
Expand All @@ -89,4 +101,26 @@ static TraceFlags fromByte(byte traceFlagsByte) {
* @return the byte representation of the {@link TraceFlags}.
*/
byte asByte();

/**
* Returns an instance of {@link TraceFlags} whose value is the result of a bitwise OR between
* this object and the SAMPLED bit. This operation does not modify this object.
*
* @return a new {@link TraceFlags} object representing {@code this | SAMPLED_BIT}
*/
default TraceFlags withSampledBit() {
Copy link
Member

Choose a reason for hiding this comment

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

Taking another look at this, I think these need to be static helpers with signatures of the form static TraceFlags with{Param}(TraceFlags traceFlags, boolean {param}) { ...}.

The instance levels make for nice UX, but are overridable, which I believe is never needed and would lead to bad / hard-to-debug behavior.

Copy link
Contributor Author

@PeterF778 PeterF778 Jan 29, 2026

Choose a reason for hiding this comment

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

Yes, we do not want them to be overridden. I definitely considered the static methods, and ultimately opted out of them: the usage is less convenient, and even though TraceFlags is an interface, it is really tightly coupled with ImmutableTraceFlags (see fromHex(), and fromByte()), so in practice developing a different implementation would be very inconvenient, if not impossible.

Another thing is about being able to reset some bits. I consciously did not provide this functionality. W3C Trace Context specification effectively says that any set bit from the upstream TraceFlags has to be cleared if the current implementation does not recognize it. I read this as a requirement to always build TraceFlags from scratch. Providing the capability to take (potentially unknown) TraceFlags and clear selected bits could confuse the users into thinking that they can do this with incoming TraceFlags.

byte newByte = (byte) (asByte() | ImmutableTraceFlags.SAMPLED_BIT);
return ImmutableTraceFlags.fromByte(newByte);
}

/**
* Returns an instance of {@link TraceFlags} whose value is the result of a bitwise OR between
* this object and the RANDOM_TRACE_ID bit. This operation does not modify this object.
*
* @return a new {@link TraceFlags} object representing {@code this | RANDOM_TRACE_ID_BIT}
*/
default TraceFlags withRandomTraceIdBit() {
byte newByte = (byte) (asByte() | ImmutableTraceFlags.RANDOM_TRACE_ID_BIT);
return ImmutableTraceFlags.fromByte(newByte);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,41 @@ class TraceFlagsTest {
@Test
void defaultInstances() {
assertThat(TraceFlags.getDefault().asHex()).isEqualTo("00");
assertThat(TraceFlags.getSampled().asHex()).isEqualTo("01");
assertThat(TraceFlags.getDefault().withSampledBit().asHex()).isEqualTo("01");
assertThat(TraceFlags.getDefault().withRandomTraceIdBit().asHex()).isEqualTo("02");
assertThat(TraceFlags.getDefault().withRandomTraceIdBit().withSampledBit().asHex())
.isEqualTo("03");
assertThat(TraceFlags.getDefault().withSampledBit().withRandomTraceIdBit().asHex())
.isEqualTo("03");
}

@Test
void idempotency() {
assertThat(TraceFlags.getDefault().withRandomTraceIdBit().withRandomTraceIdBit().asHex())
.isEqualTo("02");
assertThat(TraceFlags.getDefault().withSampledBit().withSampledBit().asHex()).isEqualTo("01");
}

@Test
void isSampled() {
assertThat(TraceFlags.fromByte((byte) 0xff).isSampled()).isTrue();
assertThat(TraceFlags.fromByte((byte) 0x01).isSampled()).isTrue();
assertThat(TraceFlags.fromByte((byte) 0x02).isSampled()).isFalse();
assertThat(TraceFlags.fromByte((byte) 0x03).isSampled()).isTrue();
assertThat(TraceFlags.fromByte((byte) 0x05).isSampled()).isTrue();
assertThat(TraceFlags.fromByte((byte) 0x00).isSampled()).isFalse();
}

@Test
void isTraceIdRandom() {
assertThat(TraceFlags.fromByte((byte) 0xff).isTraceIdRandom()).isTrue();
assertThat(TraceFlags.fromByte((byte) 0x01).isTraceIdRandom()).isFalse();
assertThat(TraceFlags.fromByte((byte) 0x02).isTraceIdRandom()).isTrue();
assertThat(TraceFlags.fromByte((byte) 0x03).isTraceIdRandom()).isTrue();
assertThat(TraceFlags.fromByte((byte) 0x05).isTraceIdRandom()).isFalse();
assertThat(TraceFlags.fromByte((byte) 0x00).isTraceIdRandom()).isFalse();
}

@Test
void toFromHex() {
for (int i = 0; i < 256; i++) {
Expand Down
6 changes: 5 additions & 1 deletion docs/apidiffs/current_vs_latest/opentelemetry-api.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
Comparing source compatibility of opentelemetry-api-1.59.0-SNAPSHOT.jar against opentelemetry-api-1.58.0.jar
No changes.
*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.api.trace.TraceFlags (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) boolean isTraceIdRandom()
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.trace.TraceFlags withRandomTraceIdBit()
+++ NEW METHOD: PUBLIC(+) io.opentelemetry.api.trace.TraceFlags withSampledBit()
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
Comparing source compatibility of opentelemetry-sdk-trace-1.59.0-SNAPSHOT.jar against opentelemetry-sdk-trace-1.58.0.jar
No changes.
*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.sdk.trace.IdGenerator (not serializable)
=== CLASS FILE FORMAT VERSION: 52.0 <- 52.0
+++ NEW METHOD: PUBLIC(+) boolean generatesRandomTraceIds()
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,16 @@ static IdGenerator random() {
* @return a new valid {@code TraceId}.
*/
String generateTraceId();

/**
* Declares whether TraceIds generated by this IdGenerator have their lower 56 bits uniformly
* distributed over the [0..2^56-1]interval, making them compatible with W3C Trace Context Level 2
* recommendation @see <a href=
* "https://www.w3.org/TR/trace-context-2/#random-trace-id-flag">Random TraceId flag</a>.
*
* @return true if the generated TraceIds are random
*/
default boolean generatesRandomTraceIds() {
return false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ public String generateTraceId() {
return TraceId.fromLongs(idHi, idLo);
}

@Override
public boolean generatesRandomTraceIds() {
return true;
}

@Override
public String toString() {
return "RandomIdGenerator{}";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanBuilder;
import io.opentelemetry.api.trace.SpanContext;
import io.opentelemetry.api.trace.SpanId;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.TraceFlags;
import io.opentelemetry.api.trace.TraceId;
import io.opentelemetry.api.trace.TraceState;
import io.opentelemetry.context.Context;
import io.opentelemetry.sdk.common.InstrumentationScopeInfo;
Expand All @@ -39,6 +41,7 @@ class SdkSpanBuilder implements SpanBuilder {
private final InstrumentationScopeInfo instrumentationScopeInfo;
private final TracerSharedState tracerSharedState;
private final SpanLimits spanLimits;
private final Context rootContextWithRandomTraceIdBit;

@Nullable private Context parent; // null means: Use current context.
private SpanKind spanKind = SpanKind.INTERNAL;
Expand All @@ -56,6 +59,23 @@ class SdkSpanBuilder implements SpanBuilder {
this.instrumentationScopeInfo = instrumentationScopeInfo;
this.tracerSharedState = tracerSharedState;
this.spanLimits = spanLimits;
this.rootContextWithRandomTraceIdBit =
Copy link
Member

Choose a reason for hiding this comment

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

If I'm reading this right, rootContextWithRandomTraceIdBit can be private static final singleton, since there's no parameters which are dependent on this constructor's params, and the instance itself is immutable and thread safe.

I've sketched this (and my other suggestions) out in this commit to improve clarity: c77bad6bb

Copy link
Contributor Author

@PeterF778 PeterF778 Jan 29, 2026

Choose a reason for hiding this comment

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

yes, it should be static - thanks!

preparePrimordialContext(
Context.root(),
TraceFlags.getDefault().withRandomTraceIdBit(),
TraceState.getDefault());
}

/*
* A primordial context can be passed as the parent context for a root span
* if a non-default TraceFlags or TraceState need to be passed to the sampler
*/
private static Context preparePrimordialContext(
Context parentContext, TraceFlags traceFlags, TraceState traceState) {
Comment on lines +69 to +74
Copy link

Choose a reason for hiding this comment

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

I see this is private. I'm aware of an underspecified region in the OTel specification. Should there be a specified way to create these "primordial" contexts with control over the root trace state?

In the Go SDK, asking for a new root gets you an empty context: https://github.com/open-telemetry/opentelemetry-go/blob/8d3b4cb2501dec9f1c5373123e425f109c43b8d2/sdk/trace/tracer.go#L92

It's not clear whether users are able to setup a context with control over TraceState at the root. We write:

Root Samplers MAY insert an explicit randomness value into the
OpenTelemetry TraceState value in cases where an explicit
randomness value is not already set.

It doesn't actually require SDKs provide a way to set explicit randomness for the root span, it just refers to the potential scenario.

Copy link
Member

Choose a reason for hiding this comment

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

Should there be a specified way to create these "primordial" contexts with control over the root trace state?

I was thinking something similar. Specifically, its weird that the primordial context always has the random trace bit set to true, even if paired with an IdGenerator which does not generate random trace ids.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In theory, the "primordial" context can have non-empty TraceFlags, and non-empty TraceState (which we do not use yet), while still remaining "invalid", forcing the "child" span to be root. Hence I opted for passing both TraceFlags and TraceState as arguments for this method, just to emphasize this point.

It is not clear to me, if the IdGenerator can be changed dynamically, but if it can, I believe this case will be handled properly (i.e. the primordial context to use will have the RandomTraceId on or off, depending on the IdGenerator). If a non-random IdGenerator is use, the RandomTraceId flad will not be set.

SpanContext spanContext =
SpanContext.create(TraceId.getInvalid(), SpanId.getInvalid(), traceFlags, traceState);
Span span = Span.wrap(spanContext);
return span.storeInContext(parentContext);
}

@Override
Expand Down Expand Up @@ -170,14 +190,25 @@ public Span startSpan() {
Span parentSpan = Span.fromContext(parentContext);
SpanContext parentSpanContext = parentSpan.getSpanContext();
String traceId;
boolean isTraceIdRandom;
IdGenerator idGenerator = tracerSharedState.getIdGenerator();
String spanId = idGenerator.generateSpanId();

Context parentContextForSampler = parentContext;
if (!parentSpanContext.isValid()) {
// New root span.
traceId = idGenerator.generateTraceId();
if (idGenerator.generatesRandomTraceIds()) {
isTraceIdRandom = true;
// Replace parentContext for sampling with one with RANDOM_TRACE_ID bit set
parentContextForSampler = rootContextWithRandomTraceIdBit;
} else {
isTraceIdRandom = false;
}
} else {
// New child span.
traceId = parentSpanContext.getTraceId();
isTraceIdRandom = parentSpanContext.getTraceFlags().isTraceIdRandom();
}
List<LinkData> currentLinks = links;
List<LinkData> immutableLinks =
Expand All @@ -190,7 +221,12 @@ public Span startSpan() {
tracerSharedState
.getSampler()
.shouldSample(
parentContext, traceId, spanName, spanKind, immutableAttributes, immutableLinks);
parentContextForSampler,
traceId,
spanName,
spanKind,
immutableAttributes,
immutableLinks);
SamplingDecision samplingDecision = samplingResult.getDecision();

TraceState samplingResultTraceState =
Expand All @@ -199,7 +235,7 @@ public Span startSpan() {
ImmutableSpanContext.create(
traceId,
spanId,
isSampled(samplingDecision) ? TraceFlags.getSampled() : TraceFlags.getDefault(),
newTraceFlags(isTraceIdRandom, isSampled(samplingDecision)),
samplingResultTraceState,
/* remote= */ false,
tracerSharedState.isIdGeneratorSafeToSkipIdValidation());
Expand Down Expand Up @@ -239,6 +275,17 @@ public Span startSpan() {
recordEndSpanMetrics);
}

private static TraceFlags newTraceFlags(boolean randomTraceId, boolean sampled) {
Copy link
Member

Choose a reason for hiding this comment

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

This reads as a near copy of the TraceFlags.withRandomTraceIdBit() and TraceFlags.withSampledBit() methods, and could go away if those methods accepted a boolean parameter:

  default TraceFlags withSampled(boolean isSampled) {
    byte newByte =
        isSampled
            ? (byte) (asByte() | ImmutableTraceFlags.SAMPLED_BIT)
            : (byte) (asByte() & ~ImmutableTraceFlags.SAMPLED_BIT);
    return ImmutableTraceFlags.fromByte(newByte);
  }

  default TraceFlags withRandomTraceId(boolean isRandomTraceId) {
    byte newByte =
        isRandomTraceId
            ? (byte) (asByte() | ImmutableTraceFlags.RANDOM_TRACE_ID_BIT)
            : (byte) (asByte() & ~ImmutableTraceFlags.RANDOM_TRACE_ID_BIT);
    return ImmutableTraceFlags.fromByte(newByte);
  }

TraceFlags traceFlags = TraceFlags.getDefault();
if (randomTraceId) {
traceFlags = traceFlags.withRandomTraceIdBit();
}
if (sampled) {
traceFlags = traceFlags.withSampledBit();
}
return traceFlags;
}

private AttributesMap attributes() {
AttributesMap attributes = this.attributes;
if (attributes == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,38 @@ void parent_invalidContext() {
}
}

@Test
void propagateRandomTraceIdFlag() {
Span parent = sdkTracer.spanBuilder(SPAN_NAME).startSpan();
assertThat(parent.getSpanContext().getTraceFlags().isTraceIdRandom()).isTrue();
try (Scope ignored = parent.makeCurrent()) {
Span span = (SdkSpan) sdkTracer.spanBuilder(SPAN_NAME).startSpan();
assertThat(span.getSpanContext().getTraceId())
.isEqualTo(parent.getSpanContext().getTraceId());
assertThat(span.getSpanContext().getTraceFlags().isTraceIdRandom()).isTrue();
try (Scope spanScope = span.makeCurrent()) {
// Nested span
Span nestedSpan = sdkTracer.spanBuilder(SPAN_NAME).startSpan();
// check that still the same trace
assertThat(nestedSpan.getSpanContext().getTraceId())
.isEqualTo(parent.getSpanContext().getTraceId());
// check if RandomTraceIdFlag is still there
assertThat(nestedSpan.getSpanContext().getTraceFlags().isTraceIdRandom()).isTrue();
try (Scope nestedScope = span.makeCurrent()) {
Context nestedContext = Context.current();
Span currentSpan = Span.fromContext(nestedContext);
assertThat(currentSpan.getSpanContext().getTraceFlags().isTraceIdRandom()).isTrue();
} finally {
nestedSpan.end();
}
} finally {
span.end();
}
} finally {
parent.end();
}
}

@Test
void startTimestamp_numeric() {
SdkSpan span =
Expand Down Expand Up @@ -983,7 +1015,7 @@ void spanDataToString() {
"SpanData\\{spanContext=ImmutableSpanContext\\{"
+ "traceId=[0-9a-f]{32}, "
+ "spanId=[0-9a-f]{16}, "
+ "traceFlags=01, "
+ "traceFlags=03, "
+ "traceState=ArrayBasedTraceState\\{entries=\\[]}, remote=false, valid=true}, "
+ "parentSpanContext=ImmutableSpanContext\\{"
+ "traceId=00000000000000000000000000000000, "
Expand Down
Loading