Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
import javax.annotation.concurrent.Immutable;

@Immutable
final class ImmutableTraceFlags implements TraceFlags {
final class ImmutableTraceFlags implements TraceFlags, TraceFlagsBuilder {
private static final ImmutableTraceFlags[] INSTANCES = buildInstances();
// Bit to represent whether trace is sampled or not.
private 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
private 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 All @@ -65,6 +73,26 @@ public byte asByte() {
return this.byteRep;
}

@Override
public ImmutableTraceFlags setSampled(boolean isSampled) {
byte newByte = isSampled ? (byte) (asByte() | SAMPLED_BIT) : (byte) (asByte() & ~SAMPLED_BIT);
return fromByte(newByte);
}

@Override
public ImmutableTraceFlags setRandomTraceId(boolean isRandomTraceId) {
byte newByte =
isRandomTraceId
? (byte) (asByte() | RANDOM_TRACE_ID_BIT)
: (byte) (asByte() & ~RANDOM_TRACE_ID_BIT);
return fromByte(newByte);
}

@Override
public TraceFlags build() {
return this;
}

@Override
public String toString() {
return asHex();
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,9 @@ static TraceFlags fromByte(byte traceFlagsByte) {
* @return the byte representation of the {@link TraceFlags}.
*/
byte asByte();

/** Returns an instance of {@link TraceFlagsBuilder} for {@link TraceFlags}. */
static TraceFlagsBuilder builder() {
return ImmutableTraceFlags.DEFAULT;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.api.trace;

/**
* {@link TraceFlagsBuilder} is used to construct {@link TraceFlags} instances which satisfy the
* https://www.w3.org/TR/trace-context-2/#trace-flags specification.
*
* <p>This is a simple usage example:
*
* <pre>{@code
* TraceFlags traceFlags = TraceFlags.builder().setSampled(true).build();
* }</pre>
*
* <p>Implementation note: no new objects are created by the methods defined by this interface when
* the default implementation, {@link ImmutableTraceFlags}, is used.
*/
public interface TraceFlagsBuilder {

/**
* Returns an instance of {@link TraceFlagsBuilder} which represents a {@link TraceFlags} object
* which has the SAMPLED bit set if the argument is {@code true} and the SAMPLED bit cleared when
* the argument is {@code false}. Other bits remain unchanged. The operation does not modify this
* object.
*
* @param isSampled the new value for the SAMPLED bit
* @return a {@link TraceFlagsBuilder} object representing the modified {@link TraceFlags}
*/
TraceFlagsBuilder setSampled(boolean isSampled);

/**
* Returns an instance of {@link TraceFlagsBuilder} which represents a {@link TraceFlags} object
* which has the RANDOM_TRACE_ID bit set if the argument is {@code true} and the RANDOM_TRACE_ID
* bit cleared when the argument is {@code false}. Other bits remain unchanged. The operation does
* not modify this object.
*
* @param isRandomTraceId the new value for the RANDOM_TRACE_ID bit
* @return a {@link TraceFlagsBuilder} object representing the modified {@link TraceFlags}
*/
TraceFlagsBuilder setRandomTraceId(boolean isRandomTraceId);

/**
* Returns {@link TraceFlags} represented by this object.
*
* @return a {@link TraceFlags} object with the bits set as configured
*/
TraceFlags build();
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,59 @@

import org.junit.jupiter.api.Test;

/** Unit tests for {@link TraceFlags}. */
/** Unit tests for {@link TraceFlags} and {@link TraceFlagsBuilder}. */
class TraceFlagsTest {

@Test
void defaultInstances() {
assertThat(TraceFlags.getDefault().asHex()).isEqualTo("00");
assertThat(TraceFlags.getSampled().asHex()).isEqualTo("01");
assertThat(TraceFlags.builder().build().asHex()).isEqualTo("00");
assertThat(TraceFlags.builder().setSampled(true).build().asHex()).isEqualTo("01");
assertThat(TraceFlags.builder().setSampled(false).build().asHex()).isEqualTo("00");
assertThat(TraceFlags.builder().setRandomTraceId(true).build().asHex()).isEqualTo("02");
assertThat(TraceFlags.builder().setRandomTraceId(false).build().asHex()).isEqualTo("00");
assertThat(TraceFlags.builder().setSampled(true).setRandomTraceId(true).build().asHex())
.isEqualTo("03");
assertThat(TraceFlags.builder().setRandomTraceId(true).setSampled(true).build().asHex())
.isEqualTo("03");
}

@Test
void idempotency() {
assertThat(TraceFlags.builder().setRandomTraceId(true).setRandomTraceId(true).build().asHex())
.isEqualTo("02");
assertThat(TraceFlags.builder().setSampled(true).setSampled(true).build().asHex())
.isEqualTo("01");
}

@Test
void setAndClear() {
assertThat(TraceFlags.builder().setRandomTraceId(true).setRandomTraceId(false).build().asHex())
.isEqualTo("00");
assertThat(TraceFlags.builder().setSampled(true).setSampled(false).build().asHex())
.isEqualTo("00");
}

@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
11 changes: 10 additions & 1 deletion docs/apidiffs/current_vs_latest/opentelemetry-api.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,11 @@
Comparing source compatibility of opentelemetry-api-1.60.0-SNAPSHOT.jar against opentelemetry-api-1.59.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(+) STATIC(+) io.opentelemetry.api.trace.TraceFlagsBuilder builder()
+++ NEW METHOD: PUBLIC(+) boolean isTraceIdRandom()
+++ NEW INTERFACE: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.trace.TraceFlagsBuilder (not serializable)
+++ CLASS FILE FORMAT VERSION: 52.0 <- n.a.
+++ NEW SUPERCLASS: java.lang.Object
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.trace.TraceFlags build()
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.trace.TraceFlagsBuilder setRandomTraceId(boolean)
+++ NEW METHOD: PUBLIC(+) ABSTRACT(+) io.opentelemetry.api.trace.TraceFlagsBuilder setSampled(boolean)
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
Comparing source compatibility of opentelemetry-sdk-trace-1.60.0-SNAPSHOT.jar against opentelemetry-sdk-trace-1.59.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 @@ -35,6 +37,10 @@
/** {@link SdkSpanBuilder} is SDK implementation of {@link SpanBuilder}. */
class SdkSpanBuilder implements SpanBuilder {

private static final Context ROOT_CONTEXT_WITH_RANDOM_TRACE_ID_BIT =
preparePrimordialContext(
TraceFlags.builder().setRandomTraceId(true).build(), TraceState.getDefault());

private final String spanName;
private final InstrumentationScopeInfo instrumentationScopeInfo;
private final TracerSharedState tracerSharedState;
Expand All @@ -58,6 +64,17 @@ class SdkSpanBuilder implements SpanBuilder {
this.spanLimits = spanLimits;
}

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

@Override
public SpanBuilder setParent(Context context) {
if (context == null) {
Expand Down Expand Up @@ -170,14 +187,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 = ROOT_CONTEXT_WITH_RANDOM_TRACE_ID_BIT;
} 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 +218,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 +232,10 @@ public Span startSpan() {
ImmutableSpanContext.create(
traceId,
spanId,
isSampled(samplingDecision) ? TraceFlags.getSampled() : TraceFlags.getDefault(),
TraceFlags.builder()
.setSampled(isSampled(samplingDecision))
.setRandomTraceId(isTraceIdRandom)
.build(),
samplingResultTraceState,
/* remote= */ false,
tracerSharedState.isIdGeneratorSafeToSkipIdValidation());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,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 @@ -989,7 +1021,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