-
Notifications
You must be signed in to change notification settings - Fork 986
Failsafe 3.0 instrumentation introduced #14057
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
463deb6
d450563
55f2c79
69a4ecb
bf6c982
8fd8249
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
# Library Instrumentation for Failsafe version 3.0.1 and higher | ||
|
||
Provides OpenTelemetry instrumentation for [Failsafe](https://failsafe.dev/). | ||
|
||
## Quickstart | ||
|
||
### Add these dependencies to your project | ||
|
||
Replace `OPENTELEMETRY_VERSION` with the [latest release](https://search.maven.org/search?q=g:io.opentelemetry.instrumentation%20AND%20a:opentelemetry-failsafe-3.0). | ||
|
||
For Maven, add to your `pom.xml` dependencies: | ||
|
||
```xml | ||
<dependencies> | ||
<dependency> | ||
<groupId>io.opentelemetry.instrumentation</groupId> | ||
<artifactId>opentelemetry-failsafe-3.0</artifactId> | ||
<version>OPENTELEMETRY_VERSION</version> | ||
</dependency> | ||
</dependencies> | ||
``` | ||
|
||
For Gradle, add to your dependencies: | ||
|
||
```groovy | ||
implementation("io.opentelemetry.instrumentation:opentelemetry-failsafe-3.0:OPENTELEMETRY_VERSION") | ||
``` | ||
|
||
### Usage | ||
|
||
The instrumentation library allows creating instrumented `CircuitBreaker` instances for collecting | ||
OpenTelemetry-based metrics. | ||
|
||
```java | ||
<R> CircuitBreaker<R> configure(OpenTelemetry openTelemetry, CircuitBreaker<R> circuitBreaker) { | ||
FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(openTelemetry); | ||
return failsafeTelemetry.createCircuitBreaker(circuitBreaker, "my-circuit-breaker"); | ||
} | ||
``` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
plugins { | ||
laurit marked this conversation as resolved.
Show resolved
Hide resolved
|
||
id("otel.library-instrumentation") | ||
} | ||
|
||
dependencies { | ||
library("dev.failsafe:failsafe:3.0.1") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @trask is it ok to name the instrumentation There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, I think it's better to keep the name of the instrumentation as |
||
|
||
testImplementation(project(":testing-common")) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.failsafe.v3_0; | ||
|
||
import dev.failsafe.CircuitBreakerConfig; | ||
import dev.failsafe.event.CircuitBreakerStateChangedEvent; | ||
import dev.failsafe.event.EventListener; | ||
import dev.failsafe.event.ExecutionCompletedEvent; | ||
import io.opentelemetry.api.common.Attributes; | ||
import io.opentelemetry.api.metrics.LongCounter; | ||
import io.opentelemetry.api.metrics.Meter; | ||
|
||
final class CircuitBreakerEventListenerBuilders { | ||
private CircuitBreakerEventListenerBuilders() {} | ||
|
||
static <R> EventListener<ExecutionCompletedEvent<R>> buildInstrumentedFailureListener( | ||
CircuitBreakerConfig<R> userConfig, Meter meter, Attributes attributes) { | ||
LongCounter failureCounter = | ||
meter | ||
.counterBuilder("failsafe.circuitbreaker.failure.count") | ||
.setDescription("Count of failed circuit breaker executions.") | ||
.build(); | ||
EventListener<ExecutionCompletedEvent<R>> failureListener = userConfig.getFailureListener(); | ||
return e -> { | ||
failureCounter.add(1, attributes); | ||
if (failureListener != null) { | ||
failureListener.accept(e); | ||
} | ||
}; | ||
} | ||
|
||
static <R> EventListener<ExecutionCompletedEvent<R>> buildInstrumentedSuccessListener( | ||
CircuitBreakerConfig<R> userConfig, Meter meter, Attributes attributes) { | ||
LongCounter successCounter = | ||
meter | ||
.counterBuilder("failsafe.circuitbreaker.success.count") | ||
.setDescription("Count of successful circuit breaker executions.") | ||
.build(); | ||
EventListener<ExecutionCompletedEvent<R>> successListener = userConfig.getSuccessListener(); | ||
return e -> { | ||
successCounter.add(1, attributes); | ||
if (successListener != null) { | ||
successListener.accept(e); | ||
} | ||
}; | ||
} | ||
|
||
static <R> EventListener<CircuitBreakerStateChangedEvent> buildInstrumentedOpenListener( | ||
CircuitBreakerConfig<R> userConfig, Meter meter, Attributes attributes) { | ||
LongCounter openCircuitBreakerCounter = | ||
meter | ||
.counterBuilder("failsafe.circuitbreaker.open.count") | ||
.setDescription("Count of times that circuit breaker was opened.") | ||
.build(); | ||
EventListener<CircuitBreakerStateChangedEvent> openListener = userConfig.getOpenListener(); | ||
return e -> { | ||
openCircuitBreakerCounter.add(1, attributes); | ||
openListener.accept(e); | ||
}; | ||
} | ||
|
||
static <R> EventListener<CircuitBreakerStateChangedEvent> buildInstrumentedHalfOpenListener( | ||
CircuitBreakerConfig<R> userConfig, Meter meter, Attributes attributes) { | ||
LongCounter halfOpenCircuitBreakerCounter = | ||
meter | ||
.counterBuilder("failsafe.circuitbreaker.halfopen.count") | ||
.setDescription("Count of times that circuit breaker was half-opened.") | ||
.build(); | ||
EventListener<CircuitBreakerStateChangedEvent> halfOpenListener = | ||
userConfig.getHalfOpenListener(); | ||
return e -> { | ||
halfOpenCircuitBreakerCounter.add(1, attributes); | ||
halfOpenListener.accept(e); | ||
}; | ||
} | ||
|
||
static <R> EventListener<CircuitBreakerStateChangedEvent> buildInstrumentedCloseListener( | ||
CircuitBreakerConfig<R> userConfig, Meter meter, Attributes attributes) { | ||
LongCounter closedCircuitBreakerCounter = | ||
meter | ||
.counterBuilder("failsafe.circuitbreaker.closed.count") | ||
.setDescription("Count of times that circuit breaker was closed.") | ||
.build(); | ||
EventListener<CircuitBreakerStateChangedEvent> closeListener = userConfig.getCloseListener(); | ||
return e -> { | ||
closedCircuitBreakerCounter.add(1, attributes); | ||
closeListener.accept(e); | ||
}; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.failsafe.v3_0; | ||
|
||
import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedCloseListener; | ||
import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedFailureListener; | ||
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 dev.failsafe.CircuitBreaker; | ||
import dev.failsafe.CircuitBreakerConfig; | ||
import io.opentelemetry.api.OpenTelemetry; | ||
import io.opentelemetry.api.common.AttributeKey; | ||
import io.opentelemetry.api.common.Attributes; | ||
import io.opentelemetry.api.metrics.Meter; | ||
|
||
/** Entrypoint for instrumenting Failsafe components. */ | ||
public final class FailsafeTelemetry { | ||
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.failsafe-3.0"; | ||
|
||
/** Returns a new {@link FailsafeTelemetry} configured with the given {@link OpenTelemetry}. */ | ||
public static FailsafeTelemetry create(OpenTelemetry openTelemetry) { | ||
return new FailsafeTelemetry(openTelemetry); | ||
} | ||
|
||
private final OpenTelemetry openTelemetry; | ||
|
||
private FailsafeTelemetry(OpenTelemetry openTelemetry) { | ||
this.openTelemetry = openTelemetry; | ||
} | ||
|
||
/** | ||
* Returns an instrumented {@link CircuitBreaker} by given values. | ||
* | ||
* @param delegate user configured {@link CircuitBreaker} to be instrumented | ||
* @param circuitBreakerName identifier of given {@link CircuitBreaker} | ||
* @param <R> {@link CircuitBreaker}'s result type | ||
* @return instrumented {@link CircuitBreaker} | ||
*/ | ||
public <R> CircuitBreaker<R> createCircuitBreaker( | ||
CircuitBreaker<R> delegate, String circuitBreakerName) { | ||
CircuitBreakerConfig<R> userConfig = delegate.getConfig(); | ||
Meter meter = openTelemetry.getMeter(INSTRUMENTATION_NAME); | ||
Attributes attributes = Attributes.of(AttributeKey.stringKey("name"), circuitBreakerName); | ||
return CircuitBreaker.builder(userConfig) | ||
.onFailure(buildInstrumentedFailureListener(userConfig, meter, attributes)) | ||
.onSuccess(buildInstrumentedSuccessListener(userConfig, meter, attributes)) | ||
.onOpen(buildInstrumentedOpenListener(userConfig, meter, attributes)) | ||
.onHalfOpen(buildInstrumentedHalfOpenListener(userConfig, meter, attributes)) | ||
.onClose(buildInstrumentedCloseListener(userConfig, meter, attributes)) | ||
.build(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
/* | ||
* Copyright The OpenTelemetry Authors | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
package io.opentelemetry.instrumentation.failsafe.v3_0; | ||
|
||
import static org.hamcrest.CoreMatchers.equalTo; | ||
import static org.hamcrest.MatcherAssert.assertThat; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
|
||
import dev.failsafe.CircuitBreakerOpenException; | ||
import dev.failsafe.Failsafe; | ||
import io.opentelemetry.api.common.AttributeKey; | ||
import io.opentelemetry.api.common.Attributes; | ||
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; | ||
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; | ||
import io.opentelemetry.sdk.metrics.data.LongPointData; | ||
import io.opentelemetry.sdk.metrics.data.MetricData; | ||
import io.opentelemetry.sdk.testing.assertj.MetricAssert; | ||
import java.time.Duration; | ||
import java.util.Objects; | ||
import org.junit.jupiter.api.Nested; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.extension.RegisterExtension; | ||
|
||
final class FailsafeTelemetryTest { | ||
@RegisterExtension | ||
static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); | ||
|
||
@Nested | ||
final class CircuitBreaker { | ||
@Test | ||
void should_Capture_CircuitBreaker_Metrics() { | ||
// given | ||
dev.failsafe.CircuitBreaker<Object> userCircuitBreaker = | ||
dev.failsafe.CircuitBreaker.builder() | ||
.handleResultIf(Objects::isNull) | ||
.withFailureThreshold(2) | ||
.withDelay(Duration.ZERO) | ||
.withSuccessThreshold(2) | ||
.build(); | ||
FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(testing.getOpenTelemetry()); | ||
dev.failsafe.CircuitBreaker<Object> instrumentedCircuitBreaker = | ||
failsafeTelemetry.createCircuitBreaker(userCircuitBreaker, "testing"); | ||
|
||
// when | ||
for (int i = 0; i < 5; i++) { | ||
try { | ||
int temp = i; | ||
Failsafe.with(instrumentedCircuitBreaker) | ||
.get( | ||
() -> { | ||
if (temp < 2) { | ||
return null; | ||
} else { | ||
return new Object(); | ||
} | ||
}); | ||
} catch (CircuitBreakerOpenException e) { | ||
assertThat(i, equalTo(2)); | ||
} | ||
} | ||
|
||
// then | ||
testing.waitAndAssertMetrics( | ||
"io.opentelemetry.failsafe-3.0", | ||
metricAssert -> | ||
assertCircuitBreakerMetric(metricAssert, "failsafe.circuitbreaker.failure.count", 2), | ||
metricAssert -> | ||
assertCircuitBreakerMetric(metricAssert, "failsafe.circuitbreaker.success.count", 3), | ||
metricAssert -> | ||
assertCircuitBreakerMetric(metricAssert, "failsafe.circuitbreaker.open.count", 1), | ||
metricAssert -> | ||
assertCircuitBreakerMetric(metricAssert, "failsafe.circuitbreaker.halfopen.count", 1), | ||
metricAssert -> | ||
assertCircuitBreakerMetric(metricAssert, "failsafe.circuitbreaker.closed.count", 1)); | ||
} | ||
} | ||
|
||
private static void assertCircuitBreakerMetric( | ||
MetricAssert metricAssert, String counterName, long expectedValue) { | ||
MetricData closeCountData = metricAssert.actual(); | ||
assertThat(closeCountData.getName(), equalTo(counterName)); | ||
assertTrue(closeCountData.getData().getPoints().stream().findFirst().isPresent()); | ||
LongPointData closeCountLongPointData = | ||
(LongPointData) closeCountData.getData().getPoints().stream().findFirst().get(); | ||
assertThat( | ||
closeCountLongPointData.getAttributes(), | ||
equalTo(Attributes.of(AttributeKey.stringKey("name"), "testing"))); | ||
assertThat(closeCountLongPointData.getValue(), equalTo(expectedValue)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -224,6 +224,7 @@ 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:library") | ||
include(":instrumentation:finagle-http-23.11:javaagent") | ||
include(":instrumentation:finatra-2.9:javaagent") | ||
include(":instrumentation:geode-1.4:javaagent") | ||
|
@@ -626,7 +627,6 @@ include(":instrumentation:xxl-job:xxl-job-2.3.0:javaagent") | |
include(":instrumentation:xxl-job:xxl-job-common:javaagent") | ||
include(":instrumentation:xxl-job:xxl-job-common:testing") | ||
include(":instrumentation:zio:zio-2.0:javaagent") | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should keep this empty line as separator. |
||
// benchmark | ||
include(":benchmark-overhead-jmh") | ||
include(":benchmark-jfr-analyzer") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@trask I think this might be the first library only instrumentation we have. Do we need to point this out somehow here? Set the
Auto-instrumented versions
toN/A
? Any suggestions?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
N/A
in the auto-instrumented versions column sounds good to me 👍