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); + } +}