Skip to content

Commit 8eea435

Browse files
onurkybsilaurit
andauthored
Failsafe 3.0 instrumentation introduced (#14057)
Co-authored-by: Lauri Tulmin <[email protected]>
1 parent 6fc2783 commit 8eea435

File tree

8 files changed

+295
-0
lines changed

8 files changed

+295
-0
lines changed

.fossa.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ targets:
100100
- type: gradle
101101
path: ./
102102
target: ':instrumentation:external-annotations:javaagent'
103+
- type: gradle
104+
path: ./
105+
target: ':instrumentation:failsafe-3.0:library'
103106
- type: gradle
104107
path: ./
105108
target: ':instrumentation:finagle-http-23.11:javaagent'

docs/supported-libraries.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ These are the supported libraries and frameworks:
6666
| [Elasticsearch API Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html) | 7.16 - 7.17.19,<br>8.0 - 8.9.+ [4] | N/A | [Elasticsearch Client Spans] |
6767
| [Elasticsearch REST Client](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/index.html) | 5.0+ | N/A | [Database Client Spans], [Database Client Metrics]&nbsp;[6] |
6868
| [Elasticsearch Transport Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/index.html) | 5.0+ | N/A | [Database Client Spans], [Database Client Metrics]&nbsp;[6] |
69+
| [Failsafe](https://failsafe.dev/) | N/A | [opentelemetry-failsafe-3.0](../instrumentation/failsafe-3.0/library) | none |
6970
| [Finagle](https://github.com/twitter/finagle) | 23.11+ | N/A | none |
7071
| [Finatra](https://github.com/twitter/finatra) | 2.9+ | N/A | Provides `http.route` [2], Controller Spans [3] |
7172
| [Geode Client](https://geode.apache.org/) | 1.4+ | N/A | [Database Client Spans], [Database Client Metrics]&nbsp;[6] |
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Library Instrumentation for Failsafe version 3.0.1 and higher
2+
3+
Provides OpenTelemetry instrumentation for [Failsafe](https://failsafe.dev/).
4+
5+
## Quickstart
6+
7+
### Add these dependencies to your project
8+
9+
Replace `OPENTELEMETRY_VERSION` with the [latest release](https://central.sonatype.com/artifact/io.opentelemetry.instrumentation/opentelemetry-failsafe-3.0).
10+
11+
For Maven, add to your `pom.xml` dependencies:
12+
13+
```xml
14+
<dependencies>
15+
<dependency>
16+
<groupId>io.opentelemetry.instrumentation</groupId>
17+
<artifactId>opentelemetry-failsafe-3.0</artifactId>
18+
<version>OPENTELEMETRY_VERSION</version>
19+
</dependency>
20+
</dependencies>
21+
```
22+
23+
For Gradle, add to your dependencies:
24+
25+
```groovy
26+
implementation("io.opentelemetry.instrumentation:opentelemetry-failsafe-3.0:OPENTELEMETRY_VERSION")
27+
```
28+
29+
### Usage
30+
31+
The instrumentation library allows creating instrumented `CircuitBreaker` instances for collecting
32+
OpenTelemetry-based metrics.
33+
34+
```java
35+
<R> CircuitBreaker<R> configure(OpenTelemetry openTelemetry, CircuitBreaker<R> circuitBreaker) {
36+
FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(openTelemetry);
37+
return failsafeTelemetry.createCircuitBreaker(circuitBreaker, "my-circuit-breaker");
38+
}
39+
```
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
plugins {
2+
id("otel.library-instrumentation")
3+
}
4+
5+
dependencies {
6+
library("dev.failsafe:failsafe:3.0.1")
7+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.failsafe.v3_0;
7+
8+
import static io.opentelemetry.api.common.AttributeKey.stringKey;
9+
10+
import dev.failsafe.CircuitBreakerConfig;
11+
import dev.failsafe.event.CircuitBreakerStateChangedEvent;
12+
import dev.failsafe.event.EventListener;
13+
import dev.failsafe.event.ExecutionCompletedEvent;
14+
import io.opentelemetry.api.common.AttributeKey;
15+
import io.opentelemetry.api.common.Attributes;
16+
import io.opentelemetry.api.metrics.LongCounter;
17+
18+
final class CircuitBreakerEventListenerBuilders {
19+
private static final AttributeKey<String> OUTCOME_KEY =
20+
stringKey("failsafe.circuit_breaker.outcome");
21+
private static final AttributeKey<String> STATE_KEY = stringKey("failsafe.circuit_breaker.state");
22+
23+
private CircuitBreakerEventListenerBuilders() {}
24+
25+
static <R> EventListener<ExecutionCompletedEvent<R>> buildInstrumentedFailureListener(
26+
CircuitBreakerConfig<R> userConfig,
27+
LongCounter executionCounter,
28+
Attributes commonAttributes) {
29+
Attributes attributes = commonAttributes.toBuilder().put(OUTCOME_KEY, "failure").build();
30+
return count(executionCounter, attributes, userConfig.getFailureListener());
31+
}
32+
33+
static <R> EventListener<ExecutionCompletedEvent<R>> buildInstrumentedSuccessListener(
34+
CircuitBreakerConfig<R> userConfig,
35+
LongCounter executionCounter,
36+
Attributes commonAttributes) {
37+
Attributes attributes = commonAttributes.toBuilder().put(OUTCOME_KEY, "success").build();
38+
return count(executionCounter, attributes, userConfig.getSuccessListener());
39+
}
40+
41+
static <R> EventListener<CircuitBreakerStateChangedEvent> buildInstrumentedOpenListener(
42+
CircuitBreakerConfig<R> userConfig,
43+
LongCounter stateChangesCounter,
44+
Attributes commonAttributes) {
45+
Attributes attributes = commonAttributes.toBuilder().put(STATE_KEY, "open").build();
46+
return count(stateChangesCounter, attributes, userConfig.getOpenListener());
47+
}
48+
49+
static <R> EventListener<CircuitBreakerStateChangedEvent> buildInstrumentedHalfOpenListener(
50+
CircuitBreakerConfig<R> userConfig,
51+
LongCounter stateChangesCounter,
52+
Attributes commonAttributes) {
53+
Attributes attributes = commonAttributes.toBuilder().put(STATE_KEY, "half_open").build();
54+
return count(stateChangesCounter, attributes, userConfig.getHalfOpenListener());
55+
}
56+
57+
static <R> EventListener<CircuitBreakerStateChangedEvent> buildInstrumentedCloseListener(
58+
CircuitBreakerConfig<R> userConfig,
59+
LongCounter stateChangesCounter,
60+
Attributes commonAttributes) {
61+
Attributes attributes = commonAttributes.toBuilder().put(STATE_KEY, "closed").build();
62+
return count(stateChangesCounter, attributes, userConfig.getCloseListener());
63+
}
64+
65+
private static <T> EventListener<T> count(
66+
LongCounter counter, Attributes attributes, EventListener<T> delegate) {
67+
return e -> {
68+
counter.add(1, attributes);
69+
if (delegate != null) {
70+
delegate.accept(e);
71+
}
72+
};
73+
}
74+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.failsafe.v3_0;
7+
8+
import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedCloseListener;
9+
import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedFailureListener;
10+
import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedHalfOpenListener;
11+
import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedOpenListener;
12+
import static io.opentelemetry.instrumentation.failsafe.v3_0.CircuitBreakerEventListenerBuilders.buildInstrumentedSuccessListener;
13+
14+
import dev.failsafe.CircuitBreaker;
15+
import dev.failsafe.CircuitBreakerConfig;
16+
import io.opentelemetry.api.OpenTelemetry;
17+
import io.opentelemetry.api.common.AttributeKey;
18+
import io.opentelemetry.api.common.Attributes;
19+
import io.opentelemetry.api.metrics.LongCounter;
20+
import io.opentelemetry.api.metrics.Meter;
21+
22+
/** Entrypoint for instrumenting Failsafe components. */
23+
public final class FailsafeTelemetry {
24+
private static final String INSTRUMENTATION_NAME = "io.opentelemetry.failsafe-3.0";
25+
26+
private static final AttributeKey<String> CIRCUIT_BREAKER_NAME =
27+
AttributeKey.stringKey("failsafe.circuit_breaker.name");
28+
29+
/** Returns a new {@link FailsafeTelemetry} configured with the given {@link OpenTelemetry}. */
30+
public static FailsafeTelemetry create(OpenTelemetry openTelemetry) {
31+
return new FailsafeTelemetry(openTelemetry);
32+
}
33+
34+
private final OpenTelemetry openTelemetry;
35+
36+
private FailsafeTelemetry(OpenTelemetry openTelemetry) {
37+
this.openTelemetry = openTelemetry;
38+
}
39+
40+
/**
41+
* Returns an instrumented {@link CircuitBreaker} by given values.
42+
*
43+
* @param delegate user configured {@link CircuitBreaker} to be instrumented
44+
* @param circuitBreakerName identifier of given {@link CircuitBreaker}
45+
* @param <R> {@link CircuitBreaker}'s result type
46+
* @return instrumented {@link CircuitBreaker}
47+
*/
48+
public <R> CircuitBreaker<R> createCircuitBreaker(
49+
CircuitBreaker<R> delegate, String circuitBreakerName) {
50+
CircuitBreakerConfig<R> userConfig = delegate.getConfig();
51+
Meter meter = openTelemetry.getMeter(INSTRUMENTATION_NAME);
52+
LongCounter executionCounter =
53+
meter
54+
.counterBuilder("failsafe.circuit_breaker.execution.count")
55+
.setDescription("Count of circuit breaker executions.")
56+
.setUnit("{execution}")
57+
.build();
58+
LongCounter stateChangesCounter =
59+
meter
60+
.counterBuilder("failsafe.circuit_breaker.state_changes.count")
61+
.setDescription("Count of circuit breaker state changes.")
62+
.setUnit("{execution}")
63+
.build();
64+
Attributes attributes = Attributes.of(CIRCUIT_BREAKER_NAME, circuitBreakerName);
65+
return CircuitBreaker.builder(userConfig)
66+
.onFailure(buildInstrumentedFailureListener(userConfig, executionCounter, attributes))
67+
.onSuccess(buildInstrumentedSuccessListener(userConfig, executionCounter, attributes))
68+
.onOpen(buildInstrumentedOpenListener(userConfig, stateChangesCounter, attributes))
69+
.onHalfOpen(buildInstrumentedHalfOpenListener(userConfig, stateChangesCounter, attributes))
70+
.onClose(buildInstrumentedCloseListener(userConfig, stateChangesCounter, attributes))
71+
.build();
72+
}
73+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright The OpenTelemetry Authors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package io.opentelemetry.instrumentation.failsafe.v3_0;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
11+
import dev.failsafe.CircuitBreaker;
12+
import dev.failsafe.CircuitBreakerOpenException;
13+
import dev.failsafe.Failsafe;
14+
import io.opentelemetry.api.common.Attributes;
15+
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
16+
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
17+
import io.opentelemetry.sdk.testing.assertj.LongPointAssert;
18+
import java.time.Duration;
19+
import java.util.Objects;
20+
import java.util.function.Consumer;
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.RegisterExtension;
23+
24+
final class FailsafeTelemetryTest {
25+
@RegisterExtension
26+
static final InstrumentationExtension testing = LibraryInstrumentationExtension.create();
27+
28+
@Test
29+
void captureCircuitBreakerMetrics() {
30+
// given
31+
CircuitBreaker<Object> userCircuitBreaker =
32+
dev.failsafe.CircuitBreaker.builder()
33+
.handleResultIf(Objects::isNull)
34+
.withFailureThreshold(2)
35+
.withDelay(Duration.ZERO)
36+
.withSuccessThreshold(2)
37+
.build();
38+
FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(testing.getOpenTelemetry());
39+
CircuitBreaker<Object> instrumentedCircuitBreaker =
40+
failsafeTelemetry.createCircuitBreaker(userCircuitBreaker, "testing");
41+
42+
// when
43+
for (int i = 0; i < 5; i++) {
44+
try {
45+
int temp = i;
46+
Failsafe.with(instrumentedCircuitBreaker).get(() -> temp < 2 ? null : new Object());
47+
} catch (CircuitBreakerOpenException e) {
48+
assertThat(i).isEqualTo(2);
49+
}
50+
}
51+
52+
// then
53+
testing.waitAndAssertMetrics(
54+
"io.opentelemetry.failsafe-3.0",
55+
metricAssert ->
56+
metricAssert
57+
.hasName("failsafe.circuit_breaker.execution.count")
58+
.hasLongSumSatisfying(
59+
sum ->
60+
sum.isMonotonic()
61+
.hasPointsSatisfying(
62+
buildCircuitBreakerAssertion(
63+
2, "failsafe.circuit_breaker.outcome", "failure"),
64+
buildCircuitBreakerAssertion(
65+
3, "failsafe.circuit_breaker.outcome", "success"))));
66+
testing.waitAndAssertMetrics(
67+
"io.opentelemetry.failsafe-3.0",
68+
metricAssert ->
69+
metricAssert
70+
.hasName("failsafe.circuit_breaker.state_changes.count")
71+
.hasLongSumSatisfying(
72+
sum ->
73+
sum.isMonotonic()
74+
.hasPointsSatisfying(
75+
buildCircuitBreakerAssertion(
76+
1, "failsafe.circuit_breaker.state", "open"),
77+
buildCircuitBreakerAssertion(
78+
1, "failsafe.circuit_breaker.state", "half_open"),
79+
buildCircuitBreakerAssertion(
80+
1, "failsafe.circuit_breaker.state", "closed"))));
81+
}
82+
83+
private static Consumer<LongPointAssert> buildCircuitBreakerAssertion(
84+
long expectedValue, String expectedAttributeKey, String expectedAttributeValue) {
85+
return longSumAssert ->
86+
longSumAssert
87+
.hasValue(expectedValue)
88+
.hasAttributesSatisfying(
89+
attributes ->
90+
assertEquals(
91+
Attributes.builder()
92+
.put("failsafe.circuit_breaker.name", "testing")
93+
.put(expectedAttributeKey, expectedAttributeValue)
94+
.build(),
95+
attributes));
96+
}
97+
}

settings.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,7 @@ include(":instrumentation:executors:jdk21-testing")
228228
include(":instrumentation:executors:testing")
229229
include(":instrumentation:external-annotations:javaagent")
230230
include(":instrumentation:external-annotations:javaagent-unit-tests")
231+
include(":instrumentation:failsafe-3.0:library")
231232
include(":instrumentation:finagle-http-23.11:javaagent")
232233
include(":instrumentation:finatra-2.9:javaagent")
233234
include(":instrumentation:geode-1.4:javaagent")

0 commit comments

Comments
 (0)