diff --git a/pom.xml b/pom.xml
index de17b7af10..e4cf4e5f53 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,6 +64,7 @@
1.3.8
2.0.3
2.13.3
+ 1.6.0
3.5.15
4.1.53.Final
2.0.19
@@ -236,6 +237,13 @@
true
+
+ io.micrometer
+ micrometer-core
+ ${micrometer.version}
+ true
+
+
org.hdrhistogram
HdrHistogram
diff --git a/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyCollectorOptions.java b/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyCollectorOptions.java
new file mode 100644
index 0000000000..5c1b25eb51
--- /dev/null
+++ b/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyCollectorOptions.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2011-2020 the original author or authors.
+ *
+ * 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
+ *
+ * https://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.lettuce.core.metrics;
+
+import io.lettuce.core.internal.LettuceAssert;
+import io.micrometer.core.instrument.Tags;
+
+import java.time.Duration;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * The Micrometer implementation of {@link CommandLatencyCollectorOptions}.
+ *
+ * @author Steven Sheehy
+ */
+public class MicrometerCommandLatencyCollectorOptions implements CommandLatencyCollectorOptions {
+
+ public static final boolean DEFAULT_ENABLED = true;
+
+ public static final boolean DEFAULT_HISTOGRAM = false;
+
+ public static final boolean DEFAULT_LOCAL_DISTINCTION = false;
+
+ public static final Duration DEFAULT_MAX_LATENCY = Duration.ofMinutes(5L);
+
+ public static final Duration DEFAULT_MIN_LATENCY = Duration.ofMillis(1L);
+
+ public static final double[] DEFAULT_TARGET_PERCENTILES = new double[] { 0.50, 0.90, 0.95, 0.99, 0.999 };
+
+ private static final MicrometerCommandLatencyCollectorOptions DISABLED = builder().disable().build();
+
+ private final Builder builder;
+
+ private final boolean enabled;
+
+ private final boolean histogram;
+
+ private final boolean localDistinction;
+
+ private final Duration maxLatency;
+
+ private final Duration minLatency;
+
+ private final Tags tags;
+
+ private final double[] targetPercentiles;
+
+ protected MicrometerCommandLatencyCollectorOptions(Builder builder) {
+ this.builder = builder;
+ this.enabled = builder.enabled;
+ this.histogram = builder.histogram;
+ this.localDistinction = builder.localDistinction;
+ this.maxLatency = builder.maxLatency;
+ this.minLatency = builder.minLatency;
+ this.tags = builder.tags;
+ this.targetPercentiles = builder.targetPercentiles;
+ }
+
+ /**
+ * Create a new {@link MicrometerCommandLatencyCollectorOptions} instance using default settings.
+ *
+ * @return a new instance of {@link MicrometerCommandLatencyCollectorOptions} instance using default settings
+ */
+ public static MicrometerCommandLatencyCollectorOptions create() {
+ return builder().build();
+ }
+
+ /**
+ * Create a {@link MicrometerCommandLatencyCollectorOptions} instance with disabled event emission.
+ *
+ * @return a new instance of {@link MicrometerCommandLatencyCollectorOptions} with disabled event emission
+ */
+ public static MicrometerCommandLatencyCollectorOptions disabled() {
+ return DISABLED;
+ }
+
+ /**
+ * Returns a new {@link MicrometerCommandLatencyCollectorOptions.Builder} to construct
+ * {@link MicrometerCommandLatencyCollectorOptions}.
+ *
+ * @return a new {@link MicrometerCommandLatencyCollectorOptions.Builder} to construct
+ * {@link MicrometerCommandLatencyCollectorOptions}.
+ */
+ public static MicrometerCommandLatencyCollectorOptions.Builder builder() {
+ return new MicrometerCommandLatencyCollectorOptions.Builder();
+ }
+
+ /**
+ * Returns a builder to create new {@link MicrometerCommandLatencyCollectorOptions} whose settings are replicated from the
+ * current {@link MicrometerCommandLatencyCollectorOptions}.
+ *
+ * @return a a {@link CommandLatencyCollectorOptions.Builder} to create new {@link MicrometerCommandLatencyCollectorOptions}
+ * whose settings are replicated from the current {@link MicrometerCommandLatencyCollectorOptions}
+ *
+ * @since 5.1
+ */
+ @Override
+ public MicrometerCommandLatencyCollectorOptions.Builder mutate() {
+ return this.builder;
+ }
+
+ /**
+ * Builder for {@link MicrometerCommandLatencyCollectorOptions}.
+ */
+ public static class Builder implements CommandLatencyCollectorOptions.Builder {
+
+ private boolean enabled = DEFAULT_ENABLED;
+
+ private boolean histogram = DEFAULT_HISTOGRAM;
+
+ private boolean localDistinction = DEFAULT_LOCAL_DISTINCTION;
+
+ private Duration maxLatency = DEFAULT_MAX_LATENCY;
+
+ private Duration minLatency = DEFAULT_MIN_LATENCY;
+
+ private Tags tags = Tags.empty();
+
+ private double[] targetPercentiles = DEFAULT_TARGET_PERCENTILES;
+
+ private Builder() {
+ }
+
+ /**
+ * Disable the latency collector.
+ *
+ * @return this {@link Builder}.
+ */
+ @Override
+ public Builder disable() {
+ this.enabled = false;
+ return this;
+ }
+
+ /**
+ * Enable the latency collector.
+ *
+ * @return this {@link Builder}.
+ */
+ @Override
+ public Builder enable() {
+ this.enabled = true;
+ return this;
+ }
+
+ /**
+ * Enable histogram buckets used to generate aggregable percentile approximations in monitoring
+ * systems that have query facilities to do so.
+ *
+ * @param histogram {@code true} if histogram buckets are recorded
+ * @return this {@link Builder}.
+ */
+ public Builder histogram(boolean histogram) {
+ this.histogram = histogram;
+ return this;
+ }
+
+ /**
+ * Enables per connection metrics tracking insead of per host/port. If {@code true}, multiple connections to the same
+ * host/connection point will be recorded separately which allows to inspect every connection individually. If
+ * {@code false}, multiple connections to the same host/connection point will be recorded together. This allows a
+ * consolidated view on one particular service. Defaults to {@code false}. See
+ * {@link MicrometerCommandLatencyCollectorOptions#DEFAULT_LOCAL_DISTINCTION}.
+ *
+ * Warning: Enabling this could potentially cause a label cardinality explosion in the remote metric system and
+ * should be used with caution.
+ *
+ * @param localDistinction {@code true} if latencies are recorded distinct on local level (per connection)
+ * @return this {@link Builder}.
+ */
+ @Override
+ public Builder localDistinction(boolean localDistinction) {
+ this.localDistinction = localDistinction;
+ return this;
+ }
+
+ /**
+ * Sets the maximum value that this timer is expected to observe. Sets an upper bound
+ * on histogram buckets that are shipped to monitoring systems that support aggregable percentile approximations.
+ * Only applicable when histogram is enabled. Defaults to {@code 5m}.
+ * See {@link MicrometerCommandLatencyCollectorOptions#DEFAULT_MAX_LATENCY}.
+ *
+ * @param maxLatency The maximum value that this timer is expected to observe.
+ * @return this {@link Builder}.
+ */
+ public Builder maxLatency(Duration maxLatency) {
+ this.maxLatency = maxLatency;
+ return this;
+ }
+
+ /**
+ * Sets the minimum value that this timer is expected to observe. Sets a lower bound
+ * on histogram buckets that are shipped to monitoring systems that support aggregable percentile approximations.
+ * Only applicable when histogram is enabled. Defaults to {@code 1ms}.
+ * See {@link MicrometerCommandLatencyCollectorOptions#DEFAULT_MIN_LATENCY}.
+ *
+ * @param minLatency The minimum value that this timer is expected to observe.
+ * @return this {@link Builder}.
+ */
+ public Builder minLatency(Duration minLatency) {
+ this.minLatency = minLatency;
+ return this;
+ }
+
+ /**
+ * Not supported since the MeterRegistry implementation defines whether metrics are cumulative or reset
+ *
+ * @param resetLatenciesAfterEvent {@code true} if the recorded latencies should be reset once the metrics event was
+ * emitted
+ * @return this {@link Builder}.
+ */
+ @Override
+ public Builder resetLatenciesAfterEvent(boolean resetLatenciesAfterEvent) {
+ throw new UnsupportedOperationException("resetLatenciesAfterEvent not supported for Micrometer");
+ }
+
+ /**
+ * Extra tags to add to the generated metrics. Defaults to {@code Tags.empty()}.
+ *
+ * @param tags Tags to add to the metrics.
+ * @return this {@link Builder}.
+ */
+ public Builder tags(Tags tags) {
+ this.tags = tags;
+ return this;
+ }
+
+ /**
+ * Sets the emitted percentiles. Defaults to 0.50, 0.90, 0.95, 0.99, 0.999}. Only applicable when histogram is enabled.
+ * See {@link MicrometerCommandLatencyCollectorOptions#DEFAULT_TARGET_PERCENTILES}.
+ *
+ * @param targetPercentiles the percentiles which should be emitted, must not be {@code null}
+ * @return this {@link Builder}.
+ */
+ @Override
+ public Builder targetPercentiles(double[] targetPercentiles) {
+ LettuceAssert.notNull(targetPercentiles, "TargetPercentiles must not be null");
+ this.targetPercentiles = targetPercentiles;
+ return this;
+ }
+
+ /**
+ * Not supported since the MeterRegistry implementation defines the base unit and cannot be changed.
+ *
+ * @param targetUnit the target unit
+ * @return this {@link Builder}.
+ */
+ @Override
+ public Builder targetUnit(TimeUnit targetUnit) {
+ throw new UnsupportedOperationException("targetUnit not supported for Micrometer");
+ }
+
+ /**
+ * @return a new instance of {@link MicrometerCommandLatencyCollectorOptions}.
+ */
+ @Override
+ public MicrometerCommandLatencyCollectorOptions build() {
+ return new MicrometerCommandLatencyCollectorOptions(this);
+ }
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return enabled;
+ }
+
+ public boolean isHistogram() {
+ return histogram;
+ }
+
+ @Override
+ public boolean localDistinction() {
+ return localDistinction;
+ }
+
+ public Duration maxLatency() {
+ return maxLatency;
+ }
+
+ public Duration minLatency() {
+ return minLatency;
+ }
+
+ @Override
+ public boolean resetLatenciesAfterEvent() {
+ throw new UnsupportedOperationException("resetLatenciesAfterEvent not supported for Micrometer");
+ }
+
+ public Tags tags() {
+ return tags;
+ }
+
+ @Override
+ public double[] targetPercentiles() {
+ double[] result = new double[targetPercentiles.length];
+ System.arraycopy(targetPercentiles, 0, result, 0, targetPercentiles.length);
+ return result;
+ }
+
+ @Override
+ public TimeUnit targetUnit() {
+ throw new UnsupportedOperationException("targetUnit not supported for Micrometer");
+ }
+}
diff --git a/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java b/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java
new file mode 100644
index 0000000000..00a3de7af1
--- /dev/null
+++ b/src/main/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorder.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2011-2020 the original author or authors.
+ *
+ * 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
+ *
+ * https://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.lettuce.core.metrics;
+
+import io.lettuce.core.protocol.ProtocolKeyword;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Timer;
+import io.netty.channel.local.LocalAddress;
+
+import java.net.SocketAddress;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author Steven Sheehy
+ */
+public class MicrometerCommandLatencyRecorder implements CommandLatencyRecorder {
+
+ static final String LABEL_COMMAND = "command";
+
+ static final String LABEL_LOCAL = "local";
+
+ static final String LABEL_REMOTE = "remote";
+
+ static final String METRIC_COMPLETION = "lettuce.command.completion";
+
+ static final String METRIC_FIRST_RESPONSE = "lettuce.command.firstresponse";
+
+ private final MeterRegistry meterRegistry;
+
+ private final MicrometerCommandLatencyCollectorOptions options;
+
+ private final Map completionTimers = new ConcurrentHashMap<>();
+
+ private final Map firstResponseTimers = new ConcurrentHashMap<>();
+
+ public MicrometerCommandLatencyRecorder(MeterRegistry meterRegistry, MicrometerCommandLatencyCollectorOptions options) {
+ this.meterRegistry = meterRegistry;
+ this.options = options;
+ }
+
+ @Override
+ public void recordCommandLatency(SocketAddress local, SocketAddress remote, ProtocolKeyword protocolKeyword,
+ long firstResponseLatency, long completionLatency) {
+
+ if (!isEnabled()) {
+ return;
+ }
+
+ CommandLatencyId commandLatencyId = createId(local, remote, protocolKeyword);
+ Timer firstResponseTimer = firstResponseTimers.computeIfAbsent(commandLatencyId, this::firstResponseTimer);
+ firstResponseTimer.record(firstResponseLatency, TimeUnit.NANOSECONDS);
+
+ Timer completionTimer = completionTimers.computeIfAbsent(commandLatencyId, this::completionTimer);
+ completionTimer.record(completionLatency, TimeUnit.NANOSECONDS);
+ }
+
+ @Override
+ public boolean isEnabled() {
+ return options.isEnabled();
+ }
+
+ private CommandLatencyId createId(SocketAddress local, SocketAddress remote, ProtocolKeyword commandType) {
+ return CommandLatencyId.create(options.localDistinction() ? local : LocalAddress.ANY, remote, commandType);
+ }
+
+ protected Timer completionTimer(CommandLatencyId commandLatencyId) {
+ Timer.Builder timer = Timer.builder(METRIC_COMPLETION)
+ .description("Latency between command send and command completion (complete response received")
+ .tag(LABEL_COMMAND, commandLatencyId.commandType().name())
+ .tag(LABEL_LOCAL, commandLatencyId.localAddress().toString())
+ .tag(LABEL_REMOTE, commandLatencyId.remoteAddress().toString())
+ .tags(options.tags());
+
+ if (options.isHistogram()) {
+ timer.publishPercentileHistogram()
+ .publishPercentiles(options.targetPercentiles())
+ .minimumExpectedValue(options.minLatency())
+ .maximumExpectedValue(options.maxLatency());
+ }
+
+ return timer.register(meterRegistry);
+ }
+
+ protected Timer firstResponseTimer(CommandLatencyId commandLatencyId) {
+ Timer.Builder timer = Timer.builder(METRIC_FIRST_RESPONSE)
+ .description("Latency between command send and first response (first response received)")
+ .tag(LABEL_COMMAND, commandLatencyId.commandType().name())
+ .tag(LABEL_LOCAL, commandLatencyId.localAddress().toString())
+ .tag(LABEL_REMOTE, commandLatencyId.remoteAddress().toString())
+ .tags(options.tags());
+
+ if (options.isHistogram()) {
+ timer.publishPercentileHistogram()
+ .publishPercentiles(options.targetPercentiles())
+ .minimumExpectedValue(options.minLatency())
+ .maximumExpectedValue(options.maxLatency());
+ }
+
+ return timer.register(meterRegistry);
+ }
+}
diff --git a/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyCollectorOptionsUnitTests.java b/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyCollectorOptionsUnitTests.java
new file mode 100644
index 0000000000..be1e8346a0
--- /dev/null
+++ b/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyCollectorOptionsUnitTests.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2011-2020 the original author or authors.
+ *
+ * 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
+ *
+ * https://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.lettuce.core.metrics;
+
+import io.micrometer.core.instrument.Tags;
+import org.junit.jupiter.api.Test;
+
+import java.time.Duration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static io.lettuce.core.metrics.MicrometerCommandLatencyCollectorOptions.*;
+
+/**
+ * @author Steven Sheehy
+ */
+class MicrometerCommandLatencyCollectorOptionsUnitTests {
+
+ @Test
+ void create() {
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.create();
+
+ assertThat(options.isEnabled()).isEqualTo(DEFAULT_ENABLED);
+ assertThat(options.isHistogram()).isEqualTo(DEFAULT_HISTOGRAM);
+ assertThat(options.localDistinction()).isEqualTo(DEFAULT_LOCAL_DISTINCTION);
+ assertThat(options.maxLatency()).isEqualTo(DEFAULT_MAX_LATENCY);
+ assertThat(options.minLatency()).isEqualTo(DEFAULT_MIN_LATENCY);
+ assertThat(options.tags()).isEqualTo(Tags.empty());
+ assertThat(options.targetPercentiles()).isEqualTo(DEFAULT_TARGET_PERCENTILES);
+ }
+
+ @Test
+ void disabled() {
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.disabled();
+
+ assertThat(options.isEnabled()).isFalse();
+ }
+
+ @Test
+ void histogram() {
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.builder()
+ .histogram(true)
+ .build();
+
+ assertThat(options.isHistogram()).isTrue();
+ }
+
+ @Test
+ void localDistinction() {
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.builder()
+ .localDistinction(true)
+ .build();
+
+ assertThat(options.localDistinction()).isTrue();
+ }
+
+ @Test
+ void maxLatency() {
+ Duration maxLatency = Duration.ofSeconds(2L);
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.builder()
+ .maxLatency(maxLatency)
+ .build();
+
+ assertThat(options.maxLatency()).isEqualTo(maxLatency);
+ }
+
+ @Test
+ void minLatency() {
+ Duration minLatency = Duration.ofSeconds(2L);
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.builder()
+ .minLatency(minLatency)
+ .build();
+
+ assertThat(options.minLatency()).isEqualTo(minLatency);
+ }
+
+ @Test
+ void targetPercentiles() {
+ double[] percentiles = new double[] { 0.1, 0.2, 0.3 };
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.builder()
+ .targetPercentiles(percentiles).build();
+
+ assertThat(options.targetPercentiles()).hasSize(3).isEqualTo(percentiles);
+ }
+}
diff --git a/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorderUnitTests.java b/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorderUnitTests.java
new file mode 100644
index 0000000000..d5912cce0f
--- /dev/null
+++ b/src/test/java/io/lettuce/core/metrics/MicrometerCommandLatencyRecorderUnitTests.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright 2011-2020 the original author or authors.
+ *
+ * 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
+ *
+ * https://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.lettuce.core.metrics;
+
+import com.google.common.primitives.Doubles;
+import io.lettuce.core.protocol.CommandType;
+import io.micrometer.core.instrument.MeterRegistry;
+import io.micrometer.core.instrument.Tags;
+import io.micrometer.core.instrument.Timer;
+import io.micrometer.core.instrument.distribution.HistogramSnapshot;
+import io.micrometer.core.instrument.distribution.ValueAtPercentile;
+import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
+import io.netty.channel.local.LocalAddress;
+import org.assertj.core.api.InstanceOfAssertFactories;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.net.SocketAddress;
+
+import static io.lettuce.core.metrics.MicrometerCommandLatencyRecorder.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Steven Sheehy
+ */
+@ExtendWith(MockitoExtension.class)
+class MicrometerCommandLatencyRecorderUnitTests {
+
+ private static final SocketAddress LOCAL_ADDRESS = new LocalAddress("localhost:54689");
+
+ private static final SocketAddress REMOTE_ADDRESS = new LocalAddress("localhost:6379");
+
+ private final MeterRegistry meterRegistry = new SimpleMeterRegistry();
+
+ @Test
+ void verifyMetrics() {
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.create();
+ MicrometerCommandLatencyRecorder commandLatencyRecorder = new MicrometerCommandLatencyRecorder(meterRegistry, options);
+
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.AUTH, 1, 10);
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.BGSAVE, 100, 500);
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.BGSAVE, 200, 1000);
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.BGSAVE, 300, 1500);
+
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).timers()).hasSize(2);
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).tag(LABEL_COMMAND, CommandType.BGSAVE.name()).timers())
+ .hasSize(1)
+ .element(0)
+ .extracting(Timer::takeSnapshot)
+ .hasFieldOrPropertyWithValue("count", 3L)
+ .hasFieldOrPropertyWithValue("max", 300.0)
+ .hasFieldOrPropertyWithValue("mean", 200.0)
+ .hasFieldOrPropertyWithValue("total", 600.0);
+
+ assertThat(meterRegistry.find(METRIC_COMPLETION).timers()).hasSize(2);
+ assertThat(meterRegistry.find(METRIC_COMPLETION).tag(LABEL_COMMAND, CommandType.BGSAVE.name()).timers())
+ .hasSize(1)
+ .element(0)
+ .extracting(Timer::takeSnapshot)
+ .hasFieldOrPropertyWithValue("count", 3L)
+ .hasFieldOrPropertyWithValue("max", 1500.0)
+ .hasFieldOrPropertyWithValue("mean", 1000.0)
+ .hasFieldOrPropertyWithValue("total", 3000.0);
+ }
+
+ @Test
+ void disabled() {
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.disabled();
+ MicrometerCommandLatencyRecorder commandLatencyRecorder = new MicrometerCommandLatencyRecorder(meterRegistry, options);
+
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.AUTH, 1, 10);
+ assertThat(meterRegistry.find(METRIC_COMPLETION).timers()).isEmpty();
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).timers()).isEmpty();
+ }
+
+ @Test
+ void histogramEnabled() {
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.builder()
+ .histogram(true)
+ .build();
+ MicrometerCommandLatencyRecorder commandLatencyRecorder = new MicrometerCommandLatencyRecorder(meterRegistry, options);
+
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.AUTH, 1, 10);
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.AUTH, 2, 5);
+
+ assertThat(meterRegistry.find(METRIC_COMPLETION).timers())
+ .hasSize(1)
+ .element(0)
+ .extracting(Timer::takeSnapshot)
+ .extracting(HistogramSnapshot::percentileValues, InstanceOfAssertFactories.array(ValueAtPercentile[].class))
+ .extracting(ValueAtPercentile::percentile)
+ .containsExactlyElementsOf(Doubles.asList(options.targetPercentiles()));
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).timers())
+ .hasSize(1)
+ .element(0)
+ .extracting(Timer::takeSnapshot)
+ .extracting(HistogramSnapshot::percentileValues, InstanceOfAssertFactories.array(ValueAtPercentile[].class))
+ .extracting(ValueAtPercentile::percentile)
+ .containsExactlyElementsOf(Doubles.asList(options.targetPercentiles()));
+ }
+
+ @Test
+ void localDistinctionEnabled() {
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.builder()
+ .localDistinction(true)
+ .build();
+ MicrometerCommandLatencyRecorder commandLatencyRecorder = new MicrometerCommandLatencyRecorder(meterRegistry, options);
+ LocalAddress localAddress2 = new LocalAddress("localhost:12345");
+
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.AUTH, 1, 10);
+ commandLatencyRecorder.recordCommandLatency(localAddress2, REMOTE_ADDRESS, CommandType.AUTH, 1, 10);
+
+ assertThat(meterRegistry.find(METRIC_COMPLETION).tagKeys(LABEL_LOCAL).timers()).hasSize(2);
+ assertThat(meterRegistry.find(METRIC_COMPLETION).tag(LABEL_LOCAL, LOCAL_ADDRESS.toString()).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_COMPLETION).tag(LABEL_LOCAL, localAddress2.toString()).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).tagKeys(LABEL_LOCAL).timers()).hasSize(2);
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).tag(LABEL_LOCAL, LOCAL_ADDRESS.toString()).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).tag(LABEL_LOCAL, localAddress2.toString()).timers()).hasSize(1);
+ }
+
+ @Test
+ void localDistinctionDisabled() {
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.builder()
+ .localDistinction(false)
+ .build();
+ MicrometerCommandLatencyRecorder commandLatencyRecorder = new MicrometerCommandLatencyRecorder(meterRegistry, options);
+ LocalAddress localAddress2 = new LocalAddress("localhost:12345");
+
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.AUTH, 1, 10);
+ commandLatencyRecorder.recordCommandLatency(localAddress2, REMOTE_ADDRESS, CommandType.AUTH, 1, 10);
+
+ assertThat(meterRegistry.find(METRIC_COMPLETION).tagKeys(LABEL_LOCAL).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_COMPLETION).tag(LABEL_LOCAL, LocalAddress.ANY.toString()).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).tagKeys(LABEL_LOCAL).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).tag(LABEL_LOCAL, LocalAddress.ANY.toString()).timers()).hasSize(1);
+ }
+
+ @Test
+ void tags() {
+ Tags tags = Tags.of("app", "foo");
+ MicrometerCommandLatencyCollectorOptions options = MicrometerCommandLatencyCollectorOptions.builder()
+ .tags(tags)
+ .build();
+ MicrometerCommandLatencyRecorder commandLatencyRecorder = new MicrometerCommandLatencyRecorder(meterRegistry, options);
+
+ commandLatencyRecorder.recordCommandLatency(LOCAL_ADDRESS, REMOTE_ADDRESS, CommandType.AUTH, 1, 10);
+
+ assertThat(meterRegistry.find(METRIC_COMPLETION).tags(tags).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_COMPLETION).tag(LABEL_COMMAND, CommandType.AUTH.name()).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_COMPLETION).tag(LABEL_REMOTE, REMOTE_ADDRESS.toString()).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).tags(tags).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).tag(LABEL_COMMAND, CommandType.AUTH.name()).timers()).hasSize(1);
+ assertThat(meterRegistry.find(METRIC_FIRST_RESPONSE).tag(LABEL_REMOTE, REMOTE_ADDRESS.toString()).timers()).hasSize(1);
+ }
+}