diff --git a/resilience4j-circuitbreaker/src/jmh/java/io/github/resilience4j/circuitbreaker/CircuitBreakerBenchmark.java b/resilience4j-circuitbreaker/src/jmh/java/io/github/resilience4j/circuitbreaker/CircuitBreakerBenchmark.java index 608a509d9b..579547057c 100644 --- a/resilience4j-circuitbreaker/src/jmh/java/io/github/resilience4j/circuitbreaker/CircuitBreakerBenchmark.java +++ b/resilience4j-circuitbreaker/src/jmh/java/io/github/resilience4j/circuitbreaker/CircuitBreakerBenchmark.java @@ -64,7 +64,6 @@ public void setUp() { protectedSupplier = CircuitBreaker.decorateSupplier(circuitBreaker, stringSupplier); CircuitBreaker circuitBreakerWithSubscriber = CircuitBreaker.ofDefaults("testCircuitBreakerWithSb"); - circuitBreakerWithSubscriber.getEventStream().subscribe(); protectedSupplierWithSb = CircuitBreaker.decorateSupplier(circuitBreakerWithSubscriber, stringSupplier); } diff --git a/resilience4j-documentation/src/docs/asciidoc/addon_guides/prometheus.adoc b/resilience4j-documentation/src/docs/asciidoc/addon_guides/prometheus.adoc index 4c777cc354..f62259d4a6 100644 --- a/resilience4j-documentation/src/docs/asciidoc/addon_guides/prometheus.adoc +++ b/resilience4j-documentation/src/docs/asciidoc/addon_guides/prometheus.adoc @@ -1,9 +1,10 @@ -=== Prometheus Metrics exporter +=== Prometheus Metrics Integration ==== Introduction -Integration of circuit breaker and rate limiter metrics with -https://github.com/prometheus/client_java[Prometheus simple client] +Integration with https://github.com/prometheus/client_java[Prometheus simple client] + +Module provides exporters for `CircuitBreaker` and `RateLimiter` metrics. For the circuit breaker library exports 2 metrics: @@ -28,6 +29,15 @@ For the rate limiter following metric with default name `rate_limiter` and label The names of the rate limiters and circuit breakers are exposed using label `name`. +This module also provides `CallMeter` -- a composite metric to measure single call/request metrics such as: + - execution time distribution, + - number of attempts and + - number of failures. + +It is implemented in Prometheus simple client's style, supports labels and produces histogram and counter metrics. + +Usage examples provided bellow in this section. + ==== Dashboard Example image::images/prometheus-dashboard.png[Circuit Breaker Dashboard Example] @@ -64,4 +74,41 @@ final RateLimiter boo = rateLimiterRegistry.rateLimiter("boo"); collectorRegistry.register(RateLimiterExports.ofRateLimiterRegistry(rateLimiterRegistry)); -- -For both it is possible to use just a collection of breakers and limiters instead of registry. \ No newline at end of file +For both it is possible to use just a collection of breakers and limiters instead of registry. + +===== Call Meter + +Simple example without labels + +[source,java] +-- +final CollectorRegistry registry = new CollectorRegistry(); + +final CallMeter meter = CallMeter.of("foo_call", "Foo call help", registry); + +CallMeter.decorateCompletionStageSupplier(meter, () -> supplyAsync(() -> { /* ... */ })); +-- + +Advanced example with labels + +[source,java] +-- +final CollectorRegistry registry = new CollectorRegistry(); + +final CallMeter meter = CallMeter + .builder() + .name("foo_call") + .help("Foo call help") + .labelNames("label_1") + .build() + .register(registry); + +meter.labels("boo").executeRunnable(() -> { /* ... */ }); + +CallMeter.decorateCompletionStageSupplier( + meter.labels("baz"), + () -> supplyAsync(() -> { /* ... */ }) +); + +-- + diff --git a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/Timer.java b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/Timer.java index b3fc0a5469..f364bf7acb 100644 --- a/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/Timer.java +++ b/resilience4j-metrics/src/main/java/io/github/resilience4j/metrics/Timer.java @@ -47,7 +47,7 @@ public interface Timer { * * @param name the name of the timer * @param metricRegistry the MetricRegistry - * @return a Bulkhead instance + * @return a Timer instance */ static Timer ofMetricRegistry(String name, MetricRegistry metricRegistry) { return new TimerImpl(name, metricRegistry); @@ -57,7 +57,7 @@ static Timer ofMetricRegistry(String name, MetricRegistry metricRegistry) { * Creates a timer of a default MetricRegistry * * @param name the name of the timer - * @return a Bulkhead instance + * @return a Timer instance */ static Timer of(String name) { return new TimerImpl(name, new MetricRegistry()); diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallCollectors.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallCollectors.java new file mode 100644 index 0000000000..6c98a74461 --- /dev/null +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallCollectors.java @@ -0,0 +1,16 @@ +package io.github.resilience4j.prometheus; + +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; + +final class CallCollectors { + public final Histogram histogram; + public final Counter totalCounter; + public final Counter errorCounter; + + CallCollectors(Histogram histogram, Counter totalCounter, Counter errorCounter) { + this.histogram = histogram; + this.totalCounter = totalCounter; + this.errorCounter = errorCounter; + } +} diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeter.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeter.java new file mode 100644 index 0000000000..c3bcf848e9 --- /dev/null +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeter.java @@ -0,0 +1,333 @@ +package io.github.resilience4j.prometheus; + +import io.prometheus.client.*; +import io.vavr.CheckedFunction0; +import io.vavr.CheckedFunction1; +import io.vavr.CheckedRunnable; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import java.util.function.Supplier; + +public interface CallMeter extends CallMeterBase { + + /** + * Creates call meter with the given name and help message + * + * @param name - metric name + * @param help - metric help + * @return the call meter + */ + static CallMeter of(String name, String help) { + return CallMeter + .builder() + .name(name) + .help(help) + .build(); + } + + /** + * Creates call meter with the given name and registers it in + * the specified collector registry + * + * @param name - metric name + * @param help - metric help + * @param registry - collector registry + * @return the call meter + */ + static CallMeter ofCollectorRegistry(String name, String help, CollectorRegistry registry) { + return of(name, help).register(registry); + } + + /** + * Creates a child call meter with the given labels + * @param labels + * @return child collector + */ + Child labels(String... labels); + + /** + * Register this call meter with the default registry. + */ + default CallMeter register() { + return register(CollectorRegistry.defaultRegistry); + } + + /** + * Registers this call meter with the given registry. + */ + CallMeter register(CollectorRegistry registry); + + /** + * Creates a new call meter {@link Builder} + * + * @return the new {@link Builder} + */ + static Builder builder() { + return new Builder(); + } + + /** + * Creates a timed checked supplier. + * + * @param meter the call meter to use + * @param supplier the original supplier + * @return a timed supplier + */ + static CheckedFunction0 decorateCheckedSupplier(CallMeterBase meter, CheckedFunction0 supplier){ + return () -> { + final Timer timer = meter.startTimer(); + try { + final T returnValue = supplier.apply(); + timer.onSuccess(); + return returnValue; + }catch (Throwable e){ + timer.onError(); + throw e; + } + }; + } + + /** + * Creates a timed runnable. + * + * @param meter the call meter to use + * @param runnable the original runnable + * @return a timed runnable + */ + static CheckedRunnable decorateCheckedRunnable(CallMeterBase meter, CheckedRunnable runnable){ + return () -> { + final Timer timer = meter.startTimer(); + try { + runnable.run(); + timer.onSuccess(); + }catch (Throwable e){ + timer.onError(); + throw e; + } + }; + } + + /** + * Creates a timed checked supplier. + * + * @param meter the call meter to use + * @param supplier the original supplier + * @return a timed supplier + */ + static Supplier decorateSupplier(CallMeterBase meter, Supplier supplier){ + return () -> { + final Timer timer = meter.startTimer(); + try { + final T returnValue = supplier.get(); + timer.onSuccess(); + return returnValue; + }catch (Throwable e){ + timer.onError(); + throw e; + } + }; + } + + /** + * Creates a timed Callable. + * + * @param meter the call meter to use + * @param callable the original Callable + * @return a timed Callable + */ + static Callable decorateCallable(CallMeterBase meter, Callable callable){ + return () -> { + final Timer timer = meter.startTimer(); + try { + final T returnValue = callable.call(); + timer.onSuccess(); + return returnValue; + }catch (Throwable e){ + timer.onError(); + throw e; + } + }; + } + + + /** + * Creates a timed runnable. + + * @param meter the call meter to use + * @param runnable the original runnable + * @return a timed runnable + */ + static Runnable decorateRunnable(CallMeterBase meter, Runnable runnable){ + return () -> { + final Timer timer = meter.startTimer(); + try { + runnable.run(); + timer.onSuccess(); + }catch (Throwable e){ + timer.onError(); + throw e; + } + }; + } + + + /** + * Creates a timed function. + * + * @param meter the call meter to use + * @param function the original function + * @return a timed function + */ + static Function decorateFunction(CallMeterBase meter, Function function){ + return (T t) -> { + final Timer timer = meter.startTimer(); + try { + R returnValue = function.apply(t); + timer.onSuccess(); + return returnValue; + }catch (Throwable e){ + timer.onError(); + throw e; + } + }; + } + + /** + * Creates a timed function. + * + * @param meter the call meter to use + * @param function the original function + * @return a timed function + */ + static CheckedFunction1 decorateCheckedFunction(CallMeterBase meter, CheckedFunction1 function){ + return (T t) -> { + final Timer timer = meter.startTimer(); + try { + R returnValue = function.apply(t); + timer.onSuccess(); + return returnValue; + }catch (Throwable e){ + timer.onError(); + throw e; + } + }; + } + + /** + * Decorates completion stage supplier with call meter + * + * @param meter the call meter to use + * @param stageSupplier the CompletionStage Supplier + * @return a decorated completion stage + */ + static Supplier> decorateCompletionStageSupplier(CallMeterBase meter, Supplier> stageSupplier) { + return () -> { + final Timer timer = meter.startTimer(); + try { + final CompletionStage stage = stageSupplier.get(); + + stage.whenComplete((result, throwable) -> { + if (throwable != null) { + timer.onError(); + } else { + timer.onSuccess(); + } + }); + + return stage; + } catch (Throwable throwable) { + timer.onError(); + throw throwable; + } + }; + } + + interface Child extends CallMeterBase { + + } + + class Builder { + private String namespace = ""; + private String subsystem = ""; + private String name = ""; + private String help = ""; + private String[] labelNames = new String[]{}; + + /** + * Set the name of the metric. Required. + */ + public Builder name(String name) { + this.name = name; + return this; + } + /** + * Set the subsystem of the metric. Optional. + */ + public Builder subsystem(String subsystem) { + this.subsystem = subsystem; + return this; + } + /** + * Set the namespace of the metric. Optional. + */ + public Builder namespace(String namespace) { + this.namespace = namespace; + return this; + } + /** + * Set the help string of the metric. Required. + */ + public Builder help(String help) { + this.help = help; + return this; + } + /** + * Set the labelNames of the metric. Optional, defaults to no labels. + */ + public Builder labelNames(String... labelNames) { + this.labelNames = labelNames; + return this; + } + + /** + * Return the constructed collector. + */ + public CallMeter build() { + return new CallMeterImpl(createMetrics()); + } + + private CallCollectors createMetrics() { + final Counter totalCounter = Counter + .build() + .namespace(namespace) + .subsystem(subsystem) + .name(name + "_total") + .help(help + " total") + .labelNames(labelNames) + .create(); + + final Counter errorCounter = Counter + .build() + .namespace(namespace) + .subsystem(subsystem) + .name(name + "_failures_total") + .help(help + " failures total") + .labelNames(labelNames) + .create(); + + final Histogram histogram = Histogram + .build() + .namespace(namespace) + .subsystem(subsystem) + .name(name + "_latency") + .help(help + " latency") + .labelNames(labelNames) + .create(); + + return new CallCollectors(histogram, totalCounter, errorCounter); + } + } +} + diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeterBase.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeterBase.java new file mode 100644 index 0000000000..50ea6cec5b --- /dev/null +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeterBase.java @@ -0,0 +1,67 @@ +package io.github.resilience4j.prometheus; + +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; + +public interface CallMeterBase { + Timer startTimer(); + + /** + * Decorates and executes the decorated Runnable. + * + * @param runnable the original Callable + */ + default void executeRunnable(Runnable runnable) throws Exception { + CallMeter.decorateRunnable(this, runnable).run(); + } + + /** + * Decorates and executes the decorated Callable. + * + * @param callable the original Callable + * @param the type of results supplied by this Callable + * @return the result of the decorated Callable. + */ + default T executeCallable(Callable callable) throws Exception { + return CallMeter.decorateCallable(this, callable).call(); + } + + /** + * Decorates and executes the decorated Supplier. + * + * @param supplier the original Supplier + * @param the type of results supplied by this supplier + * @return the result of the decorated Supplier. + */ + default T executeSupplier(Supplier supplier){ + return CallMeter.decorateSupplier(this, supplier).get(); + } + + /** + * Decorates and executes the decorated CompletionStage Supplier. + * + * @param supplier the CompletionStage Supplier + * @param the type of results supplied by this supplier + * @return the result of the decorated Supplier. + */ + default CompletionStage executeCompletionStageSupplier(Supplier> supplier){ + return CallMeter.decorateCompletionStageSupplier(this, supplier).get(); + } + + interface Timer { + + /** + * Stops the Timer and records a failed call. + * This method must be invoked when a call failed. + */ + void onError(); + + /** + * Stops the Timer and records a successful call. + */ + void onSuccess(); + } + + +} diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeterChildImpl.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeterChildImpl.java new file mode 100644 index 0000000000..8b63201ed6 --- /dev/null +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeterChildImpl.java @@ -0,0 +1,35 @@ +package io.github.resilience4j.prometheus; + +import io.prometheus.client.Counter; +import io.prometheus.client.Histogram; + +class CallMeterChildImpl implements CallMeter.Child { + + private final Histogram.Child histogram; + private final Counter.Child totalCounter; + private final Counter.Child errorCounter; + + CallMeterChildImpl(Histogram.Child histogram, Counter.Child totalCounter, Counter.Child errorCounter) { + this.histogram = histogram; + this.totalCounter = totalCounter; + this.errorCounter = errorCounter; + } + + @Override + public Timer startTimer() { + final Histogram.Timer timer = histogram.startTimer(); + totalCounter.inc(); + + return new Timer() { + @Override + public void onError() { + errorCounter.inc(); + } + + @Override + public void onSuccess() { + timer.observeDuration(); + } + }; + } +} diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeterImpl.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeterImpl.java new file mode 100644 index 0000000000..1a3544aa8e --- /dev/null +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CallMeterImpl.java @@ -0,0 +1,52 @@ +package io.github.resilience4j.prometheus; + + +import io.prometheus.client.CollectorRegistry; +import io.prometheus.client.Histogram; + +import static java.util.Objects.requireNonNull; + +class CallMeterImpl implements CallMeter { + + private final CallCollectors collectors; + + CallMeterImpl(CallCollectors collectors) { + requireNonNull(collectors); + this.collectors = collectors; + } + + @Override + public Child labels(String... labels) { + return new CallMeterChildImpl( + collectors.histogram.labels(labels), + collectors.totalCounter.labels(labels), + collectors.errorCounter.labels(labels)); + } + + @Override + public Timer startTimer() { + + final Histogram.Timer timer = collectors.histogram.startTimer(); + collectors.totalCounter.inc(); + + return new Timer() { + @Override + public void onError() { + collectors.errorCounter.inc(); + } + + @Override + public void onSuccess() { + timer.observeDuration(); + } + }; + } + + @Override + public CallMeter register(CollectorRegistry registry) { + registry.register(collectors.histogram); + registry.register(collectors.totalCounter); + registry.register(collectors.errorCounter); + return this; + } +} diff --git a/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/CallMeterTest.java b/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/CallMeterTest.java new file mode 100644 index 0000000000..281b74f639 --- /dev/null +++ b/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/CallMeterTest.java @@ -0,0 +1,188 @@ +package io.github.resilience4j.prometheus; + +import io.prometheus.client.CollectorRegistry; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.Assert.fail; + +public class CallMeterTest { + + @Test + public void testInstrumentsSuccessfulCall() throws Exception { + + // Given + final CollectorRegistry registry = new CollectorRegistry(); + + final CallMeter timer = CallMeter.ofCollectorRegistry("some_call", "Some help", registry); + + // When + timer.executeRunnable(() -> { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + fail(); + } + }); + + // Then + assertThat(registry.getSampleValue( + "some_call_total", + new String[]{}, + new String[]{})) + .isEqualTo(1.0); + + assertThat(registry.getSampleValue( + "some_call_failures_total", + new String[]{}, + new String[]{})) + .isEqualTo(0.0); + + assertThat(registry.getSampleValue( + "some_call_latency_count", + new String[]{}, + new String[]{})) + .isEqualTo(1.0); + } + + @Test + public void testInstrumentsFailedCall() throws Exception { + + // Given + final CollectorRegistry registry = new CollectorRegistry(); + + final CallMeter timer = CallMeter.ofCollectorRegistry("some_call", "Some help", registry); + + try { + // When + timer.executeRunnable(() -> { + try { + Thread.sleep(50); + throw new SomeAppException("Test Exception"); + } catch (InterruptedException e) { + fail(); + } + }); + } catch (SomeAppException e){ + assertThat(e.getMessage()).isEqualTo("Test Exception"); + // ignore + } + + // Then + assertThat(registry.getSampleValue( + "some_call_total", + new String[]{}, + new String[]{})) + .isEqualTo(1.0); + + assertThat(registry.getSampleValue( + "some_call_failures_total", + new String[]{}, + new String[]{})) + .isEqualTo(1.0); + + assertThat(registry.getSampleValue( + "some_call_latency_count", + new String[]{}, + new String[]{})) + .isEqualTo(0.0); + } + + @Test + public void testInstrumentsSuccessfulCallWithLabels() throws Exception { + + // Given + final CollectorRegistry registry = new CollectorRegistry(); + + final CallMeter timer = CallMeter + .builder() + .name("some_call") + .help("Some call help") + .labelNames("label_1") + .build() + .register(registry); + + // When + timer.labels("boo").executeRunnable(() -> { + try { + Thread.sleep(50); + } catch (InterruptedException e) { + fail(); + } + }); + + // Then + assertThat(registry.getSampleValue( + "some_call_total", + new String[]{ "label_1" }, + new String[]{ "boo" })) + .isEqualTo(1.0); + + assertThat(registry.getSampleValue( + "some_call_failures_total", + new String[]{ "label_1" }, + new String[]{ "boo" })) + .isEqualTo(0.0); + + assertThat(registry.getSampleValue( + "some_call_latency_count", + new String[]{ "label_1" }, + new String[]{ "boo" })) + .isEqualTo(1.0); + } + + @Test + public void testInstrumentsFailedCallWithLabels() throws Exception { + + // Given + final CollectorRegistry registry = new CollectorRegistry(); + + final CallMeter timer = CallMeter + .builder() + .name("some_call") + .help("Some test help") + .labelNames("label_1") + .build() + .register(registry); + + try { + // When + timer.labels("foo").executeRunnable(() -> { + try { + Thread.sleep(50); + throw new SomeAppException("Test Exception"); + } catch (InterruptedException e) { + fail(); + } + }); + } catch (SomeAppException e){ + assertThat(e.getMessage()).isEqualTo("Test Exception"); + // ignore + } + + // Then + assertThat(registry.getSampleValue( + "some_call_total", + new String[]{ "label_1" }, + new String[]{ "foo" })) + .isEqualTo(1.0); + + assertThat(registry.getSampleValue( + "some_call_failures_total", + new String[]{ "label_1" }, + new String[]{ "foo" })) + .isEqualTo(1.0); + + assertThat(registry.getSampleValue( + "some_call_latency_count", + new String[]{ "label_1" }, + new String[]{ "foo" })) + .isEqualTo(0.0); + } + + private static class SomeAppException extends RuntimeException { + SomeAppException(String message) { + super(message); + } + } +}