Skip to content
Merged
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 @@ -66,6 +66,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/) | N/A | [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]&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://central.sonatype.com/artifact/io.opentelemetry.instrumentation/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");
}
```
7 changes: 7 additions & 0 deletions instrumentation/failsafe-3.0/library/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
id("otel.library-instrumentation")
}

dependencies {
library("dev.failsafe:failsafe:3.0.1")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.failsafe.v3_0;

import static io.opentelemetry.api.common.AttributeKey.stringKey;

import dev.failsafe.CircuitBreakerConfig;
import dev.failsafe.event.CircuitBreakerStateChangedEvent;
import dev.failsafe.event.EventListener;
import dev.failsafe.event.ExecutionCompletedEvent;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.metrics.LongCounter;

final class CircuitBreakerEventListenerBuilders {
private static final AttributeKey<String> OUTCOME_KEY =
stringKey("failsafe.circuit_breaker.outcome");
private static final AttributeKey<String> STATE_KEY = stringKey("failsafe.circuit_breaker.state");

private CircuitBreakerEventListenerBuilders() {}

static <R> EventListener<ExecutionCompletedEvent<R>> buildInstrumentedFailureListener(
CircuitBreakerConfig<R> userConfig,
LongCounter executionCounter,
Attributes commonAttributes) {
Attributes attributes = commonAttributes.toBuilder().put(OUTCOME_KEY, "failure").build();
return count(executionCounter, attributes, userConfig.getFailureListener());
}

static <R> EventListener<ExecutionCompletedEvent<R>> buildInstrumentedSuccessListener(
CircuitBreakerConfig<R> userConfig,
LongCounter executionCounter,
Attributes commonAttributes) {
Attributes attributes = commonAttributes.toBuilder().put(OUTCOME_KEY, "success").build();
return count(executionCounter, attributes, userConfig.getSuccessListener());
}

static <R> EventListener<CircuitBreakerStateChangedEvent> buildInstrumentedOpenListener(
CircuitBreakerConfig<R> userConfig,
LongCounter stateChangesCounter,
Attributes commonAttributes) {
Attributes attributes = commonAttributes.toBuilder().put(STATE_KEY, "open").build();
return count(stateChangesCounter, attributes, userConfig.getOpenListener());
}

static <R> EventListener<CircuitBreakerStateChangedEvent> buildInstrumentedHalfOpenListener(
CircuitBreakerConfig<R> userConfig,
LongCounter stateChangesCounter,
Attributes commonAttributes) {
Attributes attributes = commonAttributes.toBuilder().put(STATE_KEY, "half_open").build();
return count(stateChangesCounter, attributes, userConfig.getHalfOpenListener());
}

static <R> EventListener<CircuitBreakerStateChangedEvent> buildInstrumentedCloseListener(
CircuitBreakerConfig<R> userConfig,
LongCounter stateChangesCounter,
Attributes commonAttributes) {
Attributes attributes = commonAttributes.toBuilder().put(STATE_KEY, "closed").build();
return count(stateChangesCounter, attributes, userConfig.getCloseListener());
}

private static <T> EventListener<T> count(
LongCounter counter, Attributes attributes, EventListener<T> delegate) {
return e -> {
counter.add(1, attributes);
if (delegate != null) {
delegate.accept(e);
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* 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.LongCounter;
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";

private static final AttributeKey<String> CIRCUIT_BREAKER_NAME =
AttributeKey.stringKey("failsafe.circuit_breaker.name");

/** 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);
LongCounter executionCounter =
meter
.counterBuilder("failsafe.circuit_breaker.execution.count")
.setDescription("Count of circuit breaker executions.")
.setUnit("{execution}")
.build();
LongCounter stateChangesCounter =
meter
.counterBuilder("failsafe.circuit_breaker.state_changes.count")
.setDescription("Count of circuit breaker state changes.")
.setUnit("{execution}")
.build();
Attributes attributes = Attributes.of(CIRCUIT_BREAKER_NAME, circuitBreakerName);
return CircuitBreaker.builder(userConfig)
.onFailure(buildInstrumentedFailureListener(userConfig, executionCounter, attributes))
.onSuccess(buildInstrumentedSuccessListener(userConfig, executionCounter, attributes))
.onOpen(buildInstrumentedOpenListener(userConfig, stateChangesCounter, attributes))
.onHalfOpen(buildInstrumentedHalfOpenListener(userConfig, stateChangesCounter, attributes))
.onClose(buildInstrumentedCloseListener(userConfig, stateChangesCounter, attributes))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.instrumentation.failsafe.v3_0;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

import dev.failsafe.CircuitBreaker;
import dev.failsafe.CircuitBreakerOpenException;
import dev.failsafe.Failsafe;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension;
import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension;
import io.opentelemetry.sdk.testing.assertj.LongPointAssert;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Consumer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

final class FailsafeTelemetryTest {
@RegisterExtension
static final InstrumentationExtension testing = LibraryInstrumentationExtension.create();

@Test
void captureCircuitBreakerMetrics() {
// given
CircuitBreaker<Object> userCircuitBreaker =
dev.failsafe.CircuitBreaker.builder()
.handleResultIf(Objects::isNull)
.withFailureThreshold(2)
.withDelay(Duration.ZERO)
.withSuccessThreshold(2)
.build();
FailsafeTelemetry failsafeTelemetry = FailsafeTelemetry.create(testing.getOpenTelemetry());
CircuitBreaker<Object> instrumentedCircuitBreaker =
failsafeTelemetry.createCircuitBreaker(userCircuitBreaker, "testing");

// when
for (int i = 0; i < 5; i++) {
try {
int temp = i;
Failsafe.with(instrumentedCircuitBreaker).get(() -> temp < 2 ? null : new Object());
} catch (CircuitBreakerOpenException e) {
assertThat(i).isEqualTo(2);
}
}

// then
testing.waitAndAssertMetrics(
"io.opentelemetry.failsafe-3.0",
metricAssert ->
metricAssert
.hasName("failsafe.circuit_breaker.execution.count")
.hasLongSumSatisfying(
sum ->
sum.isMonotonic()
.hasPointsSatisfying(
buildCircuitBreakerAssertion(
2, "failsafe.circuit_breaker.outcome", "failure"),
buildCircuitBreakerAssertion(
3, "failsafe.circuit_breaker.outcome", "success"))));
testing.waitAndAssertMetrics(
"io.opentelemetry.failsafe-3.0",
metricAssert ->
metricAssert
.hasName("failsafe.circuit_breaker.state_changes.count")
.hasLongSumSatisfying(
sum ->
sum.isMonotonic()
.hasPointsSatisfying(
buildCircuitBreakerAssertion(
1, "failsafe.circuit_breaker.state", "open"),
buildCircuitBreakerAssertion(
1, "failsafe.circuit_breaker.state", "half_open"),
buildCircuitBreakerAssertion(
1, "failsafe.circuit_breaker.state", "closed"))));
}

private static Consumer<LongPointAssert> buildCircuitBreakerAssertion(
long expectedValue, String expectedAttributeKey, String expectedAttributeValue) {
return longSumAssert ->
longSumAssert
.hasValue(expectedValue)
.hasAttributesSatisfying(
attributes ->
assertEquals(
Attributes.builder()
.put("failsafe.circuit_breaker.name", "testing")
.put(expectedAttributeKey, expectedAttributeValue)
.build(),
attributes));
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,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