End span with attribute on cancellation of Guava future
MODIFY
instrumentation/guava-10.0/javaagent/guava-10.0-javaagent.gradle
instrumentation/guava-10.0/javaagent/guava-10.0-javaagent.gradle
@@ -9,6 +9,11 @@ muzzle {
}
}
+tasks.withType(Test).configureEach {
+ // TODO run tests both with and without experimental span attributes
+ jvmArgs "-Dotel.instrumentation.guava.experimental-span-attributes=true"
+}
+
dependencies {
library "com.google.guava:guava:10.0"

MODIFY
instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/InstrumentationHelper.java
instrumentation/guava-10.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/guava/InstrumentationHelper.java
package io.opentelemetry.javaagent.instrumentation.guava;
+import io.opentelemetry.instrumentation.api.config.Config;
import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategies;
import io.opentelemetry.instrumentation.guava.GuavaAsyncSpanEndStrategy;
public final class InstrumentationHelper {
static {
- AsyncSpanEndStrategies.getInstance().registerStrategy(GuavaAsyncSpanEndStrategy.INSTANCE);
+ registerAsyncSpanEndStrategy();
+ }
+
+ private static void registerAsyncSpanEndStrategy() {
+ AsyncSpanEndStrategies.getInstance()
+ .registerStrategy(
+ GuavaAsyncSpanEndStrategy.newBuilder()
+ .setCaptureExperimentalSpanAttributes(
+ Config.get()
+ .getBooleanProperty(
+ "otel.instrumentation.guava.experimental-span-attributes", false))
+ .build());
}
/**

MODIFY
instrumentation/guava-10.0/javaagent/src/test/groovy/GuavaWithSpanInstrumentationTest.groovy
instrumentation/guava-10.0/javaagent/src/test/groovy/GuavaWithSpanInstrumentationTest.groovy
@@ -101,4 +101,29 @@ class GuavaWithSpanInstrumentationTest extends AgentInstrumentationSpecification
}
}
}
+
+ def "should capture span for canceled ListenableFuture"() {
+ setup:
+ def future = SettableFuture.<String>create()
+ new TracedWithSpan().listenableFuture(future)
+
+ expect:
+ Thread.sleep(500) // sleep a bit just to make sure no span is captured
+ assertTraces(0) {}
+
+ future.cancel(true)
+
+ assertTraces(1) {
+ trace(0, 1) {
+ span(0) {
+ name "TracedWithSpan.listenableFuture"
+ kind SpanKind.INTERNAL
+ hasNoParent()
+ attributes {
+ "guava.canceled" true
+ }
+ }
+ }
+ }
+ }
}

MODIFY
instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/GuavaAsyncSpanEndStrategy.java
instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/GuavaAsyncSpanEndStrategy.java
@@ -7,12 +7,29 @@ package io.opentelemetry.instrumentation.guava;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.Uninterruptibles;
+import io.opentelemetry.api.common.AttributeKey;
+import io.opentelemetry.api.trace.Span;
import io.opentelemetry.context.Context;
import io.opentelemetry.instrumentation.api.tracer.BaseTracer;
import io.opentelemetry.instrumentation.api.tracer.async.AsyncSpanEndStrategy;
-public enum GuavaAsyncSpanEndStrategy implements AsyncSpanEndStrategy {
- INSTANCE;
+public final class GuavaAsyncSpanEndStrategy implements AsyncSpanEndStrategy {
+ private static final AttributeKey<Boolean> CANCELED_ATTRIBUTE_KEY =
+ AttributeKey.booleanKey("guava.canceled");
+
+ public static GuavaAsyncSpanEndStrategy create() {
+ return newBuilder().build();
+ }
+
+ public static GuavaAsyncSpanEndStrategyBuilder newBuilder() {
+ return new GuavaAsyncSpanEndStrategyBuilder();
+ }
+
+ private final boolean captureExperimentalSpanAttributes;
+
+ GuavaAsyncSpanEndStrategy(boolean captureExperimentalSpanAttributes) {
+ this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes;
+ }
@Override
public boolean supports(Class<?> returnType) {
@@ -30,7 +47,13 @@ public enum GuavaAsyncSpanEndStrategy implements AsyncSpanEndStrategy {
return future;
}
- private static void endSpan(BaseTracer tracer, Context context, ListenableFuture<?> future) {
+ private void endSpan(BaseTracer tracer, Context context, ListenableFuture<?> future) {
+ if (future.isCancelled()) {
+ if (captureExperimentalSpanAttributes) {
+ Span.fromContext(context).setAttribute(CANCELED_ATTRIBUTE_KEY, true);
+ }
+ tracer.end(context);
+ } else {
try {
Uninterruptibles.getUninterruptibly(future);
tracer.end(context);
@@ -39,3 +62,4 @@ public enum GuavaAsyncSpanEndStrategy implements AsyncSpanEndStrategy {
}
}
}
+}

ADD
instrumentation/guava-10.0/library/src/main/java/io/opentelemetry/instrumentation/guava/GuavaAsyncSpanEndStrategyBuilder.java
+/*
+ * Copyright The OpenTelemetry Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package io.opentelemetry.instrumentation.guava;
+
+public final class GuavaAsyncSpanEndStrategyBuilder {
+ private boolean captureExperimentalSpanAttributes = false;
+
+ GuavaAsyncSpanEndStrategyBuilder() {}
+
+ public GuavaAsyncSpanEndStrategyBuilder setCaptureExperimentalSpanAttributes(
+ boolean captureExperimentalSpanAttributes) {
+ this.captureExperimentalSpanAttributes = captureExperimentalSpanAttributes;
+ return this;
+ }
+
+ public GuavaAsyncSpanEndStrategy build() {
+ return new GuavaAsyncSpanEndStrategy(captureExperimentalSpanAttributes);
+ }
+}

MODIFY
instrumentation/guava-10.0/library/src/test/groovy/GuavaAsyncSpanEndStrategyTest.groovy
instrumentation/guava-10.0/library/src/test/groovy/GuavaAsyncSpanEndStrategyTest.groovy
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture
+import io.opentelemetry.api.trace.Span
import io.opentelemetry.context.Context
import io.opentelemetry.instrumentation.api.tracer.BaseTracer
import io.opentelemetry.instrumentation.guava.GuavaAsyncSpanEndStrategy
@@ -16,11 +17,19 @@ class GuavaAsyncSpanEndStrategyTest extends Specification {
Context context
- def underTest = GuavaAsyncSpanEndStrategy.INSTANCE
+ Span span
+
+ def underTest = GuavaAsyncSpanEndStrategy.create()
+
+ def underTestWithExperimentalAttributes = GuavaAsyncSpanEndStrategy.newBuilder()
+ .setCaptureExperimentalSpanAttributes(true)
+ .build()
void setup() {
tracer = Mock()
context = Mock()
+ span = Mock()
+ span.storeInContext(_) >> { callRealMethod() }
}
def "ListenableFuture is supported"() {
@@ -86,4 +95,42 @@ class GuavaAsyncSpanEndStrategyTest extends Specification {
then:
1 * tracer.endExceptionally(context, { it.getCause() == exception })
}
+
+ def "ends span on eventually canceled future"() {
+ given:
+ def future = SettableFuture.<String>create()
+ def context = span.storeInContext(Context.root())
+
+ when:
+ underTest.end(tracer, context, future)
+
+ then:
+ 0 * tracer._
+
+ when:
+ future.cancel(true)
+
+ then:
+ 1 * tracer.end(context)
+ 0 * span.setAttribute(_)
+ }
+
+ def "ends span on eventually canceled future and capturing experimental span attributes"() {
+ given:
+ def future = SettableFuture.<String>create()
+ def context = span.storeInContext(Context.root())
+
+ when:
+ underTestWithExperimentalAttributes.end(tracer, context, future)
+
+ then:
+ 0 * tracer._
+
+ when:
+ future.cancel(true)
+
+ then:
+ 1 * tracer.end(context)
+ 1 * span.setAttribute({ it.getKey() == "guava.canceled" }, true)
+ }
}

End span with attribute on cancellation of Guava future (#3175)