diff --git a/resilience4j-documentation/src/docs/asciidoc/addon_guides/prometheus.adoc b/resilience4j-documentation/src/docs/asciidoc/addon_guides/prometheus.adoc index f62259d4a6..db1da04e83 100644 --- a/resilience4j-documentation/src/docs/asciidoc/addon_guides/prometheus.adoc +++ b/resilience4j-documentation/src/docs/asciidoc/addon_guides/prometheus.adoc @@ -4,30 +4,43 @@ Integration with https://github.com/prometheus/client_java[Prometheus simple client] -Module provides exporters for `CircuitBreaker` and `RateLimiter` metrics. +Module provides exporters for `CircuitBreaker`, `RateLimiter` and `Bulkhead` metrics. +Every metric has `name` label indicating the relation to certain object, object category +such as `bulkhead` is inlined to the metric name, so that the combination of metric name + `name` label +uniquely identifies metric per backend. For example: -For the circuit breaker library exports 2 metrics: +``` +resilience4j_bulkhead_available_concurrent_calls{name="Backend1"} +resilience4j_circuitbreaker_buffered_calls{name="Backend1"} +resilience4j_ratelimiter_waiting_threads{name=Backend1"} +``` -1. By state with default metric name `circuit_breaker_states` and label `state`: +For every `CircuitBreaker` the library exports the following metrics: - - `closed` - - `open` - - `half_open` +1. `resilience4j_circuitbreaker_state` - the state of the circuit breaker, possible values: -2. By call result with default metric name `circuit_breaker_calls` and label `call_result`: + - 0 - CLOSED + - 1 - OPEN + - 2 - HALF_OPEN - - `successful` - - `failed` - - `not_permitted` - - `buffered` - - `buffered_max` +2. `resilience4j_circuitbreaker_calls` - the number of recorded calls, +the additional `kind` label indicates type of calls being recorded, the possible +`kind` values are `successful`, `failed` or `not_permitted`. -For the rate limiter following metric with default name `rate_limiter` and label `param` exported: +3. `resilience4j_circuitbreaker_buffered_calls` - the number of buffered calls -- `available_permissions` -- `waiting_threads` +4. `resilience4j_circuitbreaker_max_buffered_calls` - the maximum number of buffered calls + +For every `RateLimiter` the library exports the following metrics: + +1. `resilience4j_ratelimiter_available_permissions` - the number of available permissions +2. `resilience4j_ratelimiter_waiting_threads` - the number of waiting threads + +For every `Bulkhead` the library exports the following metrics: + +1. `resilience4j_bulkhead_available_concurrent_calls` - the number of available concurrent calls +2. `resilience4j_bulkhead_max_allowed_concurrent_calls` - the maximum number of allowed concurrent calls -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, @@ -56,7 +69,7 @@ final CircuitBreaker foo = circuitBreakerRegistry.circuitBreaker("foo"); final CircuitBreaker boo = circuitBreakerRegistry.circuitBreaker("boo"); // Registering metrics in prometeus CollectorRegistry -collectorRegistry.register(CircuitBreakerExports.ofCircuitBreakerRegistry(circuitBreakerRegistry)); +collectorRegistry.register(CircuitBreakerMetricsCollector.ofCircuitBreakerRegistry(circuitBreakerRegistry)); -- ===== RateLimiter @@ -71,10 +84,26 @@ final RateLimiter foo = rateLimiterRegistry.rateLimiter("foo"); final RateLimiter boo = rateLimiterRegistry.rateLimiter("boo"); // Registering metrics in prometeus CollectorRegistry -collectorRegistry.register(RateLimiterExports.ofRateLimiterRegistry(rateLimiterRegistry)); +collectorRegistry.register(RateLimiterMetricsCollector.ofRateLimiterRegistry(rateLimiterRegistry)); +-- + +===== Bulkhead + +[source,java] +-- +final CollectorRegistry collectorRegistry = CollectorRegistry.defaultRegistry; + +final BulkheadRegistry bulkheadRegistry = BulkheadRegistry.ofDefaults(); + +final Bulkhead foo = bulkheadRegistry.bulkhead("foo"); +final Bulkhead boo = bulkheadRegistry.bulkhead("boo"); + +// Registering metrics in prometeus CollectorRegistry +collectorRegistry.register(BulkheadMetricsCollector.ofBulkheadRegistry(rateLimiterRegistry)); -- -For both it is possible to use just a collection of breakers and limiters instead of registry. +Every collector exposes more methods for binding corresponding objects, for example, +you can use suppliers, collections or even collect metrics for a single object. ===== Call Meter diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/BulkheadExports.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/BulkheadExports.java index c78e79cd88..41298a90a0 100644 --- a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/BulkheadExports.java +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/BulkheadExports.java @@ -34,7 +34,10 @@ /** * An adapter from builtin {@link Bulkhead.Metrics} to prometheus * {@link io.prometheus.client.CollectorRegistry}. + * + * @deprecated use {@link io.github.resilience4j.prometheus.collectors.BulkheadMetricsCollector} instead */ +@Deprecated public class BulkheadExports extends Collector { private static final String DEFAULT_NAME = "resilience4j_bulkhead"; diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CircuitBreakerExports.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CircuitBreakerExports.java index 051145fccf..5ed4f1ff55 100644 --- a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CircuitBreakerExports.java +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/CircuitBreakerExports.java @@ -37,7 +37,10 @@ * {@link io.prometheus.client.CollectorRegistry}. * * Also exports {@link CircuitBreaker} state as a labeled metric + * + * @deprecated use {@link io.github.resilience4j.prometheus.collectors.CircuitBreakerMetricsCollector} instead. */ +@Deprecated public class CircuitBreakerExports extends Collector { private static final String DEFAULT_NAME = "resilience4j_circuitbreaker"; diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/RateLimiterExports.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/RateLimiterExports.java index 3730b63213..cd4a0cb48c 100644 --- a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/RateLimiterExports.java +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/RateLimiterExports.java @@ -34,7 +34,10 @@ /** * An adapter from builtin {@link RateLimiter.Metrics} to prometheus * {@link io.prometheus.client.CollectorRegistry}. + * + * @deprecated use {@link io.github.resilience4j.prometheus.collectors.RateLimiterMetricsCollector} instead. */ +@Deprecated public class RateLimiterExports extends Collector { private static final String DEFAULT_NAME = "resilience4j_ratelimiter"; diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/BulkheadMetricsCollector.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/BulkheadMetricsCollector.java new file mode 100644 index 0000000000..faf9bbab45 --- /dev/null +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/BulkheadMetricsCollector.java @@ -0,0 +1,175 @@ +/* + * Copyright 2019 Yevhenii Voievodin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.prometheus.collectors; + +import io.github.resilience4j.bulkhead.Bulkhead; +import io.github.resilience4j.bulkhead.Bulkhead.Metrics; +import io.github.resilience4j.bulkhead.BulkheadRegistry; +import io.prometheus.client.Collector; +import io.prometheus.client.GaugeMetricFamily; + +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; + +/** Collects bulkhead exposed {@link Metrics}. */ +public class BulkheadMetricsCollector extends Collector { + + /** + * Creates a new collector with custom metric names and + * using given {@code supplier} as source of bulkheads. + * + * @param names the custom metric names + * @param supplier the supplier of bulkheads, note that supplier will be called one every {@link #collect()} + */ + public static BulkheadMetricsCollector ofSupplier(MetricNames names, Supplier> supplier) { + return new BulkheadMetricsCollector(names, supplier); + } + + /** + * Creates a new collector using given {@code supplier} as source of bulkheads. + * + * @param supplier the supplier of bulkheads, note that supplier will be called one very {@link #collect()} + */ + public static BulkheadMetricsCollector ofSupplier(Supplier> supplier) { + return new BulkheadMetricsCollector(MetricNames.ofDefaults(), supplier); + } + + /** + * Creates a new collector using given {@code registry} as source of bulkheads. + * + * @param registry the source of bulkheads + */ + public static BulkheadMetricsCollector ofBulkheadRegistry(BulkheadRegistry registry) { + return new BulkheadMetricsCollector(MetricNames.ofDefaults(), registry::getAllBulkheads); + } + + /** + * Creates a new collector using given {@code bulkheads} iterable as source of bulkheads. + * + * @param bulkheads the source of bulkheads + */ + public static BulkheadMetricsCollector ofIterable(Iterable bulkheads) { + return new BulkheadMetricsCollector(MetricNames.ofDefaults(), () -> bulkheads); + } + + /** + * Creates a new collector for a given {@code bulkhead}. + * + * @param bulkhead the bulkhead to collect metrics for + */ + public static BulkheadMetricsCollector ofBulkhead(Bulkhead bulkhead) { + return ofIterable(singletonList(bulkhead)); + } + + private final MetricNames names; + private final Supplier> bulkheadsSupplier; + + private BulkheadMetricsCollector(MetricNames names, Supplier> bulkheadsSupplier) { + this.names = Objects.requireNonNull(names); + this.bulkheadsSupplier = Objects.requireNonNull(bulkheadsSupplier); + } + + @Override + public List collect() { + GaugeMetricFamily availableCallsFamily = new GaugeMetricFamily( + names.getAvailableConcurrentCallsMetricName(), + "The number of available concurrent calls", + LabelNames.NAME + ); + GaugeMetricFamily maxAllowedCallsFamily = new GaugeMetricFamily( + names.getMaxAllowedConcurrentCallsMetricName(), + "The maximum number of allowed concurrent calls", + LabelNames.NAME + ); + + for (Bulkhead bulkhead: bulkheadsSupplier.get()) { + List labelValues = singletonList(bulkhead.getName()); + availableCallsFamily.addMetric(labelValues, bulkhead.getMetrics().getAvailableConcurrentCalls()); + maxAllowedCallsFamily.addMetric(labelValues, bulkhead.getMetrics().getMaxAllowedConcurrentCalls()); + } + + return asList(availableCallsFamily, maxAllowedCallsFamily); + } + + /** Defines possible configuration for metric names. */ + public static class MetricNames { + + public static final String DEFAULT_BULKHEAD_AVAILABLE_CONCURRENT_CALLS_METRIC_NAME = "resilience4j_bulkhead_available_concurrent_calls"; + public static final String DEFAULT_BULKHEAD_MAX_ALLOWED_CONCURRENT_CALLS_METRIC_NAME = "resilience4j_bulkhead_max_allowed_concurrent_calls"; + + /** + * Returns a builder for creating custom metric names. + * Note that names have default values, so only desired metrics can be renamed. + */ + public static Builder custom() { + return new Builder(); + } + + /** Returns default metric names. */ + public static MetricNames ofDefaults() { + return new MetricNames(); + } + + private String availableConcurrentCallsMetricName = DEFAULT_BULKHEAD_AVAILABLE_CONCURRENT_CALLS_METRIC_NAME; + private String maxAllowedConcurrentCallsMetricName = DEFAULT_BULKHEAD_MAX_ALLOWED_CONCURRENT_CALLS_METRIC_NAME; + + private MetricNames() {} + + /** + * Returns the metric name for bulkhead concurrent calls, + * defaults to {@value DEFAULT_BULKHEAD_AVAILABLE_CONCURRENT_CALLS_METRIC_NAME}. + */ + public String getAvailableConcurrentCallsMetricName() { + return availableConcurrentCallsMetricName; + } + + /** + * Returns the metric name for bulkhead max available concurrent calls, + * defaults to {@value DEFAULT_BULKHEAD_MAX_ALLOWED_CONCURRENT_CALLS_METRIC_NAME}. + */ + public String getMaxAllowedConcurrentCallsMetricName() { + return maxAllowedConcurrentCallsMetricName; + } + + /** Helps building custom instance of {@link MetricNames}. */ + public static class Builder { + + private final MetricNames metricNames = new MetricNames(); + + /** Overrides the default metric name {@value MetricNames#DEFAULT_BULKHEAD_AVAILABLE_CONCURRENT_CALLS_METRIC_NAME} with a given one. */ + public Builder availableConcurrentCallsMetricName(String availableConcurrentCallsMetricNames) { + metricNames.availableConcurrentCallsMetricName = requireNonNull(availableConcurrentCallsMetricNames); + return this; + } + + /** Overrides the default metric name {@value MetricNames#DEFAULT_BULKHEAD_MAX_ALLOWED_CONCURRENT_CALLS_METRIC_NAME} with a given one. */ + public Builder maxAllowedConcurrentCallsMetricName(String maxAllowedConcurrentCallsMetricName) { + metricNames.maxAllowedConcurrentCallsMetricName = requireNonNull(maxAllowedConcurrentCallsMetricName); + return this; + } + + /** Builds {@link MetricNames} instance. */ + public MetricNames build() { + return metricNames; + } + } + } +} diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/CircuitBreakerMetricsCollector.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/CircuitBreakerMetricsCollector.java new file mode 100644 index 0000000000..b53ef9f7b0 --- /dev/null +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/CircuitBreakerMetricsCollector.java @@ -0,0 +1,212 @@ +/* + * Copyright 2019 Yevhenii Voievodin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.prometheus.collectors; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.github.resilience4j.circuitbreaker.CircuitBreaker.Metrics; +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import io.prometheus.client.Collector; +import io.prometheus.client.GaugeMetricFamily; + +import java.util.List; +import java.util.Objects; +import java.util.function.Supplier; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; + +/** Collects circuit breaker exposed {@link Metrics}. */ +public class CircuitBreakerMetricsCollector extends Collector { + + /** + * Creates a new collector with custom metric names and + * using given {@code supplier} as source of circuit breakers. + * + * @param names the custom metric names + * @param supplier the supplier of circuit breakers, note that supplier will be called one every {@link #collect()} + */ + public static CircuitBreakerMetricsCollector ofSupplier(MetricNames names, Supplier> supplier) { + return new CircuitBreakerMetricsCollector(names, supplier); + } + + /** + * Creates a new collector using given {@code supplier} as source of circuit breakers. + * + * @param supplier the supplier of circuit breakers, note that supplier will be called one very {@link #collect()} + */ + public static CircuitBreakerMetricsCollector ofSupplier(Supplier> supplier) { + return new CircuitBreakerMetricsCollector(MetricNames.ofDefaults(), supplier); + } + + /** + * Creates a new collector using given {@code registry} as source of circuit breakers. + * + * @param registry the source of circuit breakers + */ + public static CircuitBreakerMetricsCollector ofCircuitBreakerRegistry(CircuitBreakerRegistry registry) { + return new CircuitBreakerMetricsCollector(MetricNames.ofDefaults(), registry::getAllCircuitBreakers); + } + + /** + * Creates a new collector for given {@code circuitBreakers}. + * + * @param circuitBreakers the circuit breakers to collect metrics for + */ + public static CircuitBreakerMetricsCollector ofIterable(Iterable circuitBreakers) { + return new CircuitBreakerMetricsCollector(MetricNames.ofDefaults(), () -> circuitBreakers); + } + + /** + * Creates a new collector for a given {@code circuitBreaker}. + * + * @param circuitBreaker the circuit breaker to collect metrics for + */ + public static CircuitBreakerMetricsCollector ofCircuitBreaker(CircuitBreaker circuitBreaker) { + return ofIterable(singletonList(circuitBreaker)); + } + + private final MetricNames names; + private final Supplier> supplier; + + private CircuitBreakerMetricsCollector(MetricNames names, Supplier> supplier) { + this.names = Objects.requireNonNull(names); + this.supplier = Objects.requireNonNull(supplier); + } + + @Override + public List collect() { + GaugeMetricFamily stateFamily = new GaugeMetricFamily( + names.getStateMetricName(), + "The state of the circuit breaker: 0 - CLOSED, 1 - OPEN, 2 - HALF_OPEN", + LabelNames.NAME + ); + GaugeMetricFamily callsFamily = new GaugeMetricFamily( + names.getCallsMetricName(), + "The number of calls for a corresponding kind", + LabelNames.NAME_AND_KIND + ); + GaugeMetricFamily bufferedCallsFamily = new GaugeMetricFamily( + names.getBufferedCallsMetricName(), + "The number of buffered calls", + LabelNames.NAME + ); + GaugeMetricFamily maxBufferedCallsFamily = new GaugeMetricFamily( + names.getMaxBufferedCallsMetricName(), + "The maximum number of buffered calls", + LabelNames.NAME + ); + + for (CircuitBreaker circuitBreaker : supplier.get()) { + List nameLabel = singletonList(circuitBreaker.getName()); + + stateFamily.addMetric(nameLabel, circuitBreaker.getState().getOrder()); + + Metrics metrics = circuitBreaker.getMetrics(); + callsFamily.addMetric(asList(circuitBreaker.getName(), "successful"), metrics.getNumberOfSuccessfulCalls()); + callsFamily.addMetric(asList(circuitBreaker.getName(), "failed"), metrics.getNumberOfFailedCalls()); + callsFamily.addMetric(asList(circuitBreaker.getName(), "not_permitted"), metrics.getNumberOfNotPermittedCalls()); + + bufferedCallsFamily.addMetric(nameLabel, metrics.getNumberOfBufferedCalls()); + maxBufferedCallsFamily.addMetric(nameLabel, metrics.getMaxNumberOfBufferedCalls()); + } + + return asList(stateFamily, callsFamily, bufferedCallsFamily, maxBufferedCallsFamily); + } + + /** Defines possible configuration for metric names. */ + public static class MetricNames { + + public static final String DEFAULT_CIRCUIT_BREAKER_CALLS_METRIC_NAME = "resilience4j_circuitbreaker_calls"; + public static final String DEFAULT_CIRCUIT_BREAKER_STATE_METRIC_NAME = "resilience4j_circuitbreaker_state"; + public static final String DEFAULT_CIRCUIT_BREAKER_BUFFERED_CALLS = "resilience4j_circuitbreaker_buffered_calls"; + public static final String DEFAULT_CIRCUIT_BREAKER_MAX_BUFFERED_CALLS = "resilience4j_circuitbreaker_max_buffered_calls"; + + /** + * Returns a builder for creating custom metric names. + * Note that names have default values, so only desired metrics can be renamed. + */ + public static Builder custom() { + return new Builder(); + } + + /** Returns default metric names. */ + public static MetricNames ofDefaults() { + return new MetricNames(); + } + + private String callsMetricName = DEFAULT_CIRCUIT_BREAKER_CALLS_METRIC_NAME; + private String stateMetricName = DEFAULT_CIRCUIT_BREAKER_STATE_METRIC_NAME; + private String bufferedCallsMetricName = DEFAULT_CIRCUIT_BREAKER_BUFFERED_CALLS; + private String maxBufferedCallsMetricName = DEFAULT_CIRCUIT_BREAKER_MAX_BUFFERED_CALLS; + + private MetricNames() {} + + /** Returns the metric name for circuit breaker calls, defaults to {@value DEFAULT_CIRCUIT_BREAKER_STATE_METRIC_NAME}. */ + public String getCallsMetricName() { + return callsMetricName; + } + + /** Returns the metric name for currently buffered calls, defaults to {@value DEFAULT_CIRCUIT_BREAKER_STATE_METRIC_NAME}. */ + public String getBufferedCallsMetricName() { + return bufferedCallsMetricName; + } + + /** Returns the metric name for max buffered calls, defaults to {@value DEFAULT_CIRCUIT_BREAKER_STATE_METRIC_NAME}. */ + public String getMaxBufferedCallsMetricName() { + return maxBufferedCallsMetricName; + } + + /** Returns the metric name for state, defaults to {@value DEFAULT_CIRCUIT_BREAKER_STATE_METRIC_NAME}. */ + public String getStateMetricName() { + return stateMetricName; + } + + /** Helps building custom instance of {@link MetricNames}. */ + public static class Builder { + private final MetricNames metricNames = new MetricNames(); + + /** Overrides the default metric name {@value MetricNames#DEFAULT_CIRCUIT_BREAKER_CALLS_METRIC_NAME} with a given one. */ + public Builder callsMetricName(String callsMetricName) { + metricNames.callsMetricName = requireNonNull(callsMetricName); + return this; + } + + /** Overrides the default metric name {@value MetricNames#DEFAULT_CIRCUIT_BREAKER_STATE_METRIC_NAME} with a given one. */ + public Builder stateMetricName(String stateMetricName) { + metricNames.stateMetricName = requireNonNull(stateMetricName); + return this; + } + + /** Overrides the default metric name {@value MetricNames#DEFAULT_CIRCUIT_BREAKER_BUFFERED_CALLS} with a given one. */ + public Builder bufferedCallsMetricName(String bufferedCallsMetricName) { + metricNames.bufferedCallsMetricName = requireNonNull(bufferedCallsMetricName); + return this; + } + + /** Overrides the default metric name {@value MetricNames#DEFAULT_CIRCUIT_BREAKER_MAX_BUFFERED_CALLS} with a given one. */ + public Builder maxBufferedCallsMetricName(String maxBufferedCallsMetricName) { + metricNames.maxBufferedCallsMetricName = requireNonNull(maxBufferedCallsMetricName); + return this; + } + + /** Builds {@link MetricNames} instance. */ + public MetricNames build() { + return metricNames; + } + } + } +} diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/LabelNames.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/LabelNames.java new file mode 100644 index 0000000000..fdee518910 --- /dev/null +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/LabelNames.java @@ -0,0 +1,26 @@ +/* + * Copyright 2019 Yevhenii Voievodin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.prometheus.collectors; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** Common constants for metric binder implementations based on tags. */ +public final class LabelNames { + public static final List NAME = Collections.singletonList("name"); + public static final List NAME_AND_KIND = Arrays.asList("name", "kind"); +} diff --git a/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/RateLimiterMetricsCollector.java b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/RateLimiterMetricsCollector.java new file mode 100644 index 0000000000..0a68e9f151 --- /dev/null +++ b/resilience4j-prometheus/src/main/java/io/github/resilience4j/prometheus/collectors/RateLimiterMetricsCollector.java @@ -0,0 +1,166 @@ +/* + * Copyright 2019 Yevhenii Voievodin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.prometheus.collectors; + +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.github.resilience4j.ratelimiter.RateLimiterRegistry; +import io.prometheus.client.Collector; +import io.prometheus.client.GaugeMetricFamily; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +import static io.github.resilience4j.ratelimiter.RateLimiter.Metrics; +import static java.util.Collections.singletonList; +import static java.util.Objects.requireNonNull; + +/** Collects rate limiter exposed {@link Metrics}. */ +public class RateLimiterMetricsCollector extends Collector { + + /** + * Creates a new collector with custom metric names and + * using given {@code supplier} as source of rate limiters. + * + * @param names the custom metric names + * @param supplier the supplier of rate limiters, note that supplier will be called one every {@link #collect()} + */ + public static RateLimiterMetricsCollector ofSupplier(MetricNames names, Supplier> supplier) { + return new RateLimiterMetricsCollector(names, supplier); + } + + /** + * Creates a new collector using given {@code supplier} as source of rate limiters. + * + * @param supplier the supplier of rate limiters, note that supplier will be called one very {@link #collect()} + */ + public static RateLimiterMetricsCollector ofSupplier(Supplier> supplier) { + return new RateLimiterMetricsCollector(MetricNames.ofDefaults(), supplier); + } + + /** + * Creates a new collector using given {@code registry} as source of rate limiters. + * + * @param registry the source of rate limiters + */ + public static RateLimiterMetricsCollector ofRateLimiterRegistry(RateLimiterRegistry registry) { + return new RateLimiterMetricsCollector(MetricNames.ofDefaults(), registry::getAllRateLimiters); + } + + /** + * Creates a new collector for given {@code rateLimiters}. + * + * @param rateLimiters the rate limiters to collect metrics for + */ + public static RateLimiterMetricsCollector ofIterable(Iterable rateLimiters) { + return new RateLimiterMetricsCollector(MetricNames.ofDefaults(), () -> rateLimiters); + } + + /** + * Creates a new collector for a given {@code rateLimiter}. + * + * @param rateLimiter the rate limiter to collect metrics for + */ + public static RateLimiterMetricsCollector ofRateLimiter(RateLimiter rateLimiter) { + return ofIterable(singletonList(rateLimiter)); + } + + private final MetricNames names; + private final Supplier> supplier; + + private RateLimiterMetricsCollector(MetricNames names, Supplier> supplier) { + this.names = requireNonNull(names); + this.supplier = requireNonNull(supplier); + } + + @Override + public List collect() { + GaugeMetricFamily availablePermissionsFamily = new GaugeMetricFamily( + names.getAvailablePermissionsMetricName(), + "The number of available permissions", + LabelNames.NAME + ); + GaugeMetricFamily waitingThreadsFamily = new GaugeMetricFamily( + names.getWaitingThreadsMetricName(), + "The number of waiting threads", + LabelNames.NAME + ); + + for (RateLimiter rateLimiter : supplier.get()) { + List nameLabel = singletonList(rateLimiter.getName()); + availablePermissionsFamily.addMetric(nameLabel, rateLimiter.getMetrics().getAvailablePermissions()); + waitingThreadsFamily.addMetric(nameLabel, rateLimiter.getMetrics().getNumberOfWaitingThreads()); + } + + return Arrays.asList(availablePermissionsFamily, waitingThreadsFamily); + } + + /** Defines possible configuration for metric names. */ + public static class MetricNames { + + public static final String DEFAULT_AVAILABLE_PERMISSIONS_METRIC_NAME = "resilience4j_ratelimiter_available_permissions"; + public static final String DEFAULT_WAITING_THREADS_METRIC_NAME = "resilience4j_ratelimiter_waiting_threads"; + + /** + * Returns a builder for creating custom metric names. + * Note that names have default values, so only desired metrics can be renamed. + */ + public static Builder custom() { + return new Builder(); + } + + /** Returns default metric names. */ + public static MetricNames ofDefaults() { + return new MetricNames(); + } + + private String availablePermissionsMetricName = DEFAULT_AVAILABLE_PERMISSIONS_METRIC_NAME; + private String waitingThreadsMetricName = DEFAULT_WAITING_THREADS_METRIC_NAME; + + /** Returns the metric name for available permissions, defaults to {@value DEFAULT_AVAILABLE_PERMISSIONS_METRIC_NAME}. */ + public String getAvailablePermissionsMetricName() { + return availablePermissionsMetricName; + } + + /** Returns the metric name for waiting threads, defaults to {@value DEFAULT_WAITING_THREADS_METRIC_NAME}. */ + public String getWaitingThreadsMetricName() { + return waitingThreadsMetricName; + } + + /** Helps building custom instance of {@link MetricNames}. */ + public static class Builder { + + private final MetricNames metricNames = new MetricNames(); + + /** Overrides the default metric name {@value MetricNames#DEFAULT_AVAILABLE_PERMISSIONS_METRIC_NAME} with a given one. */ + public Builder availablePermissionsMetricName(String availablePermissionsMetricName) { + metricNames.availablePermissionsMetricName = requireNonNull(availablePermissionsMetricName); + return this; + } + + /** Overrides the default metric name {@value MetricNames#DEFAULT_WAITING_THREADS_METRIC_NAME} with a given one. */ + public Builder waitingThreadsMetricName(String waitingThreadsMetricName) { + metricNames.waitingThreadsMetricName = requireNonNull(waitingThreadsMetricName); + return this; + } + + /** Builds {@link MetricNames} instance. */ + public MetricNames build() { + return metricNames; + } + } + } +} diff --git a/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/collectors/BulkheadMetricsCollectorTest.java b/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/collectors/BulkheadMetricsCollectorTest.java new file mode 100644 index 0000000000..2aeb26efef --- /dev/null +++ b/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/collectors/BulkheadMetricsCollectorTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019 Yevhenii Voievodin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.prometheus.collectors; + +import io.github.resilience4j.bulkhead.Bulkhead; +import io.prometheus.client.CollectorRegistry; +import org.junit.Before; +import org.junit.Test; + +import static io.github.resilience4j.prometheus.collectors.BulkheadMetricsCollector.MetricNames.DEFAULT_BULKHEAD_AVAILABLE_CONCURRENT_CALLS_METRIC_NAME; +import static io.github.resilience4j.prometheus.collectors.BulkheadMetricsCollector.MetricNames.DEFAULT_BULKHEAD_MAX_ALLOWED_CONCURRENT_CALLS_METRIC_NAME; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class BulkheadMetricsCollectorTest { + + CollectorRegistry registry; + Bulkhead bulkhead; + + @Before + public void setup() { + registry = new CollectorRegistry(); + bulkhead = Bulkhead.ofDefaults("backendA"); + // record some basic stats + bulkhead.isCallPermitted(); + bulkhead.isCallPermitted(); + + BulkheadMetricsCollector.ofBulkhead(bulkhead).register(registry); + } + + @Test + public void availableConcurrentCallsReportsCorrespondingValue() { + double availableCalls = registry.getSampleValue( + DEFAULT_BULKHEAD_AVAILABLE_CONCURRENT_CALLS_METRIC_NAME, + new String[]{"name"}, + new String[]{bulkhead.getName()} + ); + + assertThat(availableCalls).isEqualTo(bulkhead.getMetrics().getAvailableConcurrentCalls()); + } + + @Test + public void maxAllowedConcurrentCallsReportsCorrespondingValue() { + double maxAllowed = registry.getSampleValue( + DEFAULT_BULKHEAD_MAX_ALLOWED_CONCURRENT_CALLS_METRIC_NAME, + new String[]{"name"}, + new String[]{bulkhead.getName()} + ); + + assertThat(maxAllowed).isEqualTo(bulkhead.getMetrics().getMaxAllowedConcurrentCalls()); + } + + @Test + public void customMetricNamesOverrideDefaultOnes() { + CollectorRegistry registry = new CollectorRegistry(); + + BulkheadMetricsCollector.ofSupplier( + BulkheadMetricsCollector.MetricNames.custom() + .availableConcurrentCallsMetricName("custom_available_calls") + .maxAllowedConcurrentCallsMetricName("custom_max_allowed_calls") + .build(), + () -> singletonList(Bulkhead.ofDefaults("backendA")) + ).register(registry); + + assertThat(registry.getSampleValue( + "custom_available_calls", + new String[]{"name"}, + new String[]{"backendA"} + )).isNotNull(); + assertThat(registry.getSampleValue( + "custom_max_allowed_calls", + new String[]{"name"}, + new String[]{"backendA"} + )).isNotNull(); + } +} diff --git a/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/collectors/CircuitBreakerMetricsCollectorTest.java b/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/collectors/CircuitBreakerMetricsCollectorTest.java new file mode 100644 index 0000000000..5686b18e55 --- /dev/null +++ b/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/collectors/CircuitBreakerMetricsCollectorTest.java @@ -0,0 +1,158 @@ +/* + * Copyright 2019 Yevhenii Voievodin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.prometheus.collectors; + +import io.github.resilience4j.circuitbreaker.CircuitBreaker; +import io.prometheus.client.CollectorRegistry; +import org.junit.Before; +import org.junit.Test; + +import static io.github.resilience4j.prometheus.collectors.CircuitBreakerMetricsCollector.MetricNames.DEFAULT_CIRCUIT_BREAKER_BUFFERED_CALLS; +import static io.github.resilience4j.prometheus.collectors.CircuitBreakerMetricsCollector.MetricNames.DEFAULT_CIRCUIT_BREAKER_CALLS_METRIC_NAME; +import static io.github.resilience4j.prometheus.collectors.CircuitBreakerMetricsCollector.MetricNames.DEFAULT_CIRCUIT_BREAKER_MAX_BUFFERED_CALLS; +import static io.github.resilience4j.prometheus.collectors.CircuitBreakerMetricsCollector.MetricNames.DEFAULT_CIRCUIT_BREAKER_STATE_METRIC_NAME; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class CircuitBreakerMetricsCollectorTest { + + CollectorRegistry registry; + CircuitBreaker circuitBreaker; + + @Before + public void setup() { + registry = new CollectorRegistry(); + circuitBreaker = CircuitBreaker.ofDefaults("backendA"); + // record some basic stats + circuitBreaker.onSuccess(0); + circuitBreaker.onError(0, new RuntimeException("oops")); + circuitBreaker.transitionToOpenState(); + + CircuitBreakerMetricsCollector.ofCircuitBreaker(circuitBreaker).register(registry); + } + + @Test + public void stateReportsCorrespondingValue() { + double state = registry.getSampleValue( + DEFAULT_CIRCUIT_BREAKER_STATE_METRIC_NAME, + new String[]{"name"}, + new String[]{circuitBreaker.getName()} + ); + + assertThat(state).isEqualTo(circuitBreaker.getState().getOrder()); + } + + @Test + public void bufferedCallsReportsCorrespondingValue() { + double bufferedCalls = registry.getSampleValue( + DEFAULT_CIRCUIT_BREAKER_BUFFERED_CALLS, + new String[]{"name"}, + new String[]{circuitBreaker.getName()} + ); + + assertThat(bufferedCalls).isEqualTo(circuitBreaker.getMetrics().getNumberOfBufferedCalls()); + } + + @Test + public void maxBufferedCallsReportsCorrespondingValue() { + double maxBufferedCalls = registry.getSampleValue( + DEFAULT_CIRCUIT_BREAKER_MAX_BUFFERED_CALLS, + new String[]{"name"}, + new String[]{circuitBreaker.getName()} + ); + + assertThat(maxBufferedCalls).isEqualTo(circuitBreaker.getMetrics().getMaxNumberOfBufferedCalls()); + } + + @Test + public void successfulCallsReportsCorrespondingValue() { + double successfulCalls = registry.getSampleValue( + DEFAULT_CIRCUIT_BREAKER_CALLS_METRIC_NAME, + new String[]{"name", "kind"}, + new String[]{circuitBreaker.getName(), "successful"} + ); + + assertThat(successfulCalls).isEqualTo(circuitBreaker.getMetrics().getNumberOfSuccessfulCalls()); + } + + @Test + public void failedCallsReportsCorrespondingValue() { + double failedCalls = registry.getSampleValue( + DEFAULT_CIRCUIT_BREAKER_CALLS_METRIC_NAME, + new String[]{"name", "kind"}, + new String[]{circuitBreaker.getName(), "failed"} + ); + + assertThat(failedCalls).isEqualTo(circuitBreaker.getMetrics().getNumberOfFailedCalls()); + } + + @Test + public void notPermittedCallsReportsCorrespondingValue() { + double notPermitted = registry.getSampleValue( + DEFAULT_CIRCUIT_BREAKER_CALLS_METRIC_NAME, + new String[]{"name", "kind"}, + new String[]{circuitBreaker.getName(), "not_permitted"} + ); + + assertThat(notPermitted).isEqualTo(circuitBreaker.getMetrics().getNumberOfNotPermittedCalls()); + } + + @Test + public void customMetricNamesOverrideDefaultOnes() { + CollectorRegistry registry = new CollectorRegistry(); + + CircuitBreakerMetricsCollector.ofSupplier( + CircuitBreakerMetricsCollector.MetricNames.custom() + .callsMetricName("custom_calls") + .stateMetricName("custom_state") + .maxBufferedCallsMetricName("custom_max_buffered_calls") + .bufferedCallsMetricName("custom_buffered_calls") + .build(), + () -> singletonList(CircuitBreaker.ofDefaults("backendA")) + ).register(registry); + + assertThat(registry.getSampleValue( + "custom_calls", + new String[]{"name", "kind"}, + new String[]{"backendA", "successful"} + )).isNotNull(); + assertThat(registry.getSampleValue( + "custom_calls", + new String[]{"name", "kind"}, + new String[]{"backendA", "failed"} + )).isNotNull(); + assertThat(registry.getSampleValue( + "custom_calls", + new String[]{"name", "kind"}, + new String[]{"backendA", "not_permitted"} + )).isNotNull(); + assertThat(registry.getSampleValue( + "custom_state", + new String[]{"name"}, + new String[]{"backendA"} + )).isNotNull(); + assertThat(registry.getSampleValue( + "custom_max_buffered_calls", + new String[]{"name"}, + new String[]{"backendA"} + )).isNotNull(); + assertThat(registry.getSampleValue( + "custom_buffered_calls", + new String[]{"name"}, + new String[]{"backendA"} + )).isNotNull(); + } +} diff --git a/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/collectors/RateLimiterMetricsCollectorTest.java b/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/collectors/RateLimiterMetricsCollectorTest.java new file mode 100644 index 0000000000..d446700cfb --- /dev/null +++ b/resilience4j-prometheus/src/test/java/io/github/resilience4j/prometheus/collectors/RateLimiterMetricsCollectorTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2019 Yevhenii Voievodin + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.resilience4j.prometheus.collectors; + +import io.github.resilience4j.ratelimiter.RateLimiter; +import io.prometheus.client.CollectorRegistry; +import org.junit.Before; +import org.junit.Test; + +import static io.github.resilience4j.prometheus.collectors.RateLimiterMetricsCollector.MetricNames.DEFAULT_AVAILABLE_PERMISSIONS_METRIC_NAME; +import static io.github.resilience4j.prometheus.collectors.RateLimiterMetricsCollector.MetricNames.DEFAULT_WAITING_THREADS_METRIC_NAME; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class RateLimiterMetricsCollectorTest { + + CollectorRegistry registry; + RateLimiter rateLimiter; + + @Before + public void setup() { + registry = new CollectorRegistry(); + rateLimiter = RateLimiter.ofDefaults("backendA"); + + RateLimiterMetricsCollector.ofRateLimiter(rateLimiter).register(registry); + } + + @Test + public void availablePermissionsReportsCorrespondingValue() { + double availablePermissions = registry.getSampleValue( + DEFAULT_AVAILABLE_PERMISSIONS_METRIC_NAME, + new String[]{"name"}, + new String[]{rateLimiter.getName()} + ); + + assertThat(availablePermissions).isEqualTo(rateLimiter.getMetrics().getAvailablePermissions()); + } + + @Test + public void waitingThreadsReportsCorrespondingValue() { + double waitingThreads = registry.getSampleValue( + DEFAULT_WAITING_THREADS_METRIC_NAME, + new String[]{"name"}, + new String[]{rateLimiter.getName()} + ); + + assertThat(waitingThreads).isEqualTo(rateLimiter.getMetrics().getNumberOfWaitingThreads()); + } + + @Test + public void customMetricNamesOverrideDefaultOnes() { + CollectorRegistry registry = new CollectorRegistry(); + + RateLimiterMetricsCollector.ofSupplier( + RateLimiterMetricsCollector.MetricNames.custom() + .availablePermissionsMetricName("custom_available_permissions") + .waitingThreadsMetricName("custom_waiting_threads") + .build(), + () -> singletonList(RateLimiter.ofDefaults("backendA")) + ).register(registry); + + assertThat(registry.getSampleValue( + "custom_available_permissions", + new String[]{"name"}, + new String[]{"backendA"} + )).isNotNull(); + assertThat(registry.getSampleValue( + "custom_waiting_threads", + new String[]{"name"}, + new String[]{"backendA"} + )).isNotNull(); + } +}