Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .fossa.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
26 changes: 26 additions & 0 deletions instrumentation/failsafe-3.0/javaagent/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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<Test>().configureEach {
jvmArgs("-Dotel.instrumentation.failsafe.enabled=true")
}
}
Original file line number Diff line number Diff line change
@@ -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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a convention that instrumentations that don't follow any semantic conventions are disabled by default

implements ExperimentalInstrumentationModule {

public FailsafeInstrumentationModule() {
super("failsafe", "failsafe-3.0");
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new RetryPolicyInstrumentation());
}

@Override
public boolean defaultEnabled() {
return false;
}

@Override
public boolean isIndyReady() {
return true;
}
}
Original file line number Diff line number Diff line change
@@ -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<TypeDescription> 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Style] The caught exception is intentionally unused, so the preferred name is ignored (the // Ignored comment can be removed — the name itself conveys intent and IntelliJ's CatchMayIgnoreException inspection recognizes it).

Suggested change
successListenerField.setAccessible(true);
} catch (Exception ignored) {
// field layout differs from the expected Failsafe 3.0 layout
}

} catch (Exception e) {
// Ignored
}
FAILURE_LISTENER_FIELD = failureListenerField;
SUCCESS_LISTENER_FIELD = successListenerField;
}

@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Javaagent] Return-only exit advice should omit onThrowable. This advice only processes @Advice.Return (it has no @Advice.Enter state to clean up). When build() throws, retryPolicyImpl is null and the cast on L62 triggers a (suppressed) ClassCastException/NullPointerException for no benefit. Keep suppress = Throwable.class but drop onThrowable.

Suggested change
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
@Advice.OnMethodExit(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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Javaagent] This instrumentation mutates private fields of dev.failsafe.PolicyConfig (failureListener, successListener) via reflection. Muzzle cannot detect field renames/removals in future Failsafe versions, so a breaking change upstream will silently disable only the field-lookup static block (best case) or silently swap in listeners that clobber something unexpected (worst case). Consider:

  • Adding a comment documenting the tight coupling to the Failsafe 3.0 field layout.
  • Tightening the muzzle range (failsafe:[3.0,4) with a fail block for 4.0+) to force an explicit revisit when Failsafe 4 lands.

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()));
Comment on lines +67 to +71

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[General] Using impl.toString() as the retry-policy name will produce values like dev.failsafe.internal.RetryPolicyImpl@1a2b3c4d for the failsafe.retry_policy.name metric attribute — high-cardinality and not human-meaningful. Since RetryPolicyBuilder does not expose a user-supplied name, consider either omitting the name attribute when none was provided, or using a stable placeholder such as the simple class name. Also, GlobalOpenTelemetry.get() is called twice — a single local would be clearer.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @trask, I proposed a solution regarding this above, let me know if it makes sense.

} catch (IllegalAccessException ignored) {
// Ignored
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Object> configure(CircuitBreaker<Object> userCircuitBreaker) {
return userCircuitBreaker;
}

@Override
protected RetryPolicy<Object> configure(RetryPolicy<Object> userRetryPolicy) {
return userRetryPolicy;
}

@Override
@Disabled
public void captureCircuitBreakerMetrics() {
// TODO: Will be enabled once Java agent CircuitBreaker instrumentation is added.
}

@Test
void captureRetryPolicyMetrics() {
captureRetryPolicyMetrics(null);
}
}
2 changes: 2 additions & 0 deletions instrumentation/failsafe-3.0/library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ plugins {

dependencies {
library("dev.failsafe:failsafe:3.0.1")

testImplementation(project(":instrumentation:failsafe-3.0:testing"))
}

tasks.test {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<String> CIRCUIT_BREAKER_NAME =
AttributeKey.stringKey("failsafe.circuit_breaker.name");
private static final AttributeKey<String> RETRY_POLICY_NAME =
AttributeKey.stringKey("failsafe.retry_policy.name");

private final OpenTelemetry openTelemetry;

Expand Down Expand Up @@ -86,31 +83,13 @@ public <R> CircuitBreaker<R> createCircuitBreaker(
*/
public <R> RetryPolicy<R> createRetryPolicy(RetryPolicy<R> delegate, String retryPolicyName) {
RetryPolicyConfig<R> 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();
}
}

This file was deleted.

Loading
Loading