diff --git a/.fossa.yml b/.fossa.yml index 92ff822c1296..30e9ef493d8c 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -103,6 +103,9 @@ targets: - type: gradle path: ./ target: ':instrumentation:external-annotations:javaagent' + - type: gradle + path: ./ + target: ':instrumentation:failsafe-3.0:javaagent' - type: gradle path: ./ target: ':instrumentation:failsafe-3.0:library' diff --git a/instrumentation/failsafe-3.0/javaagent/build.gradle.kts b/instrumentation/failsafe-3.0/javaagent/build.gradle.kts new file mode 100644 index 000000000000..3c1efd9f7384 --- /dev/null +++ b/instrumentation/failsafe-3.0/javaagent/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + id("otel.javaagent-instrumentation") +} + +muzzle { + pass { + group.set("dev.failsafe") + module.set("failsafe") + versions.set("[3.0.0,)") + assertInverse.set(true) + } +} + +dependencies { + library("dev.failsafe:failsafe:3.0.1") + + implementation(project(":instrumentation:failsafe-3.0:library")) + + testImplementation(project(":instrumentation:failsafe-3.0:testing")) +} + +tasks { + withType().configureEach { + jvmArgs("-Dotel.instrumentation.failsafe.enabled=true") + } +} diff --git a/instrumentation/failsafe-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/failsafe/v3_0/FailsafeInstrumentationModule.java b/instrumentation/failsafe-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/failsafe/v3_0/FailsafeInstrumentationModule.java new file mode 100644 index 000000000000..9a5cd8196c3a --- /dev/null +++ b/instrumentation/failsafe-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/failsafe/v3_0/FailsafeInstrumentationModule.java @@ -0,0 +1,38 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.failsafe.v3_0; + +import static java.util.Collections.singletonList; + +import com.google.auto.service.AutoService; +import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule; +import java.util.List; + +@AutoService(InstrumentationModule.class) +public class FailsafeInstrumentationModule extends InstrumentationModule + implements ExperimentalInstrumentationModule { + + public FailsafeInstrumentationModule() { + super("failsafe", "failsafe-3.0"); + } + + @Override + public List typeInstrumentations() { + return singletonList(new RetryPolicyInstrumentation()); + } + + @Override + public boolean defaultEnabled() { + return false; + } + + @Override + public boolean isIndyReady() { + return true; + } +} diff --git a/instrumentation/failsafe-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/failsafe/v3_0/RetryPolicyInstrumentation.java b/instrumentation/failsafe-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/failsafe/v3_0/RetryPolicyInstrumentation.java new file mode 100644 index 000000000000..2c2f483cf0a7 --- /dev/null +++ b/instrumentation/failsafe-3.0/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/failsafe/v3_0/RetryPolicyInstrumentation.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.failsafe.v3_0; + +import static io.opentelemetry.instrumentation.failsafe.v3_0.internal.RetryPolicyEventListenerBuilders.buildInstrumentedFailureListener; +import static io.opentelemetry.instrumentation.failsafe.v3_0.internal.RetryPolicyEventListenerBuilders.buildInstrumentedSuccessListener; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesNoArguments; + +import dev.failsafe.PolicyConfig; +import dev.failsafe.internal.RetryPolicyImpl; +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; +import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; +import java.lang.reflect.Field; +import javax.annotation.Nullable; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public final class RetryPolicyInstrumentation implements TypeInstrumentation { + @Override + public ElementMatcher typeMatcher() { + return named("dev.failsafe.RetryPolicyBuilder"); + } + + @Override + public void transform(TypeTransformer transformer) { + transformer.applyAdviceToMethod( + named("build").and(takesNoArguments()), this.getClass().getName() + "$BuildAdvice"); + } + + public static final class BuildAdvice { + @Nullable public static final Field FAILURE_LISTENER_FIELD; + @Nullable public static final Field SUCCESS_LISTENER_FIELD; + + static { + Field failureListenerField = null; + Field successListenerField = null; + try { + failureListenerField = PolicyConfig.class.getDeclaredField("failureListener"); + failureListenerField.setAccessible(true); + + successListenerField = PolicyConfig.class.getDeclaredField("successListener"); + successListenerField.setAccessible(true); + } catch (Exception e) { + // Ignored + } + FAILURE_LISTENER_FIELD = failureListenerField; + SUCCESS_LISTENER_FIELD = successListenerField; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void onExit(@Advice.Return Object retryPolicyImpl) { + if (FAILURE_LISTENER_FIELD == null || SUCCESS_LISTENER_FIELD == null) { + return; + } + + try { + RetryPolicyImpl impl = (RetryPolicyImpl) retryPolicyImpl; + FAILURE_LISTENER_FIELD.set( + impl.getConfig(), + buildInstrumentedFailureListener( + GlobalOpenTelemetry.get(), impl.getConfig(), impl.toString())); + SUCCESS_LISTENER_FIELD.set( + impl.getConfig(), + buildInstrumentedSuccessListener( + GlobalOpenTelemetry.get(), impl.getConfig(), impl.toString())); + } catch (IllegalAccessException ignored) { + // Ignored + } + } + } +} diff --git a/instrumentation/failsafe-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/failsafe/v3_0/FailsafeInstrumentationTest.java b/instrumentation/failsafe-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/failsafe/v3_0/FailsafeInstrumentationTest.java new file mode 100644 index 000000000000..c0737b0f696e --- /dev/null +++ b/instrumentation/failsafe-3.0/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/failsafe/v3_0/FailsafeInstrumentationTest.java @@ -0,0 +1,46 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.failsafe.v3_0; + +import dev.failsafe.CircuitBreaker; +import dev.failsafe.RetryPolicy; +import io.opentelemetry.instrumentation.failsafe.AbstractFailsafeInstrumentationTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class FailsafeInstrumentationTest extends AbstractFailsafeInstrumentationTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected CircuitBreaker configure(CircuitBreaker userCircuitBreaker) { + return userCircuitBreaker; + } + + @Override + protected RetryPolicy configure(RetryPolicy userRetryPolicy) { + return userRetryPolicy; + } + + @Override + @Disabled + public void captureCircuitBreakerMetrics() { + // TODO: Will be enabled once Java agent CircuitBreaker instrumentation is added. + } + + @Test + void captureRetryPolicyMetrics() { + captureRetryPolicyMetrics(null); + } +} diff --git a/instrumentation/failsafe-3.0/library/build.gradle.kts b/instrumentation/failsafe-3.0/library/build.gradle.kts index 58bbd7499a1e..423c3dea1eaa 100644 --- a/instrumentation/failsafe-3.0/library/build.gradle.kts +++ b/instrumentation/failsafe-3.0/library/build.gradle.kts @@ -4,6 +4,8 @@ plugins { dependencies { library("dev.failsafe:failsafe:3.0.1") + + testImplementation(project(":instrumentation:failsafe-3.0:testing")) } tasks.test { diff --git a/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetry.java b/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetry.java index 91e85cd440ef..3fb7bcd76c0e 100644 --- a/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetry.java +++ b/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetry.java @@ -10,7 +10,6 @@ import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedHalfOpenListener; import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedOpenListener; import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedSuccessListener; -import static java.util.Arrays.asList; import dev.failsafe.CircuitBreaker; import dev.failsafe.CircuitBreakerConfig; @@ -20,16 +19,14 @@ import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.metrics.LongCounter; -import io.opentelemetry.api.metrics.LongHistogram; import io.opentelemetry.api.metrics.Meter; +import io.opentelemetry.instrumentation.failsafe.v3_0.internal.RetryPolicyEventListenerBuilders; /** Entrypoint for instrumenting Failsafe components. */ public final class FailsafeTelemetry { private static final String INSTRUMENTATION_NAME = "io.opentelemetry.failsafe-3.0"; private static final AttributeKey CIRCUIT_BREAKER_NAME = AttributeKey.stringKey("failsafe.circuit_breaker.name"); - private static final AttributeKey RETRY_POLICY_NAME = - AttributeKey.stringKey("failsafe.retry_policy.name"); private final OpenTelemetry openTelemetry; @@ -86,31 +83,13 @@ public CircuitBreaker createCircuitBreaker( */ public RetryPolicy createRetryPolicy(RetryPolicy delegate, String retryPolicyName) { RetryPolicyConfig userConfig = delegate.getConfig(); - Meter meter = openTelemetry.getMeter(INSTRUMENTATION_NAME); - LongCounter executionCounter = - meter - .counterBuilder("failsafe.retry_policy.execution.count") - .setDescription( - "Count of execution attempts processed by the retry policy, " - + "where one execution represents the total number of attempts.") - .setUnit("{execution}") - .build(); - LongHistogram attemptsHistogram = - meter - .histogramBuilder("failsafe.retry_policy.attempts") - .setDescription("Number of attempts for each execution.") - .setUnit("{attempt}") - .ofLongs() - .setExplicitBucketBoundariesAdvice(asList(1L, 2L, 3L, 5L)) - .build(); - Attributes attributes = Attributes.of(RETRY_POLICY_NAME, retryPolicyName); return RetryPolicy.builder(userConfig) .onFailure( RetryPolicyEventListenerBuilders.buildInstrumentedFailureListener( - userConfig, executionCounter, attemptsHistogram, attributes)) + openTelemetry, userConfig, retryPolicyName)) .onSuccess( RetryPolicyEventListenerBuilders.buildInstrumentedSuccessListener( - userConfig, executionCounter, attemptsHistogram, attributes)) + openTelemetry, userConfig, retryPolicyName)) .build(); } } diff --git a/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/RetryPolicyEventListenerBuilders.java b/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/RetryPolicyEventListenerBuilders.java deleted file mode 100644 index d62b12a9cdea..000000000000 --- a/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/RetryPolicyEventListenerBuilders.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.failsafe.v3_0; - -import static io.opentelemetry.api.common.AttributeKey.stringKey; - -import dev.failsafe.RetryPolicyConfig; -import dev.failsafe.event.EventListener; -import dev.failsafe.event.ExecutionCompletedEvent; -import io.opentelemetry.api.common.AttributeKey; -import io.opentelemetry.api.common.Attributes; -import io.opentelemetry.api.metrics.LongCounter; -import io.opentelemetry.api.metrics.LongHistogram; -import javax.annotation.Nullable; - -final class RetryPolicyEventListenerBuilders { - private static final AttributeKey OUTCOME_KEY = - stringKey("failsafe.retry_policy.outcome"); - - private RetryPolicyEventListenerBuilders() {} - - static EventListener> buildInstrumentedFailureListener( - RetryPolicyConfig userConfig, - LongCounter executionCounter, - LongHistogram attemptsHistogram, - Attributes commonAttributes) { - Attributes attributes = commonAttributes.toBuilder().put(OUTCOME_KEY, "failure").build(); - return countAndRecord( - executionCounter, attemptsHistogram, attributes, userConfig.getFailureListener()); - } - - static EventListener> buildInstrumentedSuccessListener( - RetryPolicyConfig userConfig, - LongCounter executionCounter, - LongHistogram attemptsHistogram, - Attributes commonAttributes) { - Attributes attributes = commonAttributes.toBuilder().put(OUTCOME_KEY, "success").build(); - return countAndRecord( - executionCounter, attemptsHistogram, attributes, userConfig.getSuccessListener()); - } - - private static EventListener> countAndRecord( - LongCounter executionCounter, - LongHistogram attemptsHistogram, - Attributes attributes, - @Nullable EventListener> delegate) { - return e -> { - executionCounter.add(1, attributes); - attemptsHistogram.record(e.getAttemptCount(), attributes); - if (delegate != null) { - delegate.accept(e); - } - }; - } -} diff --git a/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/internal/RetryPolicyEventListenerBuilders.java b/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/internal/RetryPolicyEventListenerBuilders.java new file mode 100644 index 000000000000..93a71d9cc52f --- /dev/null +++ b/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/internal/RetryPolicyEventListenerBuilders.java @@ -0,0 +1,85 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.failsafe.v3_0.internal; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static java.util.Arrays.asList; + +import dev.failsafe.RetryPolicyConfig; +import dev.failsafe.event.EventListener; +import dev.failsafe.event.ExecutionCompletedEvent; +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.common.AttributeKey; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.api.metrics.LongCounter; +import io.opentelemetry.api.metrics.LongHistogram; + +/** + * This class is internal and is hence not for public use. Its APIs are unstable and can change at + * any time. + */ +public final class RetryPolicyEventListenerBuilders { + private static final String INSTRUMENTATION_NAME = "io.opentelemetry.failsafe-3.0"; + + private static final AttributeKey OUTCOME_KEY = + stringKey("failsafe.retry_policy.outcome"); + private static final AttributeKey RETRY_POLICY_NAME = + AttributeKey.stringKey("failsafe.retry_policy.name"); + + private RetryPolicyEventListenerBuilders() {} + + public static EventListener> buildInstrumentedFailureListener( + OpenTelemetry openTelemetry, RetryPolicyConfig userConfig, String policyName) { + LongCounter executionCounter = buildRetryPolicyExecutionCounter(openTelemetry); + LongHistogram attemptsHistogram = buildRetryPolicyAttemptsHistogram(openTelemetry); + Attributes attributes = Attributes.of(RETRY_POLICY_NAME, policyName, OUTCOME_KEY, "failure"); + EventListener> userFailureListener = userConfig.getFailureListener(); + return e -> { + executionCounter.add(1, attributes); + attemptsHistogram.record(e.getAttemptCount(), attributes); + if (userFailureListener != null) { + userFailureListener.accept(e); + } + }; + } + + public static EventListener> buildInstrumentedSuccessListener( + OpenTelemetry openTelemetry, RetryPolicyConfig userConfig, String policyName) { + LongCounter executionCounter = buildRetryPolicyExecutionCounter(openTelemetry); + LongHistogram attemptsHistogram = buildRetryPolicyAttemptsHistogram(openTelemetry); + Attributes attributes = Attributes.of(RETRY_POLICY_NAME, policyName, OUTCOME_KEY, "success"); + EventListener> userSuccessListener = userConfig.getSuccessListener(); + return e -> { + executionCounter.add(1, attributes); + attemptsHistogram.record(e.getAttemptCount(), attributes); + if (userSuccessListener != null) { + userSuccessListener.accept(e); + } + }; + } + + private static LongCounter buildRetryPolicyExecutionCounter(OpenTelemetry openTelemetry) { + return openTelemetry + .getMeter(INSTRUMENTATION_NAME) + .counterBuilder("failsafe.retry_policy.execution.count") + .setDescription( + "Count of execution attempts processed by the retry policy, " + + "where one execution represents the total number of attempts.") + .setUnit("{execution}") + .build(); + } + + private static LongHistogram buildRetryPolicyAttemptsHistogram(OpenTelemetry openTelemetry) { + return openTelemetry + .getMeter(INSTRUMENTATION_NAME) + .histogramBuilder("failsafe.retry_policy.attempts") + .setDescription("Number of attempts for each execution.") + .setUnit("{attempt}") + .ofLongs() + .setExplicitBucketBoundariesAdvice(asList(1L, 2L, 3L, 5L)) + .build(); + } +} diff --git a/instrumentation/failsafe-3.0/library/src/test/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetryTest.java b/instrumentation/failsafe-3.0/library/src/test/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetryTest.java index f937bcaa21cd..82cc56653db6 100644 --- a/instrumentation/failsafe-3.0/library/src/test/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetryTest.java +++ b/instrumentation/failsafe-3.0/library/src/test/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetryTest.java @@ -5,105 +5,100 @@ package io.opentelemetry.instrumentation.failsafe.v3_0; -import static io.opentelemetry.api.common.AttributeKey.stringKey; -import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; -import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import dev.failsafe.CircuitBreaker; -import dev.failsafe.CircuitBreakerOpenException; -import dev.failsafe.Failsafe; import dev.failsafe.RetryPolicy; +import dev.failsafe.RetryPolicyConfig; +import dev.failsafe.event.EventListener; +import dev.failsafe.event.ExecutionCompletedEvent; +import io.opentelemetry.instrumentation.failsafe.AbstractFailsafeInstrumentationTest; +import io.opentelemetry.instrumentation.failsafe.v3_0.internal.RetryPolicyEventListenerBuilders; import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; -import io.opentelemetry.sdk.testing.assertj.LongPointAssert; -import java.time.Duration; import java.util.Objects; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -class FailsafeTelemetryTest { +class FailsafeTelemetryTest extends AbstractFailsafeInstrumentationTest { @RegisterExtension static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + @Override + protected InstrumentationExtension testing() { + return testing; + } + + @Override + protected CircuitBreaker configure(CircuitBreaker userCircuitBreaker) { + FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(testing.getOpenTelemetry()); + return failsafeTelemetry.createCircuitBreaker(userCircuitBreaker, "testing"); + } + + @Override + protected RetryPolicy configure(RetryPolicy userRetryPolicy) { + FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(testing.getOpenTelemetry()); + return failsafeTelemetry.createRetryPolicy(userRetryPolicy, "testing"); + } + @Test - void captureCircuitBreakerMetrics() { + void captureRetryPolicyMetrics() { + captureRetryPolicyMetrics("testing"); + } + + @Test + @SuppressWarnings("unchecked") + void createInstrumentedFailureListener() throws Throwable { // given - CircuitBreaker userCircuitBreaker = - CircuitBreaker.builder() + RetryPolicyConfig delegate = + RetryPolicy.builder() .handleResultIf(Objects::isNull) - .withFailureThreshold(2) - .withDelay(Duration.ZERO) - .withSuccessThreshold(2) - .build(); - FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(testing.getOpenTelemetry()); - CircuitBreaker instrumentedCircuitBreaker = - failsafeTelemetry.createCircuitBreaker(userCircuitBreaker, "testing"); + .withMaxAttempts(3) + .build() + .getConfig(); + String retryPolicyName = "testing"; // when - for (int i = 0; i < 5; i++) { - try { - int temp = i; - Failsafe.with(instrumentedCircuitBreaker).get(() -> temp < 2 ? null : new Object()); - } catch (CircuitBreakerOpenException ignored) { - assertThat(i).isEqualTo(2); - } - } + EventListener> actual = + RetryPolicyEventListenerBuilders.buildInstrumentedFailureListener( + testing.getOpenTelemetry(), delegate, retryPolicyName); + ExecutionCompletedEvent event = mock(ExecutionCompletedEvent.class); + when(event.getAttemptCount()).thenReturn(1); + actual.accept(event); // then testing.waitAndAssertMetrics( "io.opentelemetry.failsafe-3.0", metricAssert -> metricAssert - .hasName("failsafe.circuit_breaker.execution.count") - .hasLongSumSatisfying( - sum -> - sum.isMonotonic() - .hasPointsSatisfying( - buildCircuitBreakerAssertion( - 2, "failsafe.circuit_breaker.outcome", "failure"), - buildCircuitBreakerAssertion( - 3, "failsafe.circuit_breaker.outcome", "success"))), - metricAssert -> - metricAssert - .hasName("failsafe.circuit_breaker.state_change.count") + .hasName("failsafe.retry_policy.execution.count") .hasLongSumSatisfying( sum -> sum.isMonotonic() .hasPointsSatisfying( - buildCircuitBreakerAssertion( - 1, "failsafe.circuit_breaker.state", "open"), - buildCircuitBreakerAssertion( - 1, "failsafe.circuit_breaker.state", "half_open"), - buildCircuitBreakerAssertion( - 1, "failsafe.circuit_breaker.state", "closed")))); + buildRetryPolicyAssertion(1, retryPolicyName, "failure")))); } @Test - void captureRetryPolicyMetrics() { + @SuppressWarnings("unchecked") + void createInstrumentedSuccessListener() throws Throwable { // given - RetryPolicy userRetryPolicy = - RetryPolicy.builder().handleResultIf(Objects::isNull).withMaxAttempts(3).build(); - FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(testing.getOpenTelemetry()); - RetryPolicy instrumentedRetryPolicy = - failsafeTelemetry.createRetryPolicy(userRetryPolicy, "testing"); + RetryPolicyConfig delegate = + RetryPolicy.builder() + .handleResultIf(Objects::isNull) + .withMaxAttempts(3) + .build() + .getConfig(); + String retryPolicyName = "testing"; // when - for (int i = 0; i <= 4; i++) { - int temp = i; - AtomicInteger retry = new AtomicInteger(0); - Failsafe.with(instrumentedRetryPolicy) - .get( - () -> { - if (retry.get() < temp) { - retry.incrementAndGet(); - return null; - } else { - return new Object(); - } - }); - } + EventListener> actual = + RetryPolicyEventListenerBuilders.buildInstrumentedSuccessListener( + testing.getOpenTelemetry(), delegate, retryPolicyName); + ExecutionCompletedEvent event = mock(ExecutionCompletedEvent.class); + when(event.getAttemptCount()).thenReturn(1); + actual.accept(event); // then testing.waitAndAssertMetrics( @@ -115,53 +110,6 @@ void captureRetryPolicyMetrics() { sum -> sum.isMonotonic() .hasPointsSatisfying( - buildRetryPolicyAssertion(2, "failure"), - buildRetryPolicyAssertion(3, "success"))), - metricAssert -> - metricAssert - .hasName("failsafe.retry_policy.attempts") - .hasHistogramSatisfying( - histogramAssert -> - histogramAssert.hasPointsSatisfying( - histogramPointAssert -> - histogramPointAssert - .hasCount(3) - .hasMin(1) - .hasMax(3) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("failsafe.retry_policy.name"), "testing"), - equalTo( - stringKey("failsafe.retry_policy.outcome"), "success")) - .hasBucketCounts(1L, 1L, 1L, 0L, 0L), - histogramPointAssert -> - histogramPointAssert - .hasCount(2) - .hasMin(3) - .hasMax(3) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("failsafe.retry_policy.name"), "testing"), - equalTo( - stringKey("failsafe.retry_policy.outcome"), "failure")) - .hasBucketCounts(0L, 0L, 2L, 0L, 0L)))); - } - - private static Consumer buildCircuitBreakerAssertion( - long expectedValue, String expectedAttributeKey, String expectedAttributeValue) { - return longSumAssert -> - longSumAssert - .hasValue(expectedValue) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("failsafe.circuit_breaker.name"), "testing"), - equalTo(stringKey(expectedAttributeKey), expectedAttributeValue)); - } - - private static Consumer buildRetryPolicyAssertion( - long expectedValue, String expectedOutcomeValue) { - return longSumAssert -> - longSumAssert - .hasValue(expectedValue) - .hasAttributesSatisfyingExactly( - equalTo(stringKey("failsafe.retry_policy.name"), "testing"), - equalTo(stringKey("failsafe.retry_policy.outcome"), expectedOutcomeValue)); + buildRetryPolicyAssertion(1, retryPolicyName, "success")))); } } diff --git a/instrumentation/failsafe-3.0/testing/build.gradle.kts b/instrumentation/failsafe-3.0/testing/build.gradle.kts new file mode 100644 index 000000000000..bc9ac028a1dd --- /dev/null +++ b/instrumentation/failsafe-3.0/testing/build.gradle.kts @@ -0,0 +1,11 @@ +plugins { + id("otel.java-conventions") +} + +dependencies { + api("io.opentelemetry.javaagent:opentelemetry-testing-common") + api("org.mockito:mockito-core") + api("org.mockito:mockito-junit-jupiter") + + compileOnly("dev.failsafe:failsafe:3.0.1") +} diff --git a/instrumentation/failsafe-3.0/testing/src/main/java/io/opentelemetry/instrumentation/failsafe/AbstractFailsafeInstrumentationTest.java b/instrumentation/failsafe-3.0/testing/src/main/java/io/opentelemetry/instrumentation/failsafe/AbstractFailsafeInstrumentationTest.java new file mode 100644 index 000000000000..77c95ca0b74e --- /dev/null +++ b/instrumentation/failsafe-3.0/testing/src/main/java/io/opentelemetry/instrumentation/failsafe/AbstractFailsafeInstrumentationTest.java @@ -0,0 +1,178 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.failsafe; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static org.assertj.core.api.Assertions.assertThat; + +import dev.failsafe.CircuitBreaker; +import dev.failsafe.CircuitBreakerOpenException; +import dev.failsafe.Failsafe; +import dev.failsafe.RetryPolicy; +import io.opentelemetry.api.common.Attributes; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.LongPointAssert; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import javax.annotation.Nullable; +import org.junit.jupiter.api.Test; + +public abstract class AbstractFailsafeInstrumentationTest { + protected abstract InstrumentationExtension testing(); + + protected abstract CircuitBreaker configure(CircuitBreaker userCircuitBreaker); + + protected abstract RetryPolicy configure(RetryPolicy userRetryPolicy); + + @Test + public void captureCircuitBreakerMetrics() { + // given + CircuitBreaker userCircuitBreaker = + CircuitBreaker.builder() + .handleResult(null) + .withFailureThreshold(2) + .withDelay(Duration.ZERO) + .withSuccessThreshold(2) + .build(); + CircuitBreaker instrumentedCircuitBreaker = configure(userCircuitBreaker); + + // when + for (int i = 0; i < 5; i++) { + try { + int temp = i; + Failsafe.with(instrumentedCircuitBreaker).get(() -> temp < 2 ? null : new Object()); + } catch (CircuitBreakerOpenException e) { + assertThat(i).isEqualTo(2); + } + } + + // then + testing() + .waitAndAssertMetrics( + "io.opentelemetry.failsafe-3.0", + metricAssert -> + metricAssert + .hasName("failsafe.circuit_breaker.execution.count") + .hasLongSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + buildCircuitBreakerAssertion( + 2, "failsafe.circuit_breaker.outcome", "failure"), + buildCircuitBreakerAssertion( + 3, "failsafe.circuit_breaker.outcome", "success"))), + metricAssert -> + metricAssert + .hasName("failsafe.circuit_breaker.state_change.count") + .hasLongSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + buildCircuitBreakerAssertion( + 1, "failsafe.circuit_breaker.state", "open"), + buildCircuitBreakerAssertion( + 1, "failsafe.circuit_breaker.state", "half_open"), + buildCircuitBreakerAssertion( + 1, "failsafe.circuit_breaker.state", "closed")))); + } + + protected void captureRetryPolicyMetrics(@Nullable String expectedPolicyName) { + RetryPolicy userRetryPolicy = + dev.failsafe.RetryPolicy.builder().handleResult(null).withMaxAttempts(3).build(); + RetryPolicy instrumentedRetryPolicy = configure(userRetryPolicy); + captureRetryPolicyMetrics( + instrumentedRetryPolicy, + expectedPolicyName != null ? expectedPolicyName : instrumentedRetryPolicy.toString()); + } + + private void captureRetryPolicyMetrics( + RetryPolicy instrumentedRetryPolicy, String expectedPolicyName) { + // given + + // when + for (int i = 0; i <= 4; i++) { + int temp = i; + AtomicInteger retry = new AtomicInteger(0); + Failsafe.with(instrumentedRetryPolicy) + .get( + () -> { + if (retry.get() < temp) { + retry.incrementAndGet(); + return null; + } else { + return new Object(); + } + }); + } + + // then + testing() + .waitAndAssertMetrics( + "io.opentelemetry.failsafe-3.0", + metricAssert -> + metricAssert + .hasName("failsafe.retry_policy.execution.count") + .hasLongSumSatisfying( + sum -> + sum.isMonotonic() + .hasPointsSatisfying( + buildRetryPolicyAssertion(2, expectedPolicyName, "failure"), + buildRetryPolicyAssertion(3, expectedPolicyName, "success"))), + metricAssert -> + metricAssert + .hasName("failsafe.retry_policy.attempts") + .hasHistogramSatisfying( + histogramAssert -> + histogramAssert.hasPointsSatisfying( + histogramPointAssert -> + histogramPointAssert + .hasCount(3) + .hasMin(1) + .hasMax(3) + .hasAttributes( + buildExpectedRetryPolicyAttributes( + expectedPolicyName, "success")) + .hasBucketCounts(1L, 1L, 1L, 0L, 0L), + histogramPointAssert -> + histogramPointAssert + .hasCount(2) + .hasMin(3) + .hasMax(3) + .hasAttributes( + buildExpectedRetryPolicyAttributes( + expectedPolicyName, "failure")) + .hasBucketCounts(0L, 0L, 2L, 0L, 0L)))); + } + + protected static Consumer buildRetryPolicyAssertion( + long expectedValue, String expectedPolicyName, String expectedOutcomeValue) { + return longSumAssert -> + longSumAssert + .hasValue(expectedValue) + .hasAttributes( + buildExpectedRetryPolicyAttributes(expectedPolicyName, expectedOutcomeValue)); + } + + private static Consumer buildCircuitBreakerAssertion( + long expectedValue, String expectedAttributeKey, String expectedAttributeValue) { + return longSumAssert -> + longSumAssert + .hasValue(expectedValue) + .hasAttributesSatisfyingExactly( + equalTo(stringKey("failsafe.circuit_breaker.name"), "testing"), + equalTo(stringKey(expectedAttributeKey), expectedAttributeValue)); + } + + private static Attributes buildExpectedRetryPolicyAttributes( + String expectedPolicyName, String expectedOutcome) { + return Attributes.builder() + .put("failsafe.retry_policy.name", expectedPolicyName) + .put("failsafe.retry_policy.outcome", expectedOutcome) + .build(); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index a34bc80d9759..45ac8a2ad4d6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -287,6 +287,8 @@ include(":instrumentation:executors:jdk21-testing") include(":instrumentation:executors:testing") include(":instrumentation:external-annotations:javaagent") include(":instrumentation:external-annotations:javaagent-unit-tests") +include(":instrumentation:failsafe-3.0:javaagent") +include(":instrumentation:failsafe-3.0:testing") include(":instrumentation:failsafe-3.0:library") include(":instrumentation:finagle-http-23.11:javaagent") include(":instrumentation:finatra-2.9:javaagent")