diff --git a/.fossa.yml b/.fossa.yml index 0a29d3ce460a..8ca3477c2f3c 100644 --- a/.fossa.yml +++ b/.fossa.yml @@ -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' diff --git a/docs/supported-libraries.md b/docs/supported-libraries.md index 85118cd92502..1ef866ea0338 100644 --- a/docs/supported-libraries.md +++ b/docs/supported-libraries.md @@ -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,
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] [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] [6] | +| [Failsafe](https://failsafe.dev/) | 3.0.1+ | [opentelemetry-failsafe-3.0](../instrumentation/failsafe-3.0/library) | none | | [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] [6] | diff --git a/instrumentation/failsafe-3.0/README.md b/instrumentation/failsafe-3.0/README.md new file mode 100644 index 000000000000..7f2c6df5aad9 --- /dev/null +++ b/instrumentation/failsafe-3.0/README.md @@ -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 + + + io.opentelemetry.instrumentation + opentelemetry-failsafe-3.0 + OPENTELEMETRY_VERSION + + +``` + +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 + CircuitBreaker configure(OpenTelemetry openTelemetry, CircuitBreaker circuitBreaker) { + FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(openTelemetry); + return failsafeTelemetry.createCircuitBreaker(circuitBreaker, "my-circuit-breaker"); +} +``` diff --git a/instrumentation/failsafe-3.0/library/build.gradle.kts b/instrumentation/failsafe-3.0/library/build.gradle.kts new file mode 100644 index 000000000000..e1667a063e6e --- /dev/null +++ b/instrumentation/failsafe-3.0/library/build.gradle.kts @@ -0,0 +1,9 @@ +plugins { + id("otel.library-instrumentation") +} + +dependencies { + library("dev.failsafe:failsafe:3.0.1") + + testImplementation(project(":testing-common")) +} diff --git a/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/CircuitBreakerEventListenerBuilders.java b/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/CircuitBreakerEventListenerBuilders.java new file mode 100644 index 000000000000..73ff69beb14e --- /dev/null +++ b/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/CircuitBreakerEventListenerBuilders.java @@ -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 EventListener> buildInstrumentedFailureListener( + CircuitBreakerConfig userConfig, Meter meter, Attributes attributes) { + LongCounter failureCounter = + meter + .counterBuilder("failsafe.circuitbreaker.failure.count") + .setDescription("Count of failed circuit breaker executions.") + .build(); + EventListener> failureListener = userConfig.getFailureListener(); + return e -> { + failureCounter.add(1, attributes); + if (failureListener != null) { + failureListener.accept(e); + } + }; + } + + static EventListener> buildInstrumentedSuccessListener( + CircuitBreakerConfig userConfig, Meter meter, Attributes attributes) { + LongCounter successCounter = + meter + .counterBuilder("failsafe.circuitbreaker.success.count") + .setDescription("Count of successful circuit breaker executions.") + .build(); + EventListener> successListener = userConfig.getSuccessListener(); + return e -> { + successCounter.add(1, attributes); + if (successListener != null) { + successListener.accept(e); + } + }; + } + + static EventListener buildInstrumentedOpenListener( + CircuitBreakerConfig userConfig, Meter meter, Attributes attributes) { + LongCounter openCircuitBreakerCounter = + meter + .counterBuilder("failsafe.circuitbreaker.open.count") + .setDescription("Count of times that circuit breaker was opened.") + .build(); + EventListener openListener = userConfig.getOpenListener(); + return e -> { + openCircuitBreakerCounter.add(1, attributes); + openListener.accept(e); + }; + } + + static EventListener buildInstrumentedHalfOpenListener( + CircuitBreakerConfig 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 halfOpenListener = + userConfig.getHalfOpenListener(); + return e -> { + halfOpenCircuitBreakerCounter.add(1, attributes); + halfOpenListener.accept(e); + }; + } + + static EventListener buildInstrumentedCloseListener( + CircuitBreakerConfig userConfig, Meter meter, Attributes attributes) { + LongCounter closedCircuitBreakerCounter = + meter + .counterBuilder("failsafe.circuitbreaker.closed.count") + .setDescription("Count of times that circuit breaker was closed.") + .build(); + EventListener closeListener = userConfig.getCloseListener(); + return e -> { + closedCircuitBreakerCounter.add(1, attributes); + closeListener.accept(e); + }; + } +} 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 new file mode 100644 index 000000000000..1c9f4ae2c34e --- /dev/null +++ b/instrumentation/failsafe-3.0/library/src/main/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetry.java @@ -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 {@link CircuitBreaker}'s result type + * @return instrumented {@link CircuitBreaker} + */ + public CircuitBreaker createCircuitBreaker( + CircuitBreaker delegate, String circuitBreakerName) { + CircuitBreakerConfig 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(); + } +} 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 new file mode 100644 index 000000000000..26e3618367ae --- /dev/null +++ b/instrumentation/failsafe-3.0/library/src/test/java/io/opentelemetry/instrumentation/failsafe/v3_0/FailsafeTelemetryTest.java @@ -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 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 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)); + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index b8c419a9a5e0..acb68f30cffd 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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") - // benchmark include(":benchmark-overhead-jmh") include(":benchmark-jfr-analyzer")