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 samplingrandomTraceId bit is on for this {@link TraceFlags},
Copy link
Member

Choose a reason for hiding this comment

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

Is samplingrandomTraceId supposed to reference a constant?

* 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 a new 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() {
byte newByte = (byte) (asByte() | ImmutableTraceFlags.SAMPLED_BIT);
return ImmutableTraceFlags.fromByte(newByte);
}

/**
* Returns a new instance of {@link TraceFlags} whose value is the result of a bitwise OR between
Copy link
Member

@jack-berg jack-berg Jan 27, 2026

Choose a reason for hiding this comment

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

#nit: new may imply object construction. Same advice applies to withSampledBit javadoc.

Suggested change
* Returns a new instance of {@link TraceFlags} whose value is the result of a bitwise OR between
* 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 Down Expand Up @@ -170,14 +172,29 @@ 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 a temporary one with RANDOM_TRACE_ID bit set
parentContextForSampler =
preparePrimordialContext(
parentContext,
TraceFlags.getDefault().withRandomTraceIdBit(),
Copy link
Member

@jack-berg jack-berg Jan 27, 2026

Choose a reason for hiding this comment

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

Similar to this comment, we should use singletons here to avoid allocations on the hot path.

Ideally the entire primordial Span / SpanContext would be singletons.

TraceState.getDefault());
} 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 +207,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 +221,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 +261,29 @@ public Span startSpan() {
recordEndSpanMetrics);
}

/*
* 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) {
SpanContext spanContext =
SpanContext.create(TraceId.getInvalid(), SpanId.getInvalid(), traceFlags, traceState);
Span span = Span.wrap(spanContext);
return span.storeInContext(parentContext);
}

private static TraceFlags newTraceFlags(boolean randomTraceId, boolean sampled) {
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