diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt index b02f2754379..8b1cd1202c5 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-trace.txt @@ -1,2 +1,5 @@ Comparing source compatibility of opentelemetry-sdk-trace-1.53.0-SNAPSHOT.jar against opentelemetry-sdk-trace-1.52.0.jar -No changes. \ No newline at end of file +*** MODIFIED CLASS: PUBLIC FINAL io.opentelemetry.sdk.trace.SdkTracerProviderBuilder (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.trace.SdkTracerProviderBuilder setInternalTelemetry(io.opentelemetry.sdk.common.InternalTelemetryVersion) + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.trace.SdkTracerProviderBuilder setInternalTelemetryOpenTelemetry(java.util.function.Supplier) diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/grpc/GrpcExporterBuilder.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/grpc/GrpcExporterBuilder.java index 27d06b7116b..632ac8f031d 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/grpc/GrpcExporterBuilder.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/grpc/GrpcExporterBuilder.java @@ -287,7 +287,7 @@ public String toString(boolean includePrefixAndSuffix) { joiner.add("executorService=" + executorService); } joiner.add("exporterType=" + exporterType.toString()); - joiner.add("internalTelemetrySchemaVersion=" + internalTelemetryVersion); + joiner.add("internalTelemetryVersion=" + internalTelemetryVersion); // Note: omit tlsConfigHelper because we can't log the configuration in any readable way // Note: omit meterProviderSupplier because we can't log the configuration in any readable way return joiner.toString(); diff --git a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/http/HttpExporterBuilder.java b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/http/HttpExporterBuilder.java index 42d55cb0a2c..f63153ddf44 100644 --- a/exporters/common/src/main/java/io/opentelemetry/exporter/internal/http/HttpExporterBuilder.java +++ b/exporters/common/src/main/java/io/opentelemetry/exporter/internal/http/HttpExporterBuilder.java @@ -284,7 +284,7 @@ public String toString(boolean includePrefixAndSuffix) { joiner.add("executorService=" + executorService); } joiner.add("exporterType=" + exporterType); - joiner.add("internalTelemetrySchemaVersion=" + internalTelemetryVersion); + joiner.add("internalTelemetryVersion=" + internalTelemetryVersion); // Note: omit tlsConfigHelper because we can't log the configuration in any readable way // Note: omit meterProviderSupplier because we can't log the configuration in any readable way return joiner.toString(); diff --git a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java index ec25c9983a4..20db8764299 100644 --- a/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java +++ b/exporters/zipkin/src/main/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterBuilder.java @@ -208,7 +208,7 @@ String toString(boolean includePrefixAndSuffix) { joiner.add("endpoint=" + endpoint); joiner.add("compressionEnabled=" + compressionEnabled); joiner.add("readTimeoutMillis=" + readTimeoutMillis); - joiner.add("internalTelemetrySchemaVersion=" + internalTelemetryVersion); + joiner.add("internalTelemetryVersion=" + internalTelemetryVersion); // Note: omit sender because we can't log the configuration in any readable way // Note: omit encoder because we can't log the configuration in any readable way // Note: omit localIpAddressSupplier because we can't log the configuration in any readable way diff --git a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java index 759cfae782b..cc48ff0fb8f 100644 --- a/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java +++ b/exporters/zipkin/src/test/java/io/opentelemetry/exporter/zipkin/ZipkinSpanExporterTest.java @@ -256,7 +256,7 @@ void stringRepresentation() { try (ZipkinSpanExporter exporter = ZipkinSpanExporter.builder().build()) { assertThat(exporter.toString()) .isEqualTo( - "ZipkinSpanExporter{endpoint=http://localhost:9411/api/v2/spans, compressionEnabled=true, readTimeoutMillis=10000, internalTelemetrySchemaVersion=LEGACY}"); + "ZipkinSpanExporter{endpoint=http://localhost:9411/api/v2/spans, compressionEnabled=true, readTimeoutMillis=10000, internalTelemetryVersion=LEGACY}"); } try (ZipkinSpanExporter exporter = ZipkinSpanExporter.builder() @@ -266,7 +266,7 @@ void stringRepresentation() { .build()) { assertThat(exporter.toString()) .isEqualTo( - "ZipkinSpanExporter{endpoint=http://zipkin:9411/api/v2/spans, compressionEnabled=false, readTimeoutMillis=15000, internalTelemetrySchemaVersion=LEGACY}"); + "ZipkinSpanExporter{endpoint=http://zipkin:9411/api/v2/spans, compressionEnabled=false, readTimeoutMillis=15000, internalTelemetryVersion=LEGACY}"); } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/SemConvAttributes.java b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/SemConvAttributes.java index 1075d537604..b73d6251cd4 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/internal/SemConvAttributes.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/internal/SemConvAttributes.java @@ -25,6 +25,13 @@ private SemConvAttributes() {} AttributeKey.stringKey("otel.component.name"); public static final AttributeKey ERROR_TYPE = AttributeKey.stringKey("error.type"); + public static final AttributeKey OTEL_SPAN_SAMPLING_RESULT = + AttributeKey.stringKey("otel.span.sampling_result"); + + // TODO: Add tests verifying correctness when included in the semconv-incubating release + public static final AttributeKey OTEL_SPAN_PARENT_ORIGIN = + AttributeKey.stringKey("otel.span.parent.origin"); + public static final AttributeKey SERVER_ADDRESS = AttributeKey.stringKey("server.address"); public static final AttributeKey SERVER_PORT = AttributeKey.longKey("server.port"); diff --git a/sdk/common/src/test/java/io/opentelemetry/sdk/internal/SemConvAttributesTest.java b/sdk/common/src/test/java/io/opentelemetry/sdk/internal/SemConvAttributesTest.java index d4a5d5de1f5..1b4df0b84a5 100644 --- a/sdk/common/src/test/java/io/opentelemetry/sdk/internal/SemConvAttributesTest.java +++ b/sdk/common/src/test/java/io/opentelemetry/sdk/internal/SemConvAttributesTest.java @@ -22,6 +22,8 @@ void testAttributeKeys() { .isEqualTo(OtelIncubatingAttributes.OTEL_COMPONENT_NAME); assertThat(SemConvAttributes.OTEL_COMPONENT_TYPE) .isEqualTo(OtelIncubatingAttributes.OTEL_COMPONENT_TYPE); + assertThat(SemConvAttributes.OTEL_SPAN_SAMPLING_RESULT) + .isEqualTo(OtelIncubatingAttributes.OTEL_SPAN_SAMPLING_RESULT); assertThat(SemConvAttributes.ERROR_TYPE).isEqualTo(ErrorAttributes.ERROR_TYPE); diff --git a/sdk/trace/build.gradle.kts b/sdk/trace/build.gradle.kts index 35d30c8513b..e644b724e5b 100644 --- a/sdk/trace/build.gradle.kts +++ b/sdk/trace/build.gradle.kts @@ -32,6 +32,7 @@ dependencies { testImplementation(project(":sdk:testing")) testImplementation("com.google.guava:guava") testImplementation("com.google.guava:guava-testlib") + testImplementation("io.opentelemetry.semconv:opentelemetry-semconv-incubating") jmh(project(":sdk:metrics")) jmh(project(":sdk:testing")) { diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java index 37deab7ffc8..4866587ec9a 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkSpan.java @@ -26,6 +26,7 @@ import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor; +import io.opentelemetry.sdk.trace.internal.SpanInstrumentation; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -99,6 +100,8 @@ final class SdkSpan implements ReadWriteSpan { @GuardedBy("lock") private long endEpochNanos; + private final SpanInstrumentation.Recording metricRecording; + private enum EndState { NOT_ENDED, ENDING, @@ -132,7 +135,8 @@ private SdkSpan( @Nullable AttributesMap attributes, @Nullable List links, int totalRecordedLinks, - long startEpochNanos) { + long startEpochNanos, + SpanInstrumentation.Recording metricRecording) { this.context = context; this.instrumentationScopeInfo = instrumentationScopeInfo; this.parentSpanContext = parentSpanContext; @@ -143,6 +147,7 @@ private SdkSpan( this.spanProcessor = spanProcessor; this.exceptionAttributeResolver = exceptionAttributeResolver; this.resource = resource; + this.metricRecording = metricRecording; this.hasEnded = EndState.NOT_ENDED; this.clock = clock; this.startEpochNanos = startEpochNanos; @@ -180,7 +185,8 @@ static SdkSpan startSpan( @Nullable AttributesMap attributes, @Nullable List links, int totalRecordedLinks, - long userStartEpochNanos) { + long userStartEpochNanos, + SpanInstrumentation.Recording metricsRecording) { boolean createdAnchoredClock; AnchoredClock clock; if (parentSpan instanceof SdkSpan) { @@ -219,7 +225,8 @@ static SdkSpan startSpan( attributes, links, totalRecordedLinks, - startEpochNanos); + startEpochNanos, + metricsRecording); // Call onStart here instead of calling in the constructor to make sure the span is completely // initialized. if (spanProcessor.isStartRequired()) { @@ -557,6 +564,7 @@ private void endInternal(long endEpochNanos) { spanEndingThread = Thread.currentThread(); hasEnded = EndState.ENDING; } + metricRecording.recordSpanEnd(); if (spanProcessor instanceof ExtendedSpanProcessor) { ExtendedSpanProcessor extendedSpanProcessor = (ExtendedSpanProcessor) spanProcessor; if (extendedSpanProcessor.isOnEndingRequired()) { 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 c0f872265ec..e3b25ae7ba6 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 @@ -24,6 +24,7 @@ import io.opentelemetry.sdk.internal.AttributeUtil; import io.opentelemetry.sdk.internal.AttributesMap; import io.opentelemetry.sdk.trace.data.LinkData; +import io.opentelemetry.sdk.trace.internal.SpanInstrumentation; import io.opentelemetry.sdk.trace.samplers.SamplingDecision; import io.opentelemetry.sdk.trace.samplers.SamplingResult; import java.util.ArrayList; @@ -204,7 +205,13 @@ public Span startSpan() { /* remote= */ false, tracerSharedState.isIdGeneratorSafeToSkipIdValidation()); + SpanInstrumentation.Recording metricsRecording = + tracerSharedState.getSpanMetrics().recordSpanStart(samplingResult, parentSpanContext); if (!isRecording(samplingDecision)) { + if (!metricsRecording.isNoop()) { + throw new IllegalStateException( + "instrumentation ending is not supported for non-recording spans."); + } return Span.wrap(spanContext); } Attributes samplingAttributes = samplingResult.getAttributes(); @@ -232,7 +239,8 @@ public Span startSpan() { recordedAttributes, currentLinks, totalNumberOfLinksAdded, - startEpochNanos); + startEpochNanos, + metricsRecording); } private AttributesMap attributes() { diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java index f39ce565731..fc2136e79bf 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProvider.java @@ -5,17 +5,20 @@ package io.opentelemetry.sdk.trace; +import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.TracerBuilder; import io.opentelemetry.api.trace.TracerProvider; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.common.InternalTelemetryVersion; import io.opentelemetry.sdk.internal.ComponentRegistry; import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.internal.SdkTracerProviderUtil; +import io.opentelemetry.sdk.trace.internal.SpanInstrumentation; import io.opentelemetry.sdk.trace.internal.TracerConfig; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.io.Closeable; @@ -54,7 +57,9 @@ public static SdkTracerProviderBuilder builder() { Sampler sampler, List spanProcessors, ScopeConfigurator tracerConfigurator, - ExceptionAttributeResolver exceptionAttributeResolver) { + ExceptionAttributeResolver exceptionAttributeResolver, + InternalTelemetryVersion internalTelemetryVersion, + Supplier meterProviderSupplier) { this.sharedState = new TracerSharedState( clock, @@ -63,7 +68,8 @@ public static SdkTracerProviderBuilder builder() { spanLimitsSupplier, sampler, spanProcessors, - exceptionAttributeResolver); + exceptionAttributeResolver, + SpanInstrumentation.create(internalTelemetryVersion, meterProviderSupplier)); this.tracerSdkComponentRegistry = new ComponentRegistry<>( instrumentationScopeInfo -> diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java index 71f589d15b6..be9c08f6389 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/SdkTracerProviderBuilder.java @@ -7,9 +7,13 @@ import static java.util.Objects.requireNonNull; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.metrics.MeterProvider; import io.opentelemetry.api.trace.Span; import io.opentelemetry.sdk.common.Clock; import io.opentelemetry.sdk.common.InstrumentationScopeInfo; +import io.opentelemetry.sdk.common.InternalTelemetryVersion; import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.internal.ScopeConfigurator; import io.opentelemetry.sdk.internal.ScopeConfiguratorBuilder; @@ -34,6 +38,8 @@ public final class SdkTracerProviderBuilder { private Resource resource = Resource.getDefault(); private Supplier spanLimitsSupplier = SpanLimits::getDefault; private Sampler sampler = DEFAULT_SAMPLER; + private Supplier meterProviderSupplier = GlobalOpenTelemetry::getMeterProvider; + private InternalTelemetryVersion internalTelemetryVersion = InternalTelemetryVersion.LEGACY; private ScopeConfiguratorBuilder tracerConfiguratorBuilder = TracerConfig.configuratorBuilder(); private ExceptionAttributeResolver exceptionAttributeResolver = @@ -176,6 +182,28 @@ public SdkTracerProviderBuilder addSpanProcessorFirst(SpanProcessor spanProcesso return this; } + /** + * Sets the {@link OpenTelemetry} supplier used to collect self-monitoring telemetry. If not set, + * uses {@link GlobalOpenTelemetry#get()}. + */ + public SdkTracerProviderBuilder setInternalTelemetryOpenTelemetry( + Supplier openTelemetrySupplier) { + requireNonNull(openTelemetrySupplier, "openTelemetrySupplier"); + this.meterProviderSupplier = () -> openTelemetrySupplier.get().getMeterProvider(); + return this; + } + + /** + * Sets the {@link InternalTelemetryVersion} defining which self-monitoring metrics the tracers + * originating from this provider collect. + */ + public SdkTracerProviderBuilder setInternalTelemetry( + InternalTelemetryVersion internalTelemetryVersion) { + requireNonNull(internalTelemetryVersion, "internalTelemetryVersion"); + this.internalTelemetryVersion = internalTelemetryVersion; + return this; + } + /** * Set the tracer configurator, which computes {@link TracerConfig} for each {@link * InstrumentationScopeInfo}. @@ -246,7 +274,9 @@ public SdkTracerProvider build() { sampler, spanProcessors, tracerConfiguratorBuilder.build(), - exceptionAttributeResolver); + exceptionAttributeResolver, + internalTelemetryVersion, + meterProviderSupplier); } SdkTracerProviderBuilder() {} diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java index 74d43076b97..5d4834bd3f3 100644 --- a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/TracerSharedState.java @@ -9,6 +9,7 @@ import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.internal.ExceptionAttributeResolver; import io.opentelemetry.sdk.resources.Resource; +import io.opentelemetry.sdk.trace.internal.SpanInstrumentation; import io.opentelemetry.sdk.trace.samplers.Sampler; import java.util.List; import java.util.function.Supplier; @@ -28,6 +29,7 @@ final class TracerSharedState { private final Sampler sampler; private final SpanProcessor activeSpanProcessor; private final ExceptionAttributeResolver exceptionAttributeResolver; + private final SpanInstrumentation spanInstrumentation; @Nullable private volatile CompletableResultCode shutdownResult = null; @@ -38,7 +40,8 @@ final class TracerSharedState { Supplier spanLimitsSupplier, Sampler sampler, List spanProcessors, - ExceptionAttributeResolver exceptionAttributeResolver) { + ExceptionAttributeResolver exceptionAttributeResolver, + SpanInstrumentation spanInstrumentation) { this.clock = clock; this.idGenerator = idGenerator; this.idGeneratorSafeToSkipIdValidation = idGenerator instanceof RandomIdGenerator; @@ -47,6 +50,7 @@ final class TracerSharedState { this.sampler = sampler; this.activeSpanProcessor = SpanProcessor.composite(spanProcessors); this.exceptionAttributeResolver = exceptionAttributeResolver; + this.spanInstrumentation = spanInstrumentation; } Clock getClock() { @@ -84,6 +88,10 @@ SpanProcessor getActiveSpanProcessor() { return activeSpanProcessor; } + SpanInstrumentation getSpanMetrics() { + return spanInstrumentation; + } + /** * Returns {@code true} if tracing has been shut down. * diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/NoopSpanInstrumentation.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/NoopSpanInstrumentation.java new file mode 100644 index 00000000000..f3b3abd22e9 --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/NoopSpanInstrumentation.java @@ -0,0 +1,33 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; + +class NoopSpanInstrumentation implements SpanInstrumentation { + + static final NoopSpanInstrumentation INSTANCE = new NoopSpanInstrumentation(); + + static final SpanInstrumentation.Recording RECORDING_INSTANCE = new NoopRecording(); + + @Override + public SpanInstrumentation.Recording recordSpanStart( + SamplingResult samplingResult, SpanContext parentContext) { + return RECORDING_INSTANCE; + } + + private static class NoopRecording implements SpanInstrumentation.Recording { + + @Override + public boolean isNoop() { + return true; + } + + @Override + public void recordSpanEnd() {} + } +} diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SemConvSpanInstrumentation.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SemConvSpanInstrumentation.java new file mode 100644 index 00000000000..d8c21ad75a7 --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SemConvSpanInstrumentation.java @@ -0,0 +1,144 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.internal.GuardedBy; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongUpDownCounter; +import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.internal.SemConvAttributes; +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.function.Supplier; +import javax.annotation.Nullable; + +class SemConvSpanInstrumentation implements SpanInstrumentation { + + private final Supplier meterProviderSupplier; + + private static final Attributes SAMPLING_DROP_ATTRIBUTES = + Attributes.of(SemConvAttributes.OTEL_SPAN_SAMPLING_RESULT, "DROP"); + private static final Attributes SAMPLING_RECORD_ONLY_ATTRIBUTES = + Attributes.of(SemConvAttributes.OTEL_SPAN_SAMPLING_RESULT, "RECORD_ONLY"); + private static final Attributes SAMPLING_RECORD_AND_SAMPLED_ATTRIBUTES = + Attributes.of(SemConvAttributes.OTEL_SPAN_SAMPLING_RESULT, "RECORD_AND_SAMPLE"); + + @Nullable private volatile LongUpDownCounter live = null; + @Nullable private volatile LongCounter started = null; + + SemConvSpanInstrumentation(Supplier meterProviderSupplier) { + this.meterProviderSupplier = meterProviderSupplier; + } + + private Meter meter() { + MeterProvider meterProvider = meterProviderSupplier.get(); + if (meterProvider == null) { + meterProvider = MeterProvider.noop(); + } + return meterProvider.get("io.opentelemetry.sdk.trace"); + } + + private LongUpDownCounter live() { + LongUpDownCounter live = this.live; + if (live == null) { + live = + meter() + .upDownCounterBuilder("otel.sdk.span.live") + .setUnit("{span}") + .setDescription( + "The number of created spans for which the end operation has not been called yet") + .build(); + this.live = live; + } + return live; + } + + private LongCounter started() { + LongCounter started = this.started; + if (started == null) { + started = + meter() + .counterBuilder("otel.sdk.span.started") + .setUnit("{span}") + .setDescription("The number of created spans") + .build(); + this.started = started; + } + return started; + } + + static Attributes getAttributesForSamplingDecisions(SamplingDecision decision) { + switch (decision) { + case DROP: + return SAMPLING_DROP_ATTRIBUTES; + case RECORD_ONLY: + return SAMPLING_RECORD_ONLY_ATTRIBUTES; + case RECORD_AND_SAMPLE: + return SAMPLING_RECORD_AND_SAMPLED_ATTRIBUTES; + } + throw new IllegalStateException("Unhandled SamplingDecision case: " + decision); + } + + // TODO: Add test verifying attribute values when released in semantic conventions + static String getParentOriginAttributeValue(SpanContext parentSpanContext) { + if (!parentSpanContext.isValid()) { + return "none"; + } else if (parentSpanContext.isRemote()) { + return "remote"; + } else { + return "local"; + } + } + + @Override + public SpanInstrumentation.Recording recordSpanStart( + SamplingResult samplingResult, SpanContext parentSpanContext) { + Attributes samplingResultAttribs = + getAttributesForSamplingDecisions(samplingResult.getDecision()); + Attributes startAttributes = + samplingResultAttribs.toBuilder() + .put( + SemConvAttributes.OTEL_SPAN_PARENT_ORIGIN, + getParentOriginAttributeValue(parentSpanContext)) + .build(); + started().add(1L, startAttributes); + if (samplingResult.getDecision() == SamplingDecision.DROP) { + // Per semconv, otel.sdk.span.live is NOT collected for non-recording spans + return NoopSpanInstrumentation.RECORDING_INSTANCE; + } + live().add(1, samplingResultAttribs); + return new Recording(samplingResultAttribs); + } + + private class Recording implements SpanInstrumentation.Recording { + + private final Attributes attributes; + + @GuardedBy("this") + private boolean endAlreadyReported = false; + + private Recording(Attributes attributes) { + this.attributes = attributes; + } + + @Override + public boolean isNoop() { + return false; + } + + @Override + public synchronized void recordSpanEnd() { + if (endAlreadyReported) { + return; + } + endAlreadyReported = true; + live().add(-1, attributes); + } + } +} diff --git a/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SpanInstrumentation.java b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SpanInstrumentation.java new file mode 100644 index 00000000000..e5e2eb77e32 --- /dev/null +++ b/sdk/trace/src/main/java/io/opentelemetry/sdk/trace/internal/SpanInstrumentation.java @@ -0,0 +1,45 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import io.opentelemetry.api.metrics.MeterProvider; +import io.opentelemetry.api.trace.SpanContext; +import io.opentelemetry.sdk.common.InternalTelemetryVersion; +import io.opentelemetry.sdk.trace.samplers.SamplingResult; +import java.util.function.Supplier; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public interface SpanInstrumentation { + + static SpanInstrumentation create( + InternalTelemetryVersion internalTelemetryVersion, + Supplier meterProviderSupplier) { + switch (internalTelemetryVersion) { + case LEGACY: + return NoopSpanInstrumentation.INSTANCE; + case LATEST: + return new SemConvSpanInstrumentation(meterProviderSupplier); + } + throw new IllegalStateException( + "Unhandled telemetry schema version: " + internalTelemetryVersion); + } + + Recording recordSpanStart(SamplingResult samplingResult, SpanContext parentSpanContext); + + /** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ + interface Recording { + + boolean isNoop(); + + void recordSpanEnd(); + } +} 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 9c9ecdc7e9c..782159826f6 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 @@ -13,11 +13,17 @@ import static io.opentelemetry.api.common.AttributeKey.longKey; import static io.opentelemetry.api.common.AttributeKey.stringArrayKey; import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.semconv.incubating.OtelIncubatingAttributes.OTEL_SPAN_SAMPLING_RESULT; +import static io.opentelemetry.semconv.incubating.OtelIncubatingAttributes.OtelSpanSamplingResultIncubatingValues.DROP; +import static io.opentelemetry.semconv.incubating.OtelIncubatingAttributes.OtelSpanSamplingResultIncubatingValues.RECORD_AND_SAMPLE; +import static io.opentelemetry.semconv.incubating.OtelIncubatingAttributes.OtelSpanSamplingResultIncubatingValues.RECORD_ONLY; import static java.util.Collections.emptyList; import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatCode; +import static org.mockito.Mockito.verifyNoInteractions; +import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Span; @@ -28,18 +34,27 @@ import io.opentelemetry.api.trace.StatusCode; import io.opentelemetry.api.trace.TraceFlags; import io.opentelemetry.api.trace.TraceState; +import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.TracerProvider; import io.opentelemetry.context.Context; import io.opentelemetry.context.Scope; +import io.opentelemetry.sdk.OpenTelemetrySdk; +import io.opentelemetry.sdk.common.InternalTelemetryVersion; +import io.opentelemetry.sdk.metrics.SdkMeterProvider; +import io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions; +import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader; import io.opentelemetry.sdk.trace.data.LinkData; import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.samplers.Sampler; import io.opentelemetry.sdk.trace.samplers.SamplingDecision; import io.opentelemetry.sdk.trace.samplers.SamplingResult; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; import java.util.stream.IntStream; import javax.annotation.Nullable; import org.junit.jupiter.api.BeforeEach; @@ -951,6 +966,226 @@ void parentCurrentSpan_clockIsSame() { } } + @Test + void healthMetricsEnabled() { + AtomicReference currentSamplingDecision = new AtomicReference<>(); + Sampler sampler = + new Sampler() { + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return SamplingResult.create(currentSamplingDecision.get(), Attributes.empty()); + } + + @Override + public String getDescription() { + return "test"; + } + }; + + SpanContext remoteSpanContext = + SpanContext.createFromRemoteParent( + "12345678876543211234567887654321", + "8765432112345678", + TraceFlags.getSampled(), + TraceState.getDefault()); + + Context remoteContext = Context.root().with(Span.wrap(remoteSpanContext)); + Context localContext = Context.root().with(Span.wrap(sampledSpanContext)); + + InMemoryMetricReader inMemoryMetrics = InMemoryMetricReader.create(); + try (SdkMeterProvider meterProvider = + SdkMeterProvider.builder().registerMetricReader(inMemoryMetrics).build()) { + + OpenTelemetrySdk telemetrySdk = + OpenTelemetrySdk.builder().setMeterProvider(meterProvider).build(); + + Tracer tracer = + SdkTracerProvider.builder() + .setSampler(sampler) + .setInternalTelemetryOpenTelemetry(() -> telemetrySdk) + .setInternalTelemetry(InternalTelemetryVersion.LATEST) + .build() + .get("testing"); + + List spansToEnd = new ArrayList<>(); + + currentSamplingDecision.set(SamplingDecision.RECORD_AND_SAMPLE); + spansToEnd.add(tracer.spanBuilder("dropped").startSpan()); + + currentSamplingDecision.set(SamplingDecision.RECORD_ONLY); + spansToEnd.add(tracer.spanBuilder("record_only1").setParent(remoteContext).startSpan()); + spansToEnd.add(tracer.spanBuilder("record_only2").setParent(remoteContext).startSpan()); + + currentSamplingDecision.set(SamplingDecision.DROP); + spansToEnd.add(tracer.spanBuilder("record_and_sample1").setParent(localContext).startSpan()); + spansToEnd.add(tracer.spanBuilder("record_and_sample2").setParent(localContext).startSpan()); + spansToEnd.add(tracer.spanBuilder("record_and_sample3").setParent(localContext).startSpan()); + + assertThat(inMemoryMetrics.collectAllMetrics()) + .hasSize(2) + .anySatisfy( + metric -> + OpenTelemetryAssertions.assertThat(metric) + .hasName("otel.sdk.span.started") + .hasUnit("{span}") + .hasLongSumSatisfying( + ma -> + ma.hasPointsSatisfying( + pa -> + pa.hasAttributes( + Attributes.builder() + .put(OTEL_SPAN_SAMPLING_RESULT, RECORD_AND_SAMPLE) + .put("otel.span.parent.origin", "none") + .build()) + .hasValue(1), + pa -> + pa.hasAttributes( + Attributes.builder() + .put(OTEL_SPAN_SAMPLING_RESULT, RECORD_ONLY) + .put("otel.span.parent.origin", "remote") + .build()) + .hasValue(2), + pa -> + pa.hasAttributes( + Attributes.builder() + .put(OTEL_SPAN_SAMPLING_RESULT, DROP) + .put("otel.span.parent.origin", "local") + .build()) + .hasValue(3)))) + .anySatisfy( + metric -> + OpenTelemetryAssertions.assertThat(metric) + .hasName("otel.sdk.span.live") + .hasUnit("{span}") + .hasLongSumSatisfying( + ma -> + ma.hasPointsSatisfying( + pa -> + pa.hasAttributes( + Attributes.of( + OTEL_SPAN_SAMPLING_RESULT, RECORD_AND_SAMPLE)) + .hasValue(1), + pa -> + pa.hasAttributes( + Attributes.of(OTEL_SPAN_SAMPLING_RESULT, RECORD_ONLY)) + .hasValue(2)))); + + spansToEnd.forEach(Span::end); + Runnable endAssertions = + () -> + assertThat(inMemoryMetrics.collectAllMetrics()) + .hasSize(2) + .anySatisfy( + metric -> + OpenTelemetryAssertions.assertThat(metric) + .hasName("otel.sdk.span.started") + .hasUnit("{span}") + .hasLongSumSatisfying( + ma -> + ma.hasPointsSatisfying( + pa -> + pa.hasAttributes( + Attributes.builder() + .put( + OTEL_SPAN_SAMPLING_RESULT, + RECORD_AND_SAMPLE) + .put("otel.span.parent.origin", "none") + .build()) + .hasValue(1), + pa -> + pa.hasAttributes( + Attributes.builder() + .put( + OTEL_SPAN_SAMPLING_RESULT, + RECORD_ONLY) + .put("otel.span.parent.origin", "remote") + .build()) + .hasValue(2), + pa -> + pa.hasAttributes( + Attributes.builder() + .put(OTEL_SPAN_SAMPLING_RESULT, DROP) + .put("otel.span.parent.origin", "local") + .build()) + .hasValue(3)))) + .anySatisfy( + metric -> + OpenTelemetryAssertions.assertThat(metric) + .hasName("otel.sdk.span.live") + .hasUnit("{span}") + .hasLongSumSatisfying( + ma -> + ma.hasPointsSatisfying( + pa -> + pa.hasAttributes( + Attributes.of( + OTEL_SPAN_SAMPLING_RESULT, + RECORD_AND_SAMPLE)) + .hasValue(0), + pa -> + pa.hasAttributes( + Attributes.of( + OTEL_SPAN_SAMPLING_RESULT, RECORD_ONLY)) + .hasValue(0)))); + endAssertions.run(); + + // ensure double ending doesn't have any effect on the metrics + spansToEnd.forEach(Span::end); + endAssertions.run(); + } + } + + @Test + @SuppressWarnings("unchecked") + void healthMetricsDisabledForLegacy() { + AtomicReference currentSamplingDecision = new AtomicReference<>(); + Sampler sampler = + new Sampler() { + @Override + public SamplingResult shouldSample( + Context parentContext, + String traceId, + String name, + SpanKind spanKind, + Attributes attributes, + List parentLinks) { + return SamplingResult.create(currentSamplingDecision.get(), Attributes.empty()); + } + + @Override + public String getDescription() { + return "test"; + } + }; + + Supplier mockTelemetryProvider = Mockito.mock(Supplier.class); + + Tracer tracer = + SdkTracerProvider.builder() + .setSampler(sampler) + .setInternalTelemetryOpenTelemetry(mockTelemetryProvider) + .setInternalTelemetry(InternalTelemetryVersion.LEGACY) + .build() + .get("testing"); + + currentSamplingDecision.set(SamplingDecision.DROP); + tracer.spanBuilder("dropped").startSpan().end(); + + currentSamplingDecision.set(SamplingDecision.RECORD_ONLY); + tracer.spanBuilder("record_only").startSpan().end(); + + currentSamplingDecision.set(SamplingDecision.RECORD_AND_SAMPLE); + tracer.spanBuilder("record_and_sample").startSpan().end(); + + verifyNoInteractions(mockTelemetryProvider); + } + @Test void isSampled() { assertThat(SdkSpanBuilder.isSampled(SamplingDecision.DROP)).isFalse(); diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java index d5c40fcccbf..25c67b8dbbd 100644 --- a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/SdkSpanTest.java @@ -48,6 +48,7 @@ import io.opentelemetry.sdk.trace.data.SpanData; import io.opentelemetry.sdk.trace.data.StatusData; import io.opentelemetry.sdk.trace.internal.ExtendedSpanProcessor; +import io.opentelemetry.sdk.trace.internal.SpanInstrumentation; import java.io.PrintWriter; import java.io.StringWriter; import java.time.Duration; @@ -103,6 +104,7 @@ class SdkSpanTest { private Attributes expectedAttributes; private final LinkData link = LinkData.create(spanContext); @Mock private ExtendedSpanProcessor spanProcessor; + @Mock private SpanInstrumentation.Recording metricsRecording; private TestClock testClock; @@ -1041,7 +1043,8 @@ void addLink_FaultIn() { null, null, // exercises the fault-in path 0, - 0); + 0, + metricsRecording); SdkSpan linkedSpan = createTestSpan(SpanKind.INTERNAL); span.addLink(linkedSpan.getSpanContext()); @@ -1387,7 +1390,8 @@ void onStartOnEndNotRequired() { spanLimits.getMaxNumberOfAttributes(), spanLimits.getMaxAttributeValueLength()), Collections.emptyList(), 1, - 0); + 0, + metricsRecording); verify(spanProcessor, never()).onStart(any(), any()); span.end(); @@ -1525,7 +1529,8 @@ private SdkSpan createTestSpan( attributes, linksCopy, linksCopy.size(), - 0); + 0, + metricsRecording); Mockito.verify(spanProcessor, Mockito.times(1)).onStart(Context.root(), span); return span; } @@ -1613,7 +1618,8 @@ void testAsSpanData() { attributesWithCapacity, Collections.singletonList(link1), 1, - 0); + 0, + metricsRecording); long startEpochNanos = clock.now(); clock.advance(Duration.ofMillis(4)); long firstEventEpochNanos = clock.now(); diff --git a/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/internal/SemConvSpanInstrumentationTest.java b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/internal/SemConvSpanInstrumentationTest.java new file mode 100644 index 00000000000..5f806c3e6e3 --- /dev/null +++ b/sdk/trace/src/test/java/io/opentelemetry/sdk/trace/internal/SemConvSpanInstrumentationTest.java @@ -0,0 +1,40 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.trace.internal; + +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.assertThat; + +import io.opentelemetry.sdk.trace.samplers.SamplingDecision; +import io.opentelemetry.semconv.incubating.OtelIncubatingAttributes; +import org.junit.jupiter.api.Test; + +class SemConvSpanInstrumentationTest { + + @Test + void verifyAttributesSemConvCompliant() { + assertThat(SemConvSpanInstrumentation.getAttributesForSamplingDecisions(SamplingDecision.DROP)) + .hasSize(1) + .containsEntry( + OtelIncubatingAttributes.OTEL_SPAN_SAMPLING_RESULT, + OtelIncubatingAttributes.OtelSpanSamplingResultIncubatingValues.DROP); + + assertThat( + SemConvSpanInstrumentation.getAttributesForSamplingDecisions( + SamplingDecision.RECORD_AND_SAMPLE)) + .hasSize(1) + .containsEntry( + OtelIncubatingAttributes.OTEL_SPAN_SAMPLING_RESULT, + OtelIncubatingAttributes.OtelSpanSamplingResultIncubatingValues.RECORD_AND_SAMPLE); + + assertThat( + SemConvSpanInstrumentation.getAttributesForSamplingDecisions( + SamplingDecision.RECORD_ONLY)) + .hasSize(1) + .containsEntry( + OtelIncubatingAttributes.OTEL_SPAN_SAMPLING_RESULT, + OtelIncubatingAttributes.OtelSpanSamplingResultIncubatingValues.RECORD_ONLY); + } +}