diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableTraceFlags.java b/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableTraceFlags.java index 7cd15f6077a..ca007995abe 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableTraceFlags.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/ImmutableTraceFlags.java @@ -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); @@ -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; @@ -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(); diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/SpanContext.java b/api/all/src/main/java/io/opentelemetry/api/trace/SpanContext.java index f7832c62b07..c01f1b8e961 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/SpanContext.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/SpanContext.java @@ -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. * *

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 diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlags.java b/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlags.java index 936a1a6e06c..9f918b7c8e5 100644 --- a/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlags.java +++ b/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlags.java @@ -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}. * @@ -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; + } } diff --git a/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlagsBuilder.java b/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlagsBuilder.java new file mode 100644 index 00000000000..de95a9a43f5 --- /dev/null +++ b/api/all/src/main/java/io/opentelemetry/api/trace/TraceFlagsBuilder.java @@ -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. + * + *

This is a simple usage example: + * + *

{@code
+ * TraceFlags traceFlags = TraceFlags.builder().setSampled(true).build();
+ * }
+ * + *

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(); +} diff --git a/api/all/src/test/java/io/opentelemetry/api/trace/TraceFlagsTest.java b/api/all/src/test/java/io/opentelemetry/api/trace/TraceFlagsTest.java index d6ef529f004..fc5c52e8ed5 100644 --- a/api/all/src/test/java/io/opentelemetry/api/trace/TraceFlagsTest.java +++ b/api/all/src/test/java/io/opentelemetry/api/trace/TraceFlagsTest.java @@ -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++) { diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt index 6bd0e35a4a7..08d05547f55 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-api.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-api.txt @@ -1,2 +1,11 @@ Comparing source compatibility of opentelemetry-api-1.60.0-SNAPSHOT.jar against opentelemetry-api-1.59.0.jar -No changes. \ No newline at end of file +*** 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) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt index e62044ac46a..5e8cfb5214e 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt @@ -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. \ No newline at end of file +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.sdk.trace.IdGenerator (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) boolean generatesRandomTraceIds() diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/IdGenerator.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/IdGenerator.java index e0ecd8efac5..50c808b660f 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/IdGenerator.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/IdGenerator.java @@ -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 Random TraceId flag. + * + * @return true if the generated TraceIds are random + */ + default boolean generatesRandomTraceIds() { + return false; + } } diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RandomIdGenerator.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RandomIdGenerator.java index 14045a5a923..5456afcbb39 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RandomIdGenerator.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/RandomIdGenerator.java @@ -38,6 +38,11 @@ public String generateTraceId() { return TraceId.fromLongs(idHi, idLo); } + @Override + public boolean generatesRandomTraceIds() { + return true; + } + @Override public String toString() { return "RandomIdGenerator{}"; diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java index 3ba8d575dd1..f8f70724644 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpanBuilder.java @@ -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; @@ -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; @@ -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) { @@ -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 currentLinks = links; List immutableLinks = @@ -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 = @@ -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()); diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java index 64b9a9483ea..55eb81d0b20 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanBuilderTest.java @@ -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 = @@ -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, "