Skip to content

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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 @@ -100,6 +100,9 @@ targets:
- type: gradle
path: ./
target: ':instrumentation:external-annotations:javaagent'
- type: gradle
path: ./
target: ':instrumentation:failsafe-3.0:library'
- type: gradle
path: ./
target: ':instrumentation:finagle-http-23.11:javaagent'
Expand Down
1 change: 1 addition & 0 deletions docs/supported-libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ These are the supported libraries and frameworks:
| [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] |
| [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] |
| [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] |
| [Failsafe](https://failsafe.dev/) | 3.0.1+ | [opentelemetry-failsafe-3.0](../instrumentation/failsafe-3.0/library) | none |
Copy link
Contributor

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 to N/A? Any suggestions?

Copy link
Member

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 👍

| [Finagle](https://github.com/twitter/finagle) | 23.11+ | N/A | none |
| [Finatra](https://github.com/twitter/finatra) | 2.9+ | N/A | Provides `http.route` [2], Controller Spans [3] |
| [Geode Client](https://geode.apache.org/) | 1.4+ | N/A | [Database Client Spans], [Database Client Metrics]&nbsp;[6] |
Expand Down
39 changes: 39 additions & 0 deletions instrumentation/failsafe-3.0/README.md
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");
}
```
9 changes: 9 additions & 0 deletions instrumentation/failsafe-3.0/library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
plugins {
id("otel.library-instrumentation")
}

dependencies {
library("dev.failsafe:failsafe:3.0.1")
Copy link
Contributor

Choose a reason for hiding this comment

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

3.0.0 doesn't work?

Copy link
Author

Choose a reason for hiding this comment

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

No, .builder(PolicyConfig) is added in 3.0.1. This enables us to extend user configured CircuitBreaker with the instrumentation.

Copy link
Contributor

Choose a reason for hiding this comment

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

@trask is it ok to name the instrumentation 3.0.1 when the first supported version is really 3.0.1? I think it is fine, we don't have any other instrumentations that would use patch version in the name.

Copy link
Member

Choose a reason for hiding this comment

The 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 3.0 to conform with others and just mention 3.0.1+ in the docs


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));
}
}
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")

Copy link
Contributor

Choose a reason for hiding this comment

The 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")
Loading